diff --git a/admin-frontend-vue/src/pages/error/NotFoundPage.vue b/admin-frontend-vue/src/pages/error/NotFoundPage.vue new file mode 100644 index 00000000..4ff85fd8 --- /dev/null +++ b/admin-frontend-vue/src/pages/error/NotFoundPage.vue @@ -0,0 +1,15 @@ + + + diff --git a/admin-frontend-vue/src/router/index.ts b/admin-frontend-vue/src/router/index.ts index aee595b2..717b110e 100644 --- a/admin-frontend-vue/src/router/index.ts +++ b/admin-frontend-vue/src/router/index.ts @@ -1,6 +1,7 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' import type { AdminMenuNode } from '@/types/admin' import AdminLayout from '@/layout/AdminLayout.vue' +import NotFoundPage from '@/pages/error/NotFoundPage.vue' import { useAdminSessionStore } from '@/stores/admin-session' import { APP_BASE_PATH } from '@/config/app' import { adminRouteRecords } from './routes' @@ -12,10 +13,13 @@ const router = createRouter({ { path: '/', component: AdminLayout, - children: adminRouteRecords.map((record) => ({ - ...record, - component: defineLazyPage(record.component as () => Promise<{ default: unknown }>), - })) as RouteRecordRaw[], + children: [ + ...adminRouteRecords.map((record) => ({ + ...record, + component: defineLazyPage(record.component as () => Promise<{ default: unknown }>), + })), + { path: ':pathMatch(.*)*', component: NotFoundPage, meta: { title: '页面不存在' } }, + ] as RouteRecordRaw[], }, ], }) diff --git a/admin-frontend-vue/src/styles/main.css b/admin-frontend-vue/src/styles/main.css index 8d737580..61090caa 100644 --- a/admin-frontend-vue/src/styles/main.css +++ b/admin-frontend-vue/src/styles/main.css @@ -50,6 +50,7 @@ button, input, textarea, select { font: inherit; } .page-loading, .page-error { padding: 48px 20px; text-align: center; } .page-loading { color: var(--admin-muted); } .page-error { color: #b91c1c; } +.not-found { padding: 56px 20px; } @media (max-width: 820px) { .admin-sidebar { flex-basis: 64px; } diff --git a/admin-frontend-vue/tests/task-12.test.ts b/admin-frontend-vue/tests/task-12.test.ts new file mode 100644 index 00000000..e72daa8e --- /dev/null +++ b/admin-frontend-vue/tests/task-12.test.ts @@ -0,0 +1,71 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { readSource, occurrences } from './helpers.ts' +import { adminPages } from '../src/router/routes.ts' + +const ROUTER = 'src/router/index.ts' +const NOT_FOUND = 'src/pages/error/NotFoundPage.vue' + +test('test_task_012_route_error_page_normal_primary_path', () => { + // 正常主路径:未知路径注册到路由级错误页兜底。 + const router = readSource(ROUTER) + assert.match(router, /path:\s*':pathMatch\(\.\*\)\*'/) + assert.match(router, /NotFoundPage/) + assert.match(router, /页面不存在/) +}) + +test('test_task_012_route_error_page_normal_variant_input', () => { + // 正常变体:错误页在 AdminLayout children 内,保留壳层 chrome。 + const router = readSource(ROUTER) + const catchAllIndex = router.indexOf(':pathMatch(.*)*') + const registryIndex = router.indexOf('adminRouteRecords.map') + assert.ok(catchAllIndex > registryIndex, 'catch-all 应在业务路由之后注册') + assert.match(router, /path: '\/',/) +}) + +test('test_task_012_route_error_page_normal_repeated_operation_is_idempotent', () => { + // 正常重复:错误页注册不重复追加。 + const first = readSource(ROUTER) + assert.equal(occurrences(first, ':pathMatch(.*)*'), 1) +}) + +test('test_task_012_route_error_page_boundary_empty_input', () => { + // 边界空值:错误页文件存在,且不进入业务路由注册表。 + assert.equal(existsSync(join(process.cwd(), NOT_FOUND)), true) + assert.equal(adminPages.length, 3, '错误页不应计入业务路由') +}) + +test('test_task_012_route_error_page_boundary_single_item', () => { + // 边界单元素:错误页是单一 Vue 页面组件。 + const source = readSource(NOT_FOUND) + assert.equal(occurrences(source, '