task-250(admin.html观感对齐): 用户管理列表页对齐(说明/所属管理员筛选/角色中文/删除文案/分页)
This commit is contained in:
@@ -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<string, unknown>
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AdminUser[]>([])
|
||||
const total = ref(0)
|
||||
const filters = reactive(createUserFilterState())
|
||||
/** 所属管理员候选(来自列表响应的 admins),供超管筛选下拉使用。 */
|
||||
const admins = ref<Array<{ id: number; username: string }>>([])
|
||||
const filters = reactive<UserFilterState>(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)
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<h2>用户管理</h2>
|
||||
<p>管理后台账号、角色和创建关系。</p>
|
||||
<p>当前没有开放注册入口,仅管理员可在此创建用户。层级关系:超级管理员 -> 管理员 -> 普通账号。</p>
|
||||
</div>
|
||||
<el-button type="primary">新建用户</el-button>
|
||||
</div>
|
||||
<el-card shadow="never">
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form inline @submit.prevent="search">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="filters.keyword" clearable placeholder="模糊搜索" @keyup.enter="search" />
|
||||
<el-form-item label="用户名(模糊搜索)">
|
||||
<el-input v-model="filters.keyword" clearable placeholder="输入用户名关键字" @keyup.enter="search" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isSuperAdmin" label="所属管理员">
|
||||
<el-select
|
||||
v-model="filters.createdByAdminId"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="全部"
|
||||
style="width: 200px"
|
||||
@change="search"
|
||||
>
|
||||
<el-option
|
||||
v-for="admin in admins"
|
||||
:key="admin.id"
|
||||
:label="admin.username"
|
||||
:value="admin.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
@@ -106,7 +133,7 @@ onMounted(loadUsers)
|
||||
<el-button link type="primary" @click="retry">重试</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-table v-loading="loading" :data="rows" stripe>
|
||||
<el-table v-loading="loading" :data="rows" stripe :empty-text="userListEmptyHint()">
|
||||
<el-table-column prop="id" label="ID" width="90" />
|
||||
<el-table-column prop="username" label="用户名" min-width="180" />
|
||||
<el-table-column label="角色" width="140">
|
||||
@@ -118,7 +145,14 @@ onMounted(loadUsers)
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" :disabled="deleteBlockReason(row) !== undefined" @click="removeUser(row)">删除</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="deleteBlockReason(row, currentUserId) !== undefined"
|
||||
@click="removeUser(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -126,7 +160,7 @@ onMounted(loadUsers)
|
||||
<span>共 {{ total }} 条</span>
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
layout="prev, pager, next, jumper"
|
||||
:page-size="filters.pageSize"
|
||||
:current-page="filters.page"
|
||||
:total="total"
|
||||
@@ -137,4 +171,8 @@ onMounted(loadUsers)
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.filter-card :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
Reference in New Issue
Block a user