diff --git a/admin-frontend-vue/src/router/index.ts b/admin-frontend-vue/src/router/index.ts index 5e250f67..758ee485 100644 --- a/admin-frontend-vue/src/router/index.ts +++ b/admin-frontend-vue/src/router/index.ts @@ -3,6 +3,7 @@ import type { AdminMenuNode } from '@/types/admin' import AdminLayout from '@/layout/AdminLayout.vue' import { useAdminSessionStore } from '@/stores/admin-session' import { APP_BASE_PATH } from '@/config/app' +import { adminRouteRecords } from './routes' const router = createRouter({ history: createWebHistory(APP_BASE_PATH), @@ -10,11 +11,7 @@ const router = createRouter({ { path: '/', component: AdminLayout, - children: [ - { path: 'account/users', component: () => import('@/pages/account/UsersPage.vue'), meta: { menuKey: 'admin_users', title: '用户管理' } }, - { path: 'account/menus', component: () => import('@/pages/account/MenusPage.vue'), meta: { menuKey: 'admin_columns', title: '菜单管理' } }, - { path: 'account/groups', component: () => import('@/pages/account/GroupsPage.vue'), meta: { menuKey: 'admin_group_manage', title: '数据权限分组' } }, - ], + children: adminRouteRecords, }, ], }) diff --git a/admin-frontend-vue/src/router/routes.ts b/admin-frontend-vue/src/router/routes.ts new file mode 100644 index 00000000..bb2f0af3 --- /dev/null +++ b/admin-frontend-vue/src/router/routes.ts @@ -0,0 +1,49 @@ +import type { RouteRecordRaw } from 'vue-router' + +export interface AdminPageMeta { + title: string + menuKey?: string +} + +export interface AdminPageDef extends AdminPageMeta { + /** 相对 /admin-vue/ 基准的子路径,不带前导斜杠、不带 base 前缀。 */ + path: string + /** 异步组件加载器(页面按路由分包)。 */ + load: () => Promise<{ default: unknown }> +} + +/** 业务域路由注册表(任务 8):首批 account 域;后续按业务域在此登记页面。 */ +export const adminPages: AdminPageDef[] = [ + { path: 'account/users', menuKey: 'admin_users', title: '用户管理', load: () => import('@/pages/account/UsersPage.vue') }, + { path: 'account/menus', menuKey: 'admin_columns', title: '菜单管理', load: () => import('@/pages/account/MenusPage.vue') }, + { path: 'account/groups', menuKey: 'admin_group_manage', title: '数据权限分组', load: () => import('@/pages/account/GroupsPage.vue') }, +] + +export function routeOf(page: AdminPageDef): RouteRecordRaw { + if (!page.path || !page.load || !page.title) { + throw new Error(`路由注册表页面定义非法,需 path/load/title 齐全: ${JSON.stringify(page)}`) + } + const meta: Record = { title: page.title } + if (page.menuKey) meta.menuKey = page.menuKey + return { path: page.path, component: page.load, meta } +} + +export const adminRouteRecords: RouteRecordRaw[] = adminPages.map(routeOf) + +/** 校验注册表完整性:路径唯一、title/load 齐全、路径为相对子路径。返回问题清单。 */ +export function validateAdminPages(pages: AdminPageDef[]): string[] { + const issues: string[] = [] + const seen = new Set() + for (const page of pages) { + if (!page.path || page.path.startsWith('/')) { + issues.push(`页面路径必须是相对子路径(不带 / 与 base 前缀): ${page.path || '(空)'}`) + } else if (seen.has(page.path)) { + issues.push(`页面路径重复: ${page.path}`) + } + seen.add(page.path) + if (!page.title) issues.push(`页面缺少标题: ${page.path}`) + if (!page.load) issues.push(`页面缺少异步加载器: ${page.path}`) + if (page.path.includes('/admin-vue')) issues.push(`页面路径不得包含 base 前缀: ${page.path}`) + } + return issues +} diff --git a/admin-frontend-vue/tests/task-3.test.ts b/admin-frontend-vue/tests/task-3.test.ts index 0eeb6ad4..efb38da1 100644 --- a/admin-frontend-vue/tests/task-3.test.ts +++ b/admin-frontend-vue/tests/task-3.test.ts @@ -13,11 +13,11 @@ test('test_task_003_layout_contract_normal_primary_path', () => { }) test('test_task_003_layout_contract_normal_variant_input', () => { - // 正常变体:页面接线归路由所有;壳层不得静态 import 业务页面。 + // 正常变体:页面接线归路由注册表所有;壳层不得静态 import 业务页面。 const layout = readSource(LAYOUT) - const router = readSource(ROUTER) + const registry = readSource('src/router/routes.ts') assert.equal(occurrences(layout, '@/pages/'), 0, 'AdminLayout 不得静态 import 业务页面') - assert.ok(router.includes('component: () => import('), '路由使用异步组件加载页面') + assert.ok(registry.includes('() => import('), '注册表使用异步组件加载页面') }) test('test_task_003_layout_contract_normal_repeated_operation_is_idempotent', () => { diff --git a/admin-frontend-vue/tests/task-8.test.ts b/admin-frontend-vue/tests/task-8.test.ts new file mode 100644 index 00000000..9b063a58 --- /dev/null +++ b/admin-frontend-vue/tests/task-8.test.ts @@ -0,0 +1,79 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + adminPages, + adminRouteRecords, + routeOf, + validateAdminPages, +} from '../src/router/routes.ts' + +test('test_task_008_domain_route_registry_normal_primary_path', () => { + // 正常主路径:注册表首批 account 域页面登记为可消费路由记录。 + assert.equal(adminPages.length, 3) + assert.equal(adminRouteRecords.length, adminPages.length) + const first = adminRouteRecords[0] + assert.equal(first.path, 'account/users') + assert.equal((first.meta as { title?: string }).title, '用户管理') + assert.equal((first.meta as { menuKey?: string }).menuKey, 'admin_users') + assert.deepEqual(validateAdminPages(adminPages), []) +}) + +test('test_task_008_domain_route_registry_normal_variant_input', () => { + // 正常变体:无 menuKey 的页面元数据不携带菜单 key,仍带标题。 + const record = routeOf({ path: 'tasks/plain', title: '无菜单页', load: async () => ({ default: {} }) }) + const meta = record.meta as { title?: string; menuKey?: string } + assert.equal(meta.title, '无菜单页') + assert.equal('menuKey' in meta, false) +}) + +test('test_task_008_domain_route_registry_normal_repeated_operation_is_idempotent', () => { + // 正常重复:从同一注册表重复生成记录结果稳定。 + const once = JSON.stringify(adminPages.map(routeOf)) + const twice = JSON.stringify(adminPages.map(routeOf)) + assert.equal(once, twice) + assert.deepEqual(validateAdminPages(adminPages), validateAdminPages(adminPages)) +}) + +test('test_task_008_domain_route_registry_boundary_empty_input', () => { + // 边界空值:空注册表可校验、可映射为空路由列表。 + assert.deepEqual(validateAdminPages([]), []) + assert.equal([].map(routeOf).length, 0) +}) + +test('test_task_008_domain_route_registry_boundary_single_item', () => { + // 边界单元素:单页面登记生成唯一路由记录。 + const records = [routeOf({ path: 'shop/list', title: '店铺列表', load: async () => ({ default: {} }) })] + assert.equal(records.length, 1) + assert.equal(records[0].path, 'shop/list') +}) + +test('test_task_008_domain_route_registry_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:路径重复、前导斜杠、缺 title、混入 base 前缀都被校验拒绝。 + const base = { title: 'T', load: async () => ({ default: {} }) } + const issues = validateAdminPages([ + { ...base, path: 'a/b' }, + { ...base, path: 'a/b' }, + { ...base, path: '/lead' }, + { path: '', title: '', load: async () => ({ default: {} }) }, + { path: '/admin-vue/x', title: 'X', load: async () => ({ default: {} }) }, + ]) + assert.ok(issues.some((i) => i.includes('路径重复'))) + assert.ok(issues.some((i) => i.includes('相对子路径'))) + assert.ok(issues.some((i) => i.includes('缺少标题'))) + assert.ok(issues.some((i) => i.includes('base 前缀'))) +}) + +test('test_task_008_domain_route_registry_invalid_input_rejected', () => { + // 异常输入:非法页面定义在生成路由时被拒绝并给出可操作消息。 + assert.throws(() => routeOf({ path: '', title: 'T', load: async () => ({ default: {} }) }), /路由注册表页面定义非法/) + assert.throws(() => routeOf({ path: 'x', title: '', load: async () => ({ default: {} }) }), /路由注册表页面定义非法/) +}) + +test('test_task_008_domain_route_registry_dependency_failure_returns_actionable_message', () => { + // 依赖失败:router 消费注册表,不再内联页面懒加载与路径字面量。 + const router = readSource('src/router/index.ts') + assert.match(router, /adminRouteRecords/) + assert.equal(router.includes('account/users'), false, '路径字面量应只存在于 routes.ts 注册表') + assert.equal(router.includes('() => import(\'@/pages'), false, '页面懒加载应只存在于注册表') +})