From 63339b401c08d907646ecdcbed8431ad57f91ba6 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 22:54:49 +0800 Subject: [PATCH] =?UTF-8?q?align(=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E8=B6=85=E7=AE=A1=E6=96=B0=E5=BB=BA=E6=99=AE=E9=80=9A=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E8=A1=A5=E6=89=80=E5=B1=9E=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?(createdBy)=E4=B8=8B=E6=8B=89=EF=BC=8Cpayload=20=E4=BB=85=20nor?= =?UTF-8?q?mal+=E5=B7=B2=E9=80=89=E6=97=B6=E5=B8=A6=20createdById=EF=BC=88?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20admin.js=20created=5Fby=5Fid=20=E8=AF=AD?= =?UTF-8?q?=E4=B9=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/account/CreateUserDialog.vue | 18 ++++++- .../src/pages/account/UsersPage.vue | 2 +- .../src/pages/account/user-create-model.ts | 15 ++++-- .../tests/align-user-createdby.test.ts | 54 +++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 admin-frontend-vue/tests/align-user-createdby.test.ts diff --git a/admin-frontend-vue/src/pages/account/CreateUserDialog.vue b/admin-frontend-vue/src/pages/account/CreateUserDialog.vue index 9cffc75a..edf3e75e 100644 --- a/admin-frontend-vue/src/pages/account/CreateUserDialog.vue +++ b/admin-frontend-vue/src/pages/account/CreateUserDialog.vue @@ -11,7 +11,12 @@ import { showAdminFeedback } from '@/components/admin-feedback-ui' import { actionableErrorText } from '@/components/admin-feedback' import UserMenuAuthTree from './UserMenuAuthTree.vue' -const props = defineProps<{ modelValue: boolean; operatorSuper?: boolean }>() +const props = defineProps<{ + modelValue: boolean + operatorSuper?: boolean + /** 所属管理员候选(列表响应 admins;仅超管创建普通账号时展示)。 */ + adminOptions?: Array<{ id: number; username: string }> +}>() const emit = defineEmits<{ (e: 'update:modelValue', v: boolean): void; (e: 'created'): void }>() const form = reactive(createEmptyCreateUserForm()) @@ -32,6 +37,7 @@ watch( form.password = '' form.role = 'normal' form.columnIds = [] + form.createdBy = null errors.username = undefined errors.password = undefined }, @@ -90,6 +96,16 @@ async function create(): Promise { + + + + +
diff --git a/admin-frontend-vue/src/pages/account/UsersPage.vue b/admin-frontend-vue/src/pages/account/UsersPage.vue index 7c694ba2..bbf97a78 100644 --- a/admin-frontend-vue/src/pages/account/UsersPage.vue +++ b/admin-frontend-vue/src/pages/account/UsersPage.vue @@ -194,7 +194,7 @@ onMounted(loadUsers) />
- + diff --git a/admin-frontend-vue/src/pages/account/user-create-model.ts b/admin-frontend-vue/src/pages/account/user-create-model.ts index d1e7b662..78c315ec 100644 --- a/admin-frontend-vue/src/pages/account/user-create-model.ts +++ b/admin-frontend-vue/src/pages/account/user-create-model.ts @@ -12,6 +12,8 @@ export interface CreateUserForm { role: CreatableUserRole /** 授权菜单 id(后端 replaceDirectPermissions 用)。 */ columnIds: number[] + /** 所属管理员 id(超管创建普通账号时可指定,对齐 admin 的 created_by_id)。 */ + createdBy?: number | null } export interface CreateUserFormErrors { @@ -25,10 +27,12 @@ export interface CreateUserPayload { password: string role: string columnIds: number[] + /** 仅 role=normal 且选了所属管理员时发送,与参考 admin.js 的 created_by_id 语义一致。 */ + createdById?: number } export function createEmptyCreateUserForm(): CreateUserForm { - return { username: '', password: '', role: 'normal', columnIds: [] } + return { username: '', password: '', role: 'normal', columnIds: [], createdBy: null } } /** 未知/空白/超管角色一律归一到 normal,与后端 "admin"/"normal" 白名单一致。 */ @@ -54,12 +58,17 @@ export function validateCreateUserForm(form: CreateUserForm): { valid: boolean; return { valid: Object.keys(errors).length === 0, errors } } -/** 表单 → 创建请求体:用户名去空格、角色归一、菜单 id 拷贝不共享引用。 */ +/** 表单 → 创建请求体:用户名去空格、角色归一、菜单 id 拷贝不共享引用; + * createdById 仅在 role=normal 且选了所属管理员时携带(对齐 admin.js 的 created_by_id 语义)。 */ export function toCreateUserRequest(form: CreateUserForm): CreateUserPayload { - return { + const payload: CreateUserPayload = { username: (form.username || '').trim(), password: form.password || '', role: normalizeCreateRole(form.role), columnIds: Array.isArray(form.columnIds) ? form.columnIds.slice() : [], } + if (payload.role === 'normal' && form.createdBy != null) { + payload.createdById = form.createdBy + } + return payload } diff --git a/admin-frontend-vue/tests/align-user-createdby.test.ts b/admin-frontend-vue/tests/align-user-createdby.test.ts new file mode 100644 index 00000000..1ab7cd3c --- /dev/null +++ b/admin-frontend-vue/tests/align-user-createdby.test.ts @@ -0,0 +1,54 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + createEmptyCreateUserForm, + toCreateUserRequest, + type CreateUserForm, +} from '../src/pages/account/user-create-model.ts' + +/** 对齐 admin.js:904-923,1020:超管创建普通账号时可指定所属管理员(created_by_id)。 */ + +test('align_user_created_by_normal_with_created_by_in_payload', () => { + // 主路径:超管建 normal 并选了所属管理员 → 请求体带 createdById。 + const form: CreateUserForm = { username: '张三', password: '123456', role: 'normal', columnIds: [], createdBy: 7 } + const payload = toCreateUserRequest(form) + assert.equal(payload.createdById, 7) +}) + +test('align_user_created_by_normal_without_selection_omits_field', () => { + // 未选归属(null/undefined)时不发 createdById,与参考"未选则不发 created_by_id"一致。 + const form: CreateUserForm = { username: '张三', password: '123456', role: 'normal', columnIds: [] } + const payload = toCreateUserRequest(form) + assert.equal('createdById' in payload, false) +}) + +test('align_user_created_by_admin_role_never_sends_created_by', () => { + // 角色为 admin 时即使选了归属也不发 createdById(参考仅 role=normal 且超管时发送)。 + const form: CreateUserForm = { username: '李四', password: '123456', role: 'admin', columnIds: [], createdBy: 7 } + const payload = toCreateUserRequest(form) + assert.equal('createdById' in payload, false) +}) + +test('align_user_created_by_empty_form_factory_resets_created_by', () => { + // 空表单工厂:createdBy 初始为空,保证弹窗重开时归属下拉清空。 + const form = createEmptyCreateUserForm() + assert.equal(form.createdBy, null) +}) + +test('align_user_created_by_serialization_is_pure', () => { + // 序列化纯函数:不改输入、幂等。 + const form: CreateUserForm = { username: '王五', password: '123456', role: 'normal', columnIds: [1, 2], createdBy: 3 } + assert.deepEqual(toCreateUserRequest(form), toCreateUserRequest(form)) + assert.equal(form.createdBy, 3) +}) + +test('align_user_created_by_dialog_ui_wiring', () => { + // UI 接线:弹窗仅在"超管 + 普通账号"时显示所属管理员下拉,选项来自父页传参。 + const dialog = readSource('src/pages/account/CreateUserDialog.vue') + assert.match(dialog, /adminOptions|admin-options/i, '弹窗接收父页管理员候选') + assert.match(dialog, /operatorSuper\s*&&\s*form\.role\s*===\s*'normal'/, '仅超管建普通账号时显示归属下拉') + assert.match(dialog, /请选择管理员/, '占位文案与参考一致') + const page = readSource('src/pages/account/UsersPage.vue') + assert.match(page, /:admin-options="admins"/, '父页传入列表响应的 admins') +})