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, ''), 1)
+ assert.match(source, /el-result/)
+})
+
+test('test_task_012_route_error_page_boundary_limit_or_missing_field', () => {
+ // 边界上限:catch-all 不参与菜单权限门禁(无 menuKey)。
+ const router = readSource(ROUTER)
+ const tail = router.slice(router.indexOf('component: NotFoundPage'))
+ assert.equal(tail.slice(0, 80).includes('menuKey'), false, '错误页不应带菜单 key')
+})
+
+test('test_task_012_route_error_page_invalid_input_rejected', () => {
+ // 异常输入:深层未知路径也能被通配兜底,且提供返回首页动作。
+ const router = readSource(ROUTER)
+ assert.match(router, /:pathMatch\(\.\*\)\*/, '通配需覆盖多段未知路径')
+ const page = readSource(NOT_FOUND)
+ assert.match(page, /router\.replace\('\/'\)/)
+})
+
+test('test_task_012_route_error_page_dependency_failure_returns_actionable_message', () => {
+ // 依赖失败:标题/样式/返回动作齐全,加载失败有明确入口。
+ const page = readSource(NOT_FOUND)
+ assert.match(page, /返回首页/)
+ assert.match(page, /404/)
+ const css = readSource('src/styles/main.css')
+ assert.match(css, /\.not-found/)
+ const router = readSource(ROUTER)
+ assert.match(router, /meta: \{ title: '页面不存在' \}/)
+})