From 41cb4ce174bbf7e83a01c9903c05b578ad92a758 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:13:03 +0800 Subject: [PATCH] =?UTF-8?q?task-250(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=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=A1=B5=E5=AF=B9=E9=BD=90=EF=BC=88=E8=AF=B4=E6=98=8E?= =?UTF-8?q?/=E6=89=80=E5=B1=9E=E7=AE=A1=E7=90=86=E5=91=98=E7=AD=9B?= =?UTF-8?q?=E9=80=89/=E8=A7=92=E8=89=B2=E4=B8=AD=E6=96=87/=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=96=87=E6=A1=88/=E5=88=86=E9=A1=B5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin-frontend-vue/src/api/users-model.ts | 11 +++ .../src/pages/account/UsersPage.vue | 68 +++++++++++++++---- admin-frontend-vue/tests/task-250.test.ts | 66 ++++++++++++++++++ 3 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 admin-frontend-vue/tests/task-250.test.ts diff --git a/admin-frontend-vue/src/api/users-model.ts b/admin-frontend-vue/src/api/users-model.ts index 4f326340..7ac8a2fc 100644 --- a/admin-frontend-vue/src/api/users-model.ts +++ b/admin-frontend-vue/src/api/users-model.ts @@ -50,6 +50,17 @@ export function parseUserPage(payload: unknown): UserPageResult { if (typeof rawSize === 'number' && rawSize >= 1) out.pageSize = Math.floor(rawSize) if (typeof record.current_user_id === 'number') out.currentUserId = record.current_user_id if (typeof record.current_user_username === 'string') out.currentUserUsername = record.current_user_username + if (Array.isArray(record.admins)) { + out.admins = record.admins + .map((raw) => { + if (!raw || typeof raw !== 'object') return null + const r = raw as Record + const id = numberOrNull(r.id) + if (id === null) return null + return { id, username: text(r.username) } + }) + .filter((a): a is { id: number; username: string } => a !== null) + } return out } diff --git a/admin-frontend-vue/src/pages/account/UsersPage.vue b/admin-frontend-vue/src/pages/account/UsersPage.vue index 55a5ea43..5d45a081 100644 --- a/admin-frontend-vue/src/pages/account/UsersPage.vue +++ b/admin-frontend-vue/src/pages/account/UsersPage.vue @@ -3,17 +3,23 @@ import { computed, onMounted, reactive, ref } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import type { AdminUser } from '@/types/admin' import { roleLabel } from '@/types/admin' +import { useAdminSessionStore } from '@/stores/admin-session' import { deleteUser, fetchUserList } from '@/api/users' -import { createUserFilterState, toUserListParams } from './users-filter' +import { createUserFilterState, toUserListParams, type UserFilterState } from './users-filter' import { totalPageCount } from './user-pagination' -import { deleteBlockReason } from './user-delete-model' -import { listPhaseOf } from './user-list-state' +import { deleteBlockReason, deleteConfirmMessage } from './user-delete-model' +import { listPhaseOf, userListEmptyHint } from './user-list-state' +const session = useAdminSessionStore() const loading = ref(false) const loadError = ref('') const rows = ref([]) const total = ref(0) -const filters = reactive(createUserFilterState()) +/** 所属管理员候选(来自列表响应的 admins),供超管筛选下拉使用。 */ +const admins = ref>([]) +const filters = reactive(createUserFilterState()) +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 })) async function loadUsers() { @@ -23,6 +29,7 @@ async function loadUsers() { const page = await fetchUserList(toUserListParams(filters)) rows.value = page.items total.value = page.total + if (Array.isArray(page.admins)) admins.value = page.admins // 数据被删/筛选后总页数收缩:页码越界时回钳到末页并重新加载一次。 const last = totalPageCount(page.total, filters.pageSize) if (last > 0 && filters.page > last) { @@ -51,13 +58,17 @@ function search() { } async function removeUser(row: AdminUser) { - const reason = deleteBlockReason(row) + const reason = deleteBlockReason(row, currentUserId.value) if (reason) { ElMessage.warning(reason) return } try { - await ElMessageBox.confirm(`确定删除用户“${row.username}”吗?`, '删除确认', { type: 'warning' }) + await ElMessageBox.confirm(deleteConfirmMessage(row), '删除用户', { + confirmButtonText: '删除', + cancelButtonText: '取消', + type: 'warning', + }) await deleteUser(row.id) ElMessage.success('删除成功') await loadUsers() @@ -79,14 +90,30 @@ onMounted(loadUsers)

用户管理

-

管理后台账号、角色和创建关系。

+

当前没有开放注册入口,仅管理员可在此创建用户。层级关系:超级管理员 -> 管理员 -> 普通账号。

- 新建用户
- + - - + + + + + + + 查询 @@ -106,7 +133,7 @@ onMounted(loadUsers) 重试 - + @@ -118,7 +145,14 @@ onMounted(loadUsers) @@ -126,7 +160,7 @@ onMounted(loadUsers) 共 {{ total }} 条 - + diff --git a/admin-frontend-vue/tests/task-250.test.ts b/admin-frontend-vue/tests/task-250.test.ts new file mode 100644 index 00000000..243d05e5 --- /dev/null +++ b/admin-frontend-vue/tests/task-250.test.ts @@ -0,0 +1,66 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { createUserFilterState, toUserListParams } from '../src/pages/account/users-filter.ts' +import { deleteConfirmMessage } from '../src/pages/account/user-delete-model.ts' + +// module 13 task 250:用户管理列表页对齐 admin panel-users(页头描述/筛选含所属管理员/角色中文/分页)。 + +test('test_task_250_users_list_normal_primary_path', () => { + const page = readSource('src/pages/account/UsersPage.vue') + assert.match(page, /roleLabel/, '角色列需走中文 roleLabel 映射') + assert.match(page, /所属管理员/, '需展示“所属管理员”列/筛选项') + assert.match(page, /无开放注册|开放注册入口/, '页头应有“无开放注册入口”说明') +}) + +test('test_task_250_users_list_normal_variant_input', () => { + // 正常变体:删除确认与不可删原因复用用户模型文案。 + const page = readSource('src/pages/account/UsersPage.vue') + assert.match(page, /deleteConfirmMessage/, '删除确认应复用 deleteConfirmMessage') + assert.match(page, /deleteBlockReason/, '不可删原因应复用 deleteBlockReason') + assert.match(page, /creatorUsername/, '列表应含所属管理员列') +}) + +test('test_task_250_users_list_normal_repeated_operation_is_idempotent', () => { + // 筛选映射:createdByAdminId -> created_by_id。 + const state = { ...createUserFilterState(), createdByAdminId: 7 } + const params = toUserListParams(state) + assert.equal(params.createdById, 7) + const plain = toUserListParams(createUserFilterState()) + assert.equal(plain.createdById, null) +}) + +test('test_task_250_users_list_boundary_empty_input', () => { + // 边界:空结果应有明确空态占位。 + const page = readSource('src/pages/account/UsersPage.vue') + assert.match(page, /el-empty|暂无用户|userListEmptyHint|empty-text/, '空态需有用户可读提示') +}) + +test('test_task_250_users_list_boundary_single_item', () => { + // 边界:分页/合计形态为“共 N 条”,查询后回第一页。 + const page = readSource('src/pages/account/UsersPage.vue') + assert.match(page, /共 .*total.*条/, '分页页脚应有“共 N 条”合计') + assert.match(page, /page\s*=\s*1|filters\.page\s*=\s*USER_PAGE_MIN_PAGE/, '查询需重置回第一页') +}) + +test('test_task_250_users_list_boundary_limit_or_missing_field', () => { + // 边界上限:所属管理员筛选仅超管可见(非超管隐藏)。 + const page = readSource('src/pages/account/UsersPage.vue') + assert.match(page, /isSuperAdmin|super_admin|createdByAdminId/, '需按超管身份控制管理员筛选项') + assert.ok(deleteConfirmMessage({ id: 1, username: 'demo', role: 'normal' }).includes('确定删除用户“demo”吗?')) +}) + +test('test_task_250_users_list_invalid_input_rejected', () => { + // 异常:不得再直出英文角色码(已改中文)。 + const page = readSource('src/pages/account/UsersPage.vue') + assert.equal(/prop="role"/.test(page), false, '角色列不得再直出 prop=role 英文') + assert.equal(/admin-user-role/.test(page), false) +}) + +test('test_task_250_users_list_dependency_failure_returns_actionable_message', () => { + // 依赖:列表响应解析 admins(供所属管理员下拉)。 + const model = readSource('src/api/users-model.ts') + assert.match(model, /record\.admins/, 'users-model 需解析 admins') + const dto = readSource('src/api/users-dto.ts') + assert.match(dto, /admins\?:/, 'DTO 需声明 admins') +})