diff --git a/admin-frontend-vue/src/layout/AdminLayout.vue b/admin-frontend-vue/src/layout/AdminLayout.vue
index 4a7ed67d..347641f7 100644
--- a/admin-frontend-vue/src/layout/AdminLayout.vue
+++ b/admin-frontend-vue/src/layout/AdminLayout.vue
@@ -12,6 +12,7 @@ import {
shouldShowEmptyMenu,
} from '@/layout/empty-state'
import GlobalErrorContainer from '@/layout/GlobalErrorContainer.vue'
+import OperationGuide from '@/layout/OperationGuide.vue'
import { crumbsForActiveRoute, resolveDocumentTitle, updateDocumentTitle } from '@/layout/title-breadcrumb'
import {
LOGOUT_CONFIRM_CANCEL,
@@ -110,6 +111,7 @@ async function signOut() {
{{ crumb.name }}
+
+/** 当前页面操作提示条:对齐旧版 admin-interactions.js 的 adminOperationGuide(14 页有独立文案,余用通用 fallback)。
+ * 挂在 AdminLayout 内容区顶部,随 route.meta.menuKey 切换文案;折叠态存 localStorage。 */
+import { computed, ref, watch } from 'vue'
+import { useRoute } from 'vue-router'
+import { OPERATION_GUIDE_STORAGE_KEY, operationGuideFor } from '@/layout/operation-guides'
+
+const route = useRoute()
+const guide = computed(() => operationGuideFor(route.meta.menuKey))
+
+function readCollapsed(): boolean {
+ try {
+ return localStorage.getItem(OPERATION_GUIDE_STORAGE_KEY) === '1'
+ } catch {
+ return false
+ }
+}
+function persistCollapsed(value: boolean) {
+ try {
+ localStorage.setItem(OPERATION_GUIDE_STORAGE_KEY, value ? '1' : '0')
+ } catch {
+ // 忽略隐私模式写入失败
+ }
+}
+const collapsed = ref(readCollapsed())
+function toggle() {
+ collapsed.value = !collapsed.value
+ persistCollapsed(collapsed.value)
+}
+watch(collapsed, (v) => persistCollapsed(v))
+
+
+
+
+
+
+
diff --git a/admin-frontend-vue/src/layout/operation-guides.ts b/admin-frontend-vue/src/layout/operation-guides.ts
new file mode 100644
index 00000000..1b837a8e
--- /dev/null
+++ b/admin-frontend-vue/src/layout/operation-guides.ts
@@ -0,0 +1,86 @@
+/**
+ * 每菜单页的操作提示(guides)文案,对齐旧版 backend/static/admin-interactions.js 的 guides 表原文
+ * (除 group-manage / duplicate-check 用通用 fallback)。key = 路由 meta.menuKey。
+ */
+export interface OperationGuideData {
+ text: string
+ steps: string[]
+}
+
+export const OPERATION_GUIDE_FALLBACK: OperationGuideData = {
+ text: '先使用筛选定位记录,再进行新增、编辑、导出等操作。涉及删除的数据会要求二次确认。',
+ steps: ['选择筛选条件', '处理记录', '核对反馈'],
+}
+
+/** 折叠状态 localStorage key(沿用旧版 shufuAdminGuideCollapsed 语义)。 */
+export const OPERATION_GUIDE_STORAGE_KEY = 'shufuAdminGuideCollapsed'
+
+export const OPERATION_GUIDES: Record = {
+ admin_users: {
+ text: '先用筛选定位账号,再编辑角色和菜单权限;删除账号会要求二次确认。',
+ steps: ['筛选账号', '编辑权限', '确认保存'],
+ },
+ admin_columns: {
+ text: '菜单会影响后台和软件端的可见范围。先填写名称并选择对应页面,再设置上级菜单;顺序直接拖动列表左侧手柄调整。',
+ steps: ['新增或调整菜单', '设置层级', '拖动排序'],
+ },
+ admin_group_manage: OPERATION_GUIDE_FALLBACK,
+ admin_dedupe_total_data: {
+ text: '支持按关键词、用户、分组和国家筛选;导入前先确认所选分组,导出会按日期范围生成文件。',
+ steps: ['选择分组', '筛选或导入', '核对并导出'],
+ },
+ admin_invalid_asin_data: {
+ text: '维护不符合规则的 ASIN 或品牌。添加后可使用上方筛选快速回查。',
+ steps: ['填写 ASIN/品牌', '选择分组', '保存并回查'],
+ },
+ admin_query_asin: {
+ text: '查询 ASIN 以店铺为单位维护。列表里点击 ASIN 即可复制,点右侧「配置」可一次维护该店铺所有站点的 ASIN。',
+ steps: ['筛选店铺', '打开配置抽屉', '保存或导出'],
+ },
+ admin_product_categories: {
+ text: '类目树支持展开查看层级。搜索、编辑和删除都在同一列表中完成。',
+ steps: ['搜索类目', '展开层级', '新增或编辑'],
+ },
+ admin_skip_price_asin: {
+ text: '最低价 ASIN 以店铺为单位维护。列表里点击 ASIN 即可复制,点右侧「配置」可一次维护该店铺所有站点的 ASIN 与最低价。',
+ steps: ['筛选店铺', '打开配置抽屉', '保存或批量导入'],
+ },
+ admin_shop_keys: {
+ text: '紫鸟令牌属于敏感配置。白名单状态可悬停查看检测详情,编辑前请先核对账号名称。',
+ steps: ['新增或筛选密钥', '查看白名单状态', '编辑或删除'],
+ },
+ admin_shop_manage: {
+ text: '店铺信息按分组管理。长商城名会自动缩略,悬停即可查看完整内容。',
+ steps: ['选择分组', '维护店铺信息', '筛选核对结果'],
+ },
+ admin_shop_data_crawl_tasks: {
+ text: '店铺数据任务按状态和时间筛选。批量操作前请核对已选任务。',
+ steps: ['筛选任务', '检查状态', '执行批量操作'],
+ },
+ // admin_shop_data_duplicate_check(店铺撞款监控)为例外页:不展示全局提示条,不参与对齐。
+ admin_image_video_tasks: {
+ text: '可先使用筛选缩小任务范围,再查看任务状态、结果和权限范围。',
+ steps: ['设置筛选', '查看任务结果', '按需处理任务'],
+ },
+ admin_history: {
+ text: '生成记录可按用户和时间范围回溯,用于核对结果文件和执行时间。',
+ steps: ['设置时间范围', '筛选记录', '查看结果预览'],
+ },
+ admin_version: {
+ text: '上传版本后请核对版本号和下载链接,再通知用户更新。',
+ steps: ['上传压缩包', '检查版本记录', '维护历史版本'],
+ },
+ digital_human_version: {
+ text: '数字人版本需先上传草稿,再发布并标记最新版本。',
+ steps: ['上传草稿', '确认更新日志', '发布或设为最新'],
+ },
+}
+
+/** 全部有独立/兜底提示的后台业务菜单 key。 */
+export const OPERATION_GUIDE_MENU_KEYS = Object.keys(OPERATION_GUIDES)
+
+/** 返回指定菜单 key 的提示数据;menuKey 不在已知集合时返回 null(登录/404 等页面不展示)。 */
+export function operationGuideFor(menuKey: unknown): OperationGuideData | null {
+ if (typeof menuKey !== 'string' || !menuKey) return null
+ return OPERATION_GUIDES[menuKey] ?? null
+}
diff --git a/admin-frontend-vue/src/pages/asin/AsinInvalidPage.vue b/admin-frontend-vue/src/pages/asin/AsinInvalidPage.vue
index dc5762fa..a1aff63e 100644
--- a/admin-frontend-vue/src/pages/asin/AsinInvalidPage.vue
+++ b/admin-frontend-vue/src/pages/asin/AsinInvalidPage.vue
@@ -202,8 +202,8 @@ onMounted(() => {
- 编辑
- 删除
+ 编辑
+ 删除
diff --git a/admin-frontend-vue/src/pages/asin/DedupeRegistryPage.vue b/admin-frontend-vue/src/pages/asin/DedupeRegistryPage.vue
index 1db785f5..c0efe07d 100644
--- a/admin-frontend-vue/src/pages/asin/DedupeRegistryPage.vue
+++ b/admin-frontend-vue/src/pages/asin/DedupeRegistryPage.vue
@@ -371,8 +371,8 @@ onMounted(() => {
- 编辑
- 删除
+ 编辑
+ 删除
diff --git a/admin-frontend-vue/src/pages/asin/ProductCategoryPage.vue b/admin-frontend-vue/src/pages/asin/ProductCategoryPage.vue
index 26bf8f8f..93e98884 100644
--- a/admin-frontend-vue/src/pages/asin/ProductCategoryPage.vue
+++ b/admin-frontend-vue/src/pages/asin/ProductCategoryPage.vue
@@ -371,9 +371,8 @@ onMounted(loadTree)
- 编辑
+ 编辑
- 编辑
- 删除
+ 编辑
+ 删除
@@ -462,8 +461,8 @@ onMounted(loadTree)
height: 20px;
border: 0;
border-radius: 5px;
- background: #eef0fe;
- color: #6366f1;
+ background: var(--admin-primary-soft, #e7f0f8);
+ color: var(--admin-primary-strong, #2f5d8b);
display: inline-flex;
align-items: center;
justify-content: center;
@@ -475,7 +474,7 @@ onMounted(loadTree)
flex: none;
transition: background 0.12s ease, color 0.12s ease;
}
-.tree-node-mark:not(.is-leaf):hover { background: #6366f1; color: #fff; }
+.tree-node-mark:not(.is-leaf):hover { background: var(--admin-primary-strong, #2f5d8b); color: #fff; }
.tree-node-mark.is-leaf { background: transparent; cursor: default; }
.node-name { font-weight: 600; color: var(--el-text-color-primary); white-space: nowrap; }
.node-desc { color: var(--el-text-color-secondary); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 320px; display: inline-block; vertical-align: middle; }
diff --git a/admin-frontend-vue/src/pages/asin/QueryAsinPage.vue b/admin-frontend-vue/src/pages/asin/QueryAsinPage.vue
index 36f09bf0..ac278dfd 100644
--- a/admin-frontend-vue/src/pages/asin/QueryAsinPage.vue
+++ b/admin-frontend-vue/src/pages/asin/QueryAsinPage.vue
@@ -381,7 +381,7 @@ onMounted(() => {
- 配置
+ 配置
diff --git a/admin-frontend-vue/src/pages/asin/SkipPricePage.vue b/admin-frontend-vue/src/pages/asin/SkipPricePage.vue
index ea632818..962476bd 100644
--- a/admin-frontend-vue/src/pages/asin/SkipPricePage.vue
+++ b/admin-frontend-vue/src/pages/asin/SkipPricePage.vue
@@ -433,7 +433,7 @@ onMounted(() => {
- 配置
+ 配置
diff --git a/admin-frontend-vue/tests/align-asin-row-buttons.test.ts b/admin-frontend-vue/tests/align-asin-row-buttons.test.ts
new file mode 100644
index 00000000..b9ad2ded
--- /dev/null
+++ b/admin-frontend-vue/tests/align-asin-row-buttons.test.ts
@@ -0,0 +1,60 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readSource } from './helpers.ts'
+
+/** 对齐旧版 .btn-sm:asin 域 5 页表格行内操作(编辑/删除/配置)统一为实心小按钮(type primary/danger + size small),不再用 link/text;类目树折叠钮由靛蓝改为蓝白 token。 */
+
+// 实心小按钮:同一 el-button 内同时含 size="small" 与 @click="",且不含 link/text 修饰。
+const SOLID_ACTION = (action: string) => new RegExp(`]*size="small"[^>]*@click="${action}`)
+const NO_MODIFIER = (action: string, mod: 'link' | 'text') =>
+ new RegExp(`]*${mod}[^>]*@click="${action}`)
+
+function assertRowButtonSolid(source: string, action: string) {
+ assert.match(source, SOLID_ACTION(action), `${action} 为 size=small 实心按钮`)
+ assert.doesNotMatch(source, NO_MODIFIER(action, 'link'), `${action} 不再 link`)
+ assert.doesNotMatch(source, NO_MODIFIER(action, 'text'), `${action} 不再 text`)
+}
+
+test('align_dedupe_registry_row_buttons_solid', () => {
+ const s = readSource('src/pages/asin/DedupeRegistryPage.vue')
+ assertRowButtonSolid(s, 'openEdit')
+ assertRowButtonSolid(s, 'removeRow')
+})
+
+test('align_invalid_asin_row_buttons_solid', () => {
+ const s = readSource('src/pages/asin/AsinInvalidPage.vue')
+ assertRowButtonSolid(s, 'openEdit')
+ assertRowButtonSolid(s, 'remove')
+})
+
+test('align_query_asin_config_solid', () => {
+ assertRowButtonSolid(readSource('src/pages/asin/QueryAsinPage.vue'), 'openConfig')
+})
+
+test('align_skip_price_config_solid', () => {
+ assertRowButtonSolid(readSource('src/pages/asin/SkipPricePage.vue'), 'openConfig')
+})
+
+test('align_product_category_tree_row_buttons_solid', () => {
+ const s = readSource('src/pages/asin/ProductCategoryPage.vue')
+ assertRowButtonSolid(s, 'openEdit')
+ assertRowButtonSolid(s, 'removeCategory')
+})
+
+test('align_product_category_no_indigo_left', () => {
+ const s = readSource('src/pages/asin/ProductCategoryPage.vue')
+ assert.doesNotMatch(s, /#6366f1|#eef0fe|6366f1|eef0fe/, '靛蓝色点已清')
+ assert.match(s, /--admin-primary-soft/, '折叠钮改用蓝白主色 soft token')
+})
+
+test('align_product_category_search_and_tree_edits_both_solid', () => {
+ const s = readSource('src/pages/asin/ProductCategoryPage.vue')
+ const opens = s.match(/]*size="small"[^>]*@click="openEdit/g) || []
+ assert.ok(opens.length >= 2, `树/搜索两处编辑均实心(${opens.length})`)
+})
+
+test('align_product_category_load_more_kept_link', () => {
+ // 类目"加载更多"是非行内操作的树加载行,保留 link 不误伤。
+ const s = readSource('src/pages/asin/ProductCategoryPage.vue')
+ assert.match(s, /link size="small" type="primary"[^>]*loadMoreChildren/, '加载更多保留 link')
+})
diff --git a/admin-frontend-vue/tests/align-operation-guide.test.ts b/admin-frontend-vue/tests/align-operation-guide.test.ts
new file mode 100644
index 00000000..2e90632e
--- /dev/null
+++ b/admin-frontend-vue/tests/align-operation-guide.test.ts
@@ -0,0 +1,73 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readSource } from './helpers.ts'
+import { adminPages } from '../src/router/routes.ts'
+import {
+ OPERATION_GUIDES,
+ OPERATION_GUIDE_FALLBACK,
+ OPERATION_GUIDE_STORAGE_KEY,
+ operationGuideFor,
+} from '../src/layout/operation-guides.ts'
+
+/** 对齐旧版 admin-interactions.js guides 表:每业务菜单页顶部展示可折叠「当前页面操作提示」,文案沿用旧版原文;店铺撞款监控(例外)不展示。 */
+
+const EXCEPTED_KEY = 'admin_shop_data_duplicate_check'
+
+test('align_guide_users_matches_reference_text', () => {
+ const g = operationGuideFor('admin_users')
+ assert.ok(g)
+ assert.equal(g.text, '先用筛选定位账号,再编辑角色和菜单权限;删除账号会要求二次确认。')
+ assert.deepEqual(g.steps, ['筛选账号', '编辑权限', '确认保存'])
+})
+
+test('align_guide_history_and_version_reference_text', () => {
+ assert.match(operationGuideFor('admin_history')!.text, /生成记录可按用户和时间范围回溯/)
+ assert.deepEqual(operationGuideFor('admin_version')!.steps, ['上传压缩包', '检查版本记录', '维护历史版本'])
+})
+
+test('align_guide_group_manage_falls_back', () => {
+ // 旧版 group-manage 无独立 guide,用通用 fallback。
+ assert.deepEqual(operationGuideFor('admin_group_manage'), OPERATION_GUIDE_FALLBACK)
+})
+
+test('align_guide_duplicate_check_not_present', () => {
+ // 例外页:不展示提示条(不参与本轮对齐)。
+ assert.equal(operationGuideFor(EXCEPTED_KEY), null)
+ assert.equal(EXCEPTED_KEY in OPERATION_GUIDES, false)
+})
+
+test('align_guide_unknown_key_null', () => {
+ assert.equal(operationGuideFor(undefined), null)
+ assert.equal(operationGuideFor(null), null)
+ assert.equal(operationGuideFor('not-a-menu'), null)
+ assert.equal(operationGuideFor(''), null)
+})
+
+test('align_guide_every_router_page_has_guide_except_duplicate_check', () => {
+ // 路由注册表里的业务页除例外页外都应有独立/兜底提示。
+ const without = adminPages.filter((p) => p.menuKey !== EXCEPTED_KEY)
+ assert.ok(without.length >= 15, '业务页至少 15')
+ for (const page of without) {
+ assert.ok(operationGuideFor(page.menuKey), `缺少 guide: ${page.menuKey}`)
+ }
+})
+
+test('align_guide_storage_key_matches_legacy', () => {
+ assert.equal(OPERATION_GUIDE_STORAGE_KEY, 'shufuAdminGuideCollapsed')
+})
+
+test('align_guide_component_in_layout_headline', () => {
+ const layout = readSource('src/layout/AdminLayout.vue')
+ assert.match(layout, /OperationGuide/, '壳层引用 OperationGuide 组件')
+ const comp = readSource('src/layout/OperationGuide.vue')
+ assert.match(comp, /当前页面操作提示/, '标题对齐旧版 adminOperationGuide 语义')
+ assert.match(comp, /收起提示/, '折叠按钮文案')
+ assert.match(comp, /展开提示/, '展开按钮文案')
+ assert.match(comp, /operationGuideFor\(route\.meta\.menuKey\)/, '按菜单 key 查文案')
+ assert.match(comp, /OPERATION_GUIDE_STORAGE_KEY/, '折叠态读写同一 localStorage key')
+})
+
+test('align_guide_fallback_shape', () => {
+ assert.equal(OPERATION_GUIDE_FALLBACK.steps.length, 3)
+ assert.ok(OPERATION_GUIDE_FALLBACK.text.length > 10)
+})