(null)
+
+function openCreate(): void {
+ createVisible.value = true
+}
+
+function openEdit(row: AdminUser): void {
+ if (!isEditableUser(row.role)) return
+ editUser.value = row
+ editVisible.value = true
+}
+
+function editableOf(row: AdminUser): boolean {
+ return isEditableUser(row.role)
+}
+
async function loadUsers() {
loading.value = true
loadError.value = ''
@@ -92,6 +113,7 @@ onMounted(loadUsers)
用户管理
当前没有开放注册入口,仅管理员可在此创建用户。层级关系:超级管理员 -> 管理员 -> 普通账号。
+ 新建用户
@@ -143,8 +165,9 @@ onMounted(loadUsers)
-
+
+ 编辑
+
+
diff --git a/admin-frontend-vue/src/pages/account/user-edit-adapter.ts b/admin-frontend-vue/src/pages/account/user-edit-adapter.ts
new file mode 100644
index 00000000..7ae5a9de
--- /dev/null
+++ b/admin-frontend-vue/src/pages/account/user-edit-adapter.ts
@@ -0,0 +1,23 @@
+/** 编辑用户请求适配(module 13 task 251):表单模型 → PUT /api/admin/user/{uid}。 */
+import { http } from '@/api/http'
+import { unwrap } from '@/api/envelope'
+import {
+ toEditUserUpdateRequest,
+ validateEditUserForm,
+ type EditUserForm,
+} from './user-edit-model'
+
+/** Java AdminUserController.updateUser 端点前缀。 */
+export const EDIT_USER_ENDPOINT = '/api/admin/user'
+
+/**
+ * 提交编辑用户:先校验(不通过抛首条错误),再序列化 PUT;http/后端失败交由调用方提示。
+ */
+export async function submitEditUser(form: EditUserForm): Promise {
+ const { valid, errors } = validateEditUserForm(form)
+ if (!valid) {
+ throw new Error(Object.values(errors)[0] || '编辑用户表单校验未通过')
+ }
+ const { data } = await http.put(`${EDIT_USER_ENDPOINT}/${form.uid}`, toEditUserUpdateRequest(form))
+ unwrap(data)
+}
diff --git a/admin-frontend-vue/tests/task-251.test.ts b/admin-frontend-vue/tests/task-251.test.ts
new file mode 100644
index 00000000..caa68a6b
--- /dev/null
+++ b/admin-frontend-vue/tests/task-251.test.ts
@@ -0,0 +1,66 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readSource } from './helpers.ts'
+
+// module 13 task 251:用户管理补齐新建/编辑/角色+菜单授权弹窗(接线孤儿模块)。
+
+test('test_task_251_user_dialog_normal_primary_path', () => {
+ const page = readSource('src/pages/account/UsersPage.vue')
+ assert.match(page, /新建用户/, '页头需有新建用户入口')
+ assert.match(page, /openCreate/, '新建按钮需绑定 openCreate')
+ assert.match(page, /CreateUserDialog/, '需挂载新建用户弹窗')
+ assert.match(page, /EditUserDialog/, '需挂载编辑用户弹窗')
+})
+
+test('test_task_251_user_dialog_normal_variant_input', () => {
+ const create = readSource('src/pages/account/CreateUserDialog.vue')
+ assert.match(create, /submitCreateUser/, '创建提交走 user-create-adapter')
+ assert.match(create, /validateCreateUserForm/, '创建校验走 user-create-model')
+ assert.match(create, /普通账号/, '角色文案需含“普通账号”')
+ assert.match(create, /管理员/, '角色文案需含“管理员”')
+ assert.match(create, /UserMenuAuthTree/, '创建弹窗需含菜单授权树')
+})
+
+test('test_task_251_user_dialog_normal_repeated_operation_is_idempotent', () => {
+ const edit = readSource('src/pages/account/EditUserDialog.vue')
+ assert.match(edit, /fillEditUserForm/, '编辑回填走 user-edit-model')
+ assert.match(edit, /submitEditUser/, '编辑提交走 user-edit-adapter')
+ assert.match(edit, /loadUserMenuAuth/, '编辑授权加载走 user-menu-auth-api')
+ assert.match(edit, /更新成功/, '编辑成功提示“更新成功”')
+})
+
+test('test_task_251_user_dialog_boundary_empty_input', () => {
+ const model = readSource('src/pages/account/user-create-model.ts')
+ assert.match(model, /用户名至少/, '需有“用户名至少 N 个字符”')
+ assert.match(model, /密码至少/, '需有“密码至少 N 个字符”')
+})
+
+test('test_task_251_user_dialog_boundary_single_item', () => {
+ // 超管用户不可编辑(行按钮禁用)。
+ const page = readSource('src/pages/account/UsersPage.vue')
+ assert.match(page, /editableOf/, '需有可编辑判定')
+ assert.match(page, /isEditableUser/, '可编辑判定应复用 user-edit-model')
+})
+
+test('test_task_251_user_dialog_boundary_limit_or_missing_field', () => {
+ // 边界上限:角色下拉仅超管操作者可见(非超管隐藏角色项)。
+ const create = readSource('src/pages/account/CreateUserDialog.vue')
+ assert.match(create, /operatorSuper/, '角色可改应受 operatorSuper 控制')
+ const edit = readSource('src/pages/account/EditUserDialog.vue')
+ assert.match(edit, /operatorSuper/, '编辑角色项应受 operatorSuper 控制')
+})
+
+test('test_task_251_user_dialog_invalid_input_rejected', () => {
+ // 异常:创建成功后需清空用户名/密码并保持弹窗;提交反馈走统一通道。
+ const create = readSource('src/pages/account/CreateUserDialog.vue')
+ assert.match(create, /form\.username\s*=\s*''/, '成功后清空用户名')
+ assert.match(create, /form\.password\s*=\s*''/, '成功后清空密码')
+ assert.match(create, /showAdminFeedback/, '反馈走 admin-feedback 统一通道')
+})
+
+test('test_task_251_user_dialog_dependency_failure_returns_actionable_message', () => {
+ // 依赖:编辑适配器 PUT /api/admin/user/{uid} 真实存在。
+ const adapter = readSource('src/pages/account/user-edit-adapter.ts')
+ assert.match(adapter, /http\.put/, '编辑需走 PUT')
+ assert.match(adapter, /\$\{EDIT_USER_ENDPOINT\}\/\$\{form\.uid\}/, '端点须为 /api/admin/user/{uid}')
+})
diff --git a/admin-frontend-vue/tests/task-43.test.ts b/admin-frontend-vue/tests/task-43.test.ts
index dad36fe1..ebfff86b 100644
--- a/admin-frontend-vue/tests/task-43.test.ts
+++ b/admin-frontend-vue/tests/task-43.test.ts
@@ -62,9 +62,11 @@ test('test_task_043_user_filter_state_invalid_input_rejected', () => {
test('test_task_043_user_filter_state_dependency_failure_returns_actionable_message', () => {
// 依赖失败:页面把筛选/分页状态局部化到 users-filter,不入 Pinia。
+ // (module 13 起页面允许读 session store 取操作者角色/当前用户,但筛选状态仍不落 store。)
const page = readSource('src/pages/account/UsersPage.vue')
assert.match(page, /users-filter/, '页面复用局部筛选状态模块')
- assert.equal(/defineStore|useAdminSessionStore/.test(page), false, '筛选状态不落在 session store')
+ assert.equal(/defineStore/.test(page), false, '页面不得自行 defineStore 存放筛选状态')
+ assert.match(page, /createUserFilterState/, '页面以局部状态机创建筛选状态')
const mod = readSource('src/pages/account/users-filter.ts')
assert.match(mod, /export function createUserFilterState/)
assert.match(mod, /users-dto/, '筛选模块复用分页 DTO 常量')