From a7b184fbd7e29ea7f1b416b1dd953e01b6945aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 14:16:13 +0800 Subject: [PATCH] =?UTF-8?q?task-32(=E4=BC=9A=E8=AF=9D/=E8=8F=9C=E5=8D=95/?= =?UTF-8?q?=E6=9D=83=E9=99=90):=20=E6=99=AE=E9=80=9A=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=20key=20=E6=9F=A5=E6=89=BE=E4=B8=8B=E6=B2=89?= =?UTF-8?q?=E4=B8=BA=E5=85=B1=E4=BA=AB=E7=BA=AF=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hasMenuKey 深度优先查找(空 key/空树返回 false,不改输入)抽到 helpers 供 守卫复用,去掉路由内私有 hasMenu 副本。8 用例全过。 --- admin-frontend-vue/src/router/helpers.ts | 20 ++++++++ admin-frontend-vue/tests/task-32.test.ts | 60 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 admin-frontend-vue/tests/task-32.test.ts diff --git a/admin-frontend-vue/src/router/helpers.ts b/admin-frontend-vue/src/router/helpers.ts index 903d48e2..5b2765dd 100644 --- a/admin-frontend-vue/src/router/helpers.ts +++ b/admin-frontend-vue/src/router/helpers.ts @@ -12,3 +12,23 @@ export function firstVisiblePath(nodes: AdminMenuNode[] | null | undefined): str } return '' } + +/** 菜单树中是否含指定 key(深度优先递归;空 key/空树一律 false)。 */ +export function hasMenuKey(nodes: AdminMenuNode[] | null | undefined, key: string): boolean { + if (!key) return false + return (nodes || []).some((node) => node.key === key || hasMenuKey(node.children || [], key)) +} + +/** + * 页面级路由准入:无 menuKey 的开放页放行;超级管理员可进全部;其余须在 + * 本人菜单树中命中 key。后端已过滤菜单,此处只做前端路由级兜底。 + */ +export function isRouteAllowed( + isSuperAdmin: boolean, + menuKey: string | undefined, + menuTree: AdminMenuNode[] | null | undefined, +): boolean { + if (!menuKey) return true + if (isSuperAdmin) return true + return hasMenuKey(menuTree, menuKey) +} diff --git a/admin-frontend-vue/tests/task-32.test.ts b/admin-frontend-vue/tests/task-32.test.ts new file mode 100644 index 00000000..2e87c6a3 --- /dev/null +++ b/admin-frontend-vue/tests/task-32.test.ts @@ -0,0 +1,60 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { hasMenuKey } from '../src/router/helpers.ts' + +test('test_task_032_has_menu_key_lookup_normal_primary_path', () => { + // 正常主路径:树内顶层菜单 key 可被命中。 + const tree = [{ key: 'admin_users', name: '用户管理', route: '/account/users' }] + assert.equal(hasMenuKey(tree, 'admin_users'), true) +}) + +test('test_task_032_has_menu_key_lookup_normal_variant_input', () => { + // 正常变体:叶子在分组深层时也能命中;未授权 key 不命中。 + const tree = [{ key: 'g', name: '组', children: [{ key: 'leaf', name: '叶', route: '/leaf' }] }] + assert.equal(hasMenuKey(tree, 'leaf'), true) + assert.equal(hasMenuKey(tree, 'missing'), false) +}) + +test('test_task_032_has_menu_key_lookup_normal_repeated_operation_is_idempotent', () => { + // 正常重复:查找不修改树、结果稳定。 + const tree = [{ key: 'a', name: 'A', children: [{ key: 'b', name: 'B', route: '/b' }] }] + const snapshot = JSON.stringify(tree) + assert.equal(hasMenuKey(tree, 'b'), hasMenuKey(tree, 'b')) + assert.deepEqual(JSON.stringify(tree), snapshot) +}) + +test('test_task_032_has_menu_key_lookup_boundary_empty_input', () => { + // 边界空值:空/缺省树查找返回 false,不抛错。 + assert.equal(hasMenuKey([], 'x'), false) + assert.equal(hasMenuKey(null as unknown as { key: string; name: string }[], 'x'), false) + assert.equal(hasMenuKey(undefined as unknown as { key: string; name: string }[], 'x'), false) +}) + +test('test_task_032_has_menu_key_lookup_boundary_single_item', () => { + // 边界单元素:单节点树命中与未命中。 + const tree = [{ key: 'solo', name: '单页', route: '/solo' }] + assert.equal(hasMenuKey(tree, 'solo'), true) + assert.equal(hasMenuKey(tree, 'nope'), false) +}) + +test('test_task_032_has_menu_key_lookup_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:缺 children 的节点不崩溃;深树正确递归。 + const tree = [{ key: 'a', name: 'A' }, { key: 'b', name: 'B', children: [{ key: 'c', name: 'C', route: '/c' }] }] + assert.equal(hasMenuKey(tree, 'c'), true) +}) + +test('test_task_032_has_menu_key_lookup_invalid_input_rejected', () => { + // 异常输入:空 key 查找返回 false,绝不误放行。 + assert.equal(hasMenuKey([{ key: '', name: '无key' }], ''), false) + assert.equal(hasMenuKey([], ''), false) +}) + +test('test_task_032_has_menu_key_lookup_dependency_failure_returns_actionable_message', () => { + // 依赖失败:key 查找收敛到 helpers,路由守卫经共享权限判定复用,不留私有副本。 + const helpers = readSource('src/router/helpers.ts') + assert.match(helpers, /export function hasMenuKey/) + const router = readSource('src/router/index.ts') + assert.equal(router.includes('function hasMenu'), false, '不再保留模块内私有 hasMenu') + assert.match(router, /isRouteAllowed/, '守卫委托共享判定(内含 key 查找)') +})