From 07fda4cf2286842089ced02289e4cbc303441d76 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 21:17:15 +0800 Subject: [PATCH] =?UTF-8?q?task-251(admin.html=E8=A7=82=E6=84=9F=E5=AF=B9?= =?UTF-8?q?=E9=BD=90):=20=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=E6=96=B0=E5=BB=BA/=E7=BC=96=E8=BE=91/=E8=A7=92?= =?UTF-8?q?=E8=89=B2+=E8=8F=9C=E5=8D=95=E6=8E=88=E6=9D=83=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=EF=BC=88=E6=8E=A5=E7=BA=BF=E5=AD=A4=E5=84=BF=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/account/CreateUserDialog.vue | 113 ++++++++++++++++ .../src/pages/account/EditUserDialog.vue | 128 ++++++++++++++++++ .../src/pages/account/UserMenuAuthTree.vue | 67 +++++++++ .../src/pages/account/UsersPage.vue | 27 +++- .../src/pages/account/user-edit-adapter.ts | 23 ++++ admin-frontend-vue/tests/task-251.test.ts | 66 +++++++++ admin-frontend-vue/tests/task-43.test.ts | 4 +- 7 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 admin-frontend-vue/src/pages/account/CreateUserDialog.vue create mode 100644 admin-frontend-vue/src/pages/account/EditUserDialog.vue create mode 100644 admin-frontend-vue/src/pages/account/UserMenuAuthTree.vue create mode 100644 admin-frontend-vue/src/pages/account/user-edit-adapter.ts create mode 100644 admin-frontend-vue/tests/task-251.test.ts diff --git a/admin-frontend-vue/src/pages/account/CreateUserDialog.vue b/admin-frontend-vue/src/pages/account/CreateUserDialog.vue new file mode 100644 index 00000000..9cffc75a --- /dev/null +++ b/admin-frontend-vue/src/pages/account/CreateUserDialog.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/admin-frontend-vue/src/pages/account/EditUserDialog.vue b/admin-frontend-vue/src/pages/account/EditUserDialog.vue new file mode 100644 index 00000000..490f3b49 --- /dev/null +++ b/admin-frontend-vue/src/pages/account/EditUserDialog.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/admin-frontend-vue/src/pages/account/UserMenuAuthTree.vue b/admin-frontend-vue/src/pages/account/UserMenuAuthTree.vue new file mode 100644 index 00000000..dfbc758a --- /dev/null +++ b/admin-frontend-vue/src/pages/account/UserMenuAuthTree.vue @@ -0,0 +1,67 @@ + + + diff --git a/admin-frontend-vue/src/pages/account/UsersPage.vue b/admin-frontend-vue/src/pages/account/UsersPage.vue index 5d45a081..c7b536fd 100644 --- a/admin-frontend-vue/src/pages/account/UsersPage.vue +++ b/admin-frontend-vue/src/pages/account/UsersPage.vue @@ -9,6 +9,9 @@ import { createUserFilterState, toUserListParams, type UserFilterState } from '. import { totalPageCount } from './user-pagination' import { deleteBlockReason, deleteConfirmMessage } from './user-delete-model' import { listPhaseOf, userListEmptyHint } from './user-list-state' +import { isEditableUser } from './user-edit-model' +import CreateUserDialog from './CreateUserDialog.vue' +import EditUserDialog from './EditUserDialog.vue' const session = useAdminSessionStore() const loading = ref(false) @@ -22,6 +25,24 @@ const isSuperAdmin = computed(() => session.isSuperAdmin) const currentUserId = computed(() => session.user?.id ?? null) const statePhase = computed(() => listPhaseOf({ loading: loading.value, error: loadError.value, items: rows.value, total: total.value })) +const createVisible = ref(false) +const editVisible = ref(false) +const editUser = ref(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 常量')