align(用户管理): 超管新建普通账号补所属管理员(createdBy)下拉,payload 仅 normal+已选时带 createdById(对齐 admin.js created_by_id 语义)

This commit is contained in:
2026-09-05 22:54:49 +08:00
parent 359f3ce1c9
commit 63339b401c
4 changed files with 84 additions and 5 deletions
@@ -11,7 +11,12 @@ import { showAdminFeedback } from '@/components/admin-feedback-ui'
import { actionableErrorText } from '@/components/admin-feedback' import { actionableErrorText } from '@/components/admin-feedback'
import UserMenuAuthTree from './UserMenuAuthTree.vue' 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 emit = defineEmits<{ (e: 'update:modelValue', v: boolean): void; (e: 'created'): void }>()
const form = reactive<CreateUserForm>(createEmptyCreateUserForm()) const form = reactive<CreateUserForm>(createEmptyCreateUserForm())
@@ -32,6 +37,7 @@ watch(
form.password = '' form.password = ''
form.role = 'normal' form.role = 'normal'
form.columnIds = [] form.columnIds = []
form.createdBy = null
errors.username = undefined errors.username = undefined
errors.password = undefined errors.password = undefined
}, },
@@ -90,6 +96,16 @@ async function create(): Promise<void> {
<el-option v-for="opt in ROLE_CHOICES" :key="opt.value" :label="opt.label" :value="opt.value" /> <el-option v-for="opt in ROLE_CHOICES" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="operatorSuper && form.role === 'normal'" label="所属管理员">
<el-select v-model="form.createdBy" clearable filterable placeholder="请选择管理员" style="width: 200px">
<el-option
v-for="admin in props.adminOptions || []"
:key="admin.id"
:label="admin.username"
:value="admin.id"
/>
</el-select>
</el-form-item>
<el-form-item label="菜单权限勾选上级菜单会继承全部子菜单"> <el-form-item label="菜单权限勾选上级菜单会继承全部子菜单">
<div class="auth-tree-box"> <div class="auth-tree-box">
<UserMenuAuthTree v-model:checked="form.columnIds" /> <UserMenuAuthTree v-model:checked="form.columnIds" />
@@ -194,7 +194,7 @@ onMounted(loadUsers)
/> />
</div> </div>
</el-card> </el-card>
<CreateUserDialog v-model="createVisible" :operator-super="isSuperAdmin" @created="loadUsers" /> <CreateUserDialog v-model="createVisible" :operator-super="isSuperAdmin" :admin-options="admins" @created="loadUsers" />
<EditUserDialog v-model="editVisible" :user="editUser" :operator-super="isSuperAdmin" @updated="loadUsers" /> <EditUserDialog v-model="editVisible" :user="editUser" :operator-super="isSuperAdmin" @updated="loadUsers" />
</div> </div>
</template> </template>
@@ -12,6 +12,8 @@ export interface CreateUserForm {
role: CreatableUserRole role: CreatableUserRole
/** 授权菜单 id(后端 replaceDirectPermissions 用)。 */ /** 授权菜单 id(后端 replaceDirectPermissions 用)。 */
columnIds: number[] columnIds: number[]
/** 所属管理员 id(超管创建普通账号时可指定,对齐 admin 的 created_by_id)。 */
createdBy?: number | null
} }
export interface CreateUserFormErrors { export interface CreateUserFormErrors {
@@ -25,10 +27,12 @@ export interface CreateUserPayload {
password: string password: string
role: string role: string
columnIds: number[] columnIds: number[]
/** 仅 role=normal 且选了所属管理员时发送,与参考 admin.js 的 created_by_id 语义一致。 */
createdById?: number
} }
export function createEmptyCreateUserForm(): CreateUserForm { export function createEmptyCreateUserForm(): CreateUserForm {
return { username: '', password: '', role: 'normal', columnIds: [] } return { username: '', password: '', role: 'normal', columnIds: [], createdBy: null }
} }
/** 未知/空白/超管角色一律归一到 normal,与后端 "admin"/"normal" 白名单一致。 */ /** 未知/空白/超管角色一律归一到 normal,与后端 "admin"/"normal" 白名单一致。 */
@@ -54,12 +58,17 @@ export function validateCreateUserForm(form: CreateUserForm): { valid: boolean;
return { valid: Object.keys(errors).length === 0, errors } return { valid: Object.keys(errors).length === 0, errors }
} }
/** 表单 → 创建请求体:用户名去空格、角色归一、菜单 id 拷贝不共享引用。 */ /** 表单 → 创建请求体:用户名去空格、角色归一、菜单 id 拷贝不共享引用
* createdById 仅在 role=normal 且选了所属管理员时携带(对齐 admin.js 的 created_by_id 语义)。 */
export function toCreateUserRequest(form: CreateUserForm): CreateUserPayload { export function toCreateUserRequest(form: CreateUserForm): CreateUserPayload {
return { const payload: CreateUserPayload = {
username: (form.username || '').trim(), username: (form.username || '').trim(),
password: form.password || '', password: form.password || '',
role: normalizeCreateRole(form.role), role: normalizeCreateRole(form.role),
columnIds: Array.isArray(form.columnIds) ? form.columnIds.slice() : [], columnIds: Array.isArray(form.columnIds) ? form.columnIds.slice() : [],
} }
if (payload.role === 'normal' && form.createdBy != null) {
payload.createdById = form.createdBy
}
return payload
} }
@@ -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')
})