diff --git a/admin-frontend-vue/src/pages/account/user-role-map.ts b/admin-frontend-vue/src/pages/account/user-role-map.ts new file mode 100644 index 00000000..2d0ec368 --- /dev/null +++ b/admin-frontend-vue/src/pages/account/user-role-map.ts @@ -0,0 +1,56 @@ +/** 用户角色字段映射(任务 48):行 role/is_admin → 角色域 → 可编辑值/锁定原因,镜像 Java 约束。 */ +import { roleKind } from '../../types/admin.ts' + +/** 后台角色域(与 AdminUserItemVo.role / is_admin 对齐)。 */ +export type RoleDomain = 'super_admin' | 'admin' | 'normal' + +/** 用户角色下拉可选值;super_admin 只能由系统持有,不入下拉。 */ +export type EditableRole = 'admin' | 'normal' + +export interface UserRoleOption { + value: EditableRole + label: string +} + +/** 编辑/创建表单角色下拉候选(镜像后端 admin|normal 白名单)。 */ +export const USER_ROLE_OPTIONS: readonly UserRoleOption[] = [ + { value: 'admin', label: '管理员' }, + { value: 'normal', label: '普通用户' }, +] + +const ROLE_DOMAIN_LABELS: Record = { + super_admin: '超级管理员', + admin: '管理员', + normal: '普通用户', +} + +/** 行字段 → 角色域:优先 role;为空时按 is_admin 布尔推断(1=管理员);未知归 normal。 */ +export function roleDomainOf(row: { role?: string | null; isAdmin?: boolean }): RoleDomain { + const kind = roleKind(row?.role) + if (kind === 'super_admin' || kind === 'admin' || kind === 'normal') return kind + return row?.isAdmin ? 'admin' : 'normal' +} + +/** 行 → 可编辑角色值:超级管理员返回 null(不可编辑),其余返回 admin/normal。 */ +export function selectRoleOf(row: { role?: string | null; isAdmin?: boolean }): EditableRole | null { + const domain = roleDomainOf(row) + return domain === 'super_admin' ? null : domain +} + +/** 角色域展示文案。 */ +export function roleDomainLabel(domain: RoleDomain): string { + return ROLE_DOMAIN_LABELS[domain] || '' +} + +/** + * 角色下拉锁定原因:目标为超管、或操作者非超管(镜像后端:admin 编辑时强制不调角色、 + * super_admin 不可改)。无锁定时返回 undefined。 + */ +export function userRoleLockMessage( + operatorRole: string | null | undefined, + target: { role?: string | null; isAdmin?: boolean }, +): string | undefined { + if (roleDomainOf(target) === 'super_admin') return '不能修改超级管理员' + if (roleKind(operatorRole) !== 'super_admin') return '仅超级管理员可调整角色' + return undefined +} diff --git a/admin-frontend-vue/tests/task-48.test.ts b/admin-frontend-vue/tests/task-48.test.ts new file mode 100644 index 00000000..b2187082 --- /dev/null +++ b/admin-frontend-vue/tests/task-48.test.ts @@ -0,0 +1,67 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + roleDomainOf, + selectRoleOf, + userRoleLockMessage, + roleDomainLabel, + USER_ROLE_OPTIONS, +} from '../src/pages/account/user-role-map.ts' + +test('test_task_048_user_role_map_normal_primary_path', () => { + // 正常主路径:role 字段直接映射为角色域。 + assert.equal(roleDomainOf({ role: 'admin' }), 'admin') + assert.equal(roleDomainOf({ role: 'normal' }), 'normal') + assert.equal(roleDomainOf({ role: 'super_admin' }), 'super_admin') +}) + +test('test_task_048_user_role_map_normal_variant_input', () => { + // 正常变体:role 为空时按 is_admin 布尔推断(1=管理员);未知角色归 normal。 + assert.equal(roleDomainOf({ role: '', isAdmin: true }), 'admin') + assert.equal(roleDomainOf({ role: '', isAdmin: false }), 'normal') + assert.equal(roleDomainOf({ role: 'biz', isAdmin: false }), 'normal') +}) + +test('test_task_048_user_role_map_normal_repeated_operation_is_idempotent', () => { + // 正常重复:映射为纯函数、不改输入、结果稳定。 + const row = { role: 'admin', isAdmin: true } + assert.equal(roleDomainOf(row), 'admin') + assert.equal(roleDomainOf(row), 'admin') + assert.equal(row.role, 'admin') +}) + +test('test_task_048_user_role_map_boundary_empty_input', () => { + // 边界空值:缺省/空角色行回退 normal,不崩溃。 + assert.equal(roleDomainOf({}), 'normal') + assert.equal(selectRoleOf({}), 'normal') +}) + +test('test_task_048_user_role_map_boundary_single_item', () => { + // 边界单元素:admin 单行 → 编辑可选 admin。 + assert.equal(selectRoleOf({ role: 'admin' }), 'admin') +}) + +test('test_task_048_user_role_map_boundary_limit_or_missing_field', () => { + // 边界上限:超级管理员不可作为可编辑角色(null);候选不含 super_admin。 + assert.equal(selectRoleOf({ role: 'super_admin' }), null) + const values = USER_ROLE_OPTIONS.map((o) => o.value) + assert.deepEqual([...values].sort(), ['admin', 'normal']) + assert.equal(values.includes('super_admin'), false) +}) + +test('test_task_048_user_role_map_invalid_input_rejected', () => { + // 异常输入:目标为超管/操作者非超管给出可操作锁定原因。 + assert.match(userRoleLockMessage('super_admin', { role: 'super_admin' }) || '', /超级管理员/) + assert.match(userRoleLockMessage('admin', { role: 'normal' }) || '', /超管|管理员/) + assert.equal(userRoleLockMessage('super_admin', { role: 'normal' }), undefined) +}) + +test('test_task_048_user_role_map_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:角色映射为纯逻辑、无 axios/http;展示文案与域一一对应。 + const mod = readSource('src/pages/account/user-role-map.ts') + assert.equal(/axios|http\./.test(mod), false, '角色映射保持纯逻辑') + assert.equal(roleDomainLabel('admin'), '管理员') + assert.equal(roleDomainLabel('normal'), '普通用户') + assert.equal(roleDomainLabel('super_admin'), '超级管理员') +})