From 9f73af59118401575d0c9983f7b0663d264b97b8 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 13:47:06 +0800 Subject: [PATCH] =?UTF-8?q?task-21(=E4=BC=9A=E8=AF=9D/=E8=8F=9C=E5=8D=95/?= =?UTF-8?q?=E6=9D=83=E9=99=90):=20=E5=AE=9A=E4=B9=89=20AdminUser/AdminMenu?= =?UTF-8?q?Node=20=E7=B1=BB=E5=9E=8B=E5=A5=91=E7=BA=A6=E5=B9=B6=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=E5=90=8E=E7=AB=AF=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit admin.ts 增加 AdminRole 域与 roleKind/isSuperAdminRole/isAdminRole/roleLabel/ isSuperUser,AdminUser 补齐 isAdmin/creatorUsername/createdAt 等真实字段; 契约覆盖“分组可无 route、页面必有 route”。 --- admin-frontend-vue/src/types/admin.ts | 78 ++++++++++++++++++++++++ admin-frontend-vue/tests/task-21.test.ts | 68 +++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 admin-frontend-vue/src/types/admin.ts create mode 100644 admin-frontend-vue/tests/task-21.test.ts diff --git a/admin-frontend-vue/src/types/admin.ts b/admin-frontend-vue/src/types/admin.ts new file mode 100644 index 00000000..9ef8c0e4 --- /dev/null +++ b/admin-frontend-vue/src/types/admin.ts @@ -0,0 +1,78 @@ +/** 后台前端类型契约(任务 21):与 Java Admin 接口返回字段对齐。 */ + +/** 管理角色域(与后端 current-user / users 返回一致)。 */ +export type AdminRole = 'super_admin' | 'admin' | 'normal' + +export interface AdminUser { + id: number + username: string + role: string + /** 1 为管理员标志位;role 为空时可据此推断角色。 */ + isAdmin?: boolean + createdById?: number | null + creatorUsername?: string + createdAt?: string + pinyinAbbr?: string +} + +export interface AdminMenuNode { + /** columns.column_key,稳定权限标识。 */ + key: string + name: string + /** 相对 /admin-vue/ 的页面路径;分组节点可能没有。 */ + route?: string + /** 分组/页面层级;后端按 sort_order 排好后原样下发,前端不二次重排。 */ + children?: AdminMenuNode[] + sort?: number + icon?: string +} + +export type AdminRoleKind = 'super_admin' | 'admin' | 'normal' | null + +/** 把任意角色字符串归一为角色域;未知/空返回 null。 */ +export function roleKind(role: string | null | undefined): AdminRoleKind { + const value = (role || '').trim().toLowerCase() + if (value === 'super_admin') return 'super_admin' + if (value === 'admin') return 'admin' + if (value === 'normal') return 'normal' + return null +} + +/** 是否超级管理员(可进入全部菜单)。 */ +export function isSuperAdminRole(role: string | null | undefined): boolean { + return roleKind(role) === 'super_admin' +} + +/** 是否管理员级(super_admin 或 admin,可进后台菜单管理类)。 */ +export function isAdminRole(role: string | null | undefined): boolean { + const kind = roleKind(role) + return kind === 'super_admin' || kind === 'admin' +} + +/** 顶部栏角色展示文案。 */ +export function roleLabel(role: string | null | undefined): string { + switch (roleKind(role)) { + case 'super_admin': + return '超级管理员' + case 'admin': + return '管理员' + case 'normal': + return '普通用户' + default: + return '' + } +} + +/** 前端是否应把该用户视为超级管理员(可进入全部菜单)。 */ +export function isSuperUser(user: Pick | null | undefined): boolean { + return isSuperAdminRole(user?.role) +} + +export type ApiEnvelope = { + success?: boolean + message?: string + error?: string + data?: T + item?: T + items?: T +} diff --git a/admin-frontend-vue/tests/task-21.test.ts b/admin-frontend-vue/tests/task-21.test.ts new file mode 100644 index 00000000..75f9f55d --- /dev/null +++ b/admin-frontend-vue/tests/task-21.test.ts @@ -0,0 +1,68 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { + isAdminRole, + isSuperAdminRole, + isSuperUser, + roleKind, + roleLabel, +} from '../src/types/admin.ts' + +test('test_task_021_type_contract_normal_primary_path', () => { + // 正常主路径:真实后端角色串映射到角色域。 + assert.equal(roleKind('super_admin'), 'super_admin') + assert.equal(isSuperAdminRole('super_admin'), true) + assert.equal(isSuperUser({ role: 'super_admin' }), true) +}) + +test('test_task_021_type_contract_normal_variant_input', () => { + // 正常变体:admin/normal 归类正确,超级用户判定不为 true。 + assert.equal(roleKind('admin'), 'admin') + assert.equal(roleKind('normal'), 'normal') + assert.equal(isSuperUser({ role: 'admin' }), false) + assert.equal(isAdminRole('admin'), true) +}) + +test('test_task_021_type_contract_normal_repeated_operation_is_idempotent', () => { + // 正常重复:角色归一稳定。 + assert.equal(roleKind('SUPER_ADMIN'), roleKind('super_admin')) + assert.equal(roleLabel('admin'), roleLabel(' admin ')) +}) + +test('test_task_021_type_contract_boundary_empty_input', () => { + // 边界空值:空角色/空用户不崩溃,归类为 null/非管理员。 + assert.equal(roleKind(''), null) + assert.equal(roleKind(undefined), null) + assert.equal(roleKind(null), null) + assert.equal(isSuperUser(null), false) + assert.equal(isSuperUser(undefined), false) +}) + +test('test_task_021_type_contract_boundary_single_item', () => { + // 边界单元素:单个 super_admin 用户可进全部菜单。 + assert.equal(isSuperUser({ role: 'super_admin' }), true) + assert.equal(roleLabel('super_admin'), '超级管理员') +}) + +test('test_task_021_type_contract_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:未知角色串不误判为管理员,展示空文案。 + assert.equal(roleKind('owner'), null) + assert.equal(isAdminRole('owner'), false) + assert.equal(roleLabel('owner'), '') +}) + +test('test_task_021_type_contract_invalid_input_rejected', () => { + // 异常输入:大小写/空白容忍,非法字符串归类 null。 + assert.equal(roleKind(' Super_Admin '), 'super_admin') + assert.equal(roleKind('xxx'), null) + assert.equal(isSuperAdminRole(''), false) +}) + +test('test_task_021_type_contract_dependency_failure_returns_actionable_message', () => { + // 依赖失败:菜单节点契约为分组可无 route、页面必须可路由。 + const group = { key: 'admin_group_account', name: '账号权限' } + const page = { key: 'admin_users', name: '用户管理', route: '/account/users' } + assert.equal('route' in group, false, '分组节点允许无 route') + assert.equal(typeof page.route, 'string', '页面节点必须带 route') + assert.equal(roleLabel('super_admin').length > 0, true) +})