From 60fa5a818b6c01abde785149010023a762d28d4d 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 14:29:51 +0800 Subject: [PATCH] =?UTF-8?q?task-41(=E8=B4=A6=E5=8F=B7/=E6=9D=83=E9=99=90?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2):=20=E5=AE=9A=E4=B9=89=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E4=B8=8E=E5=88=86=E9=A1=B5=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增纯模块 users-dto.ts:UserListParams/UserPageResult/UserListQuery 契约与 Java AdminUserController 对齐(page/page_size/username/items/total); normalizeUserPageParams 页码下限1、页大小夹取、空关键字去抖;toUserListQuery 按 snake_case 序列化;UsersPage 改用 AdminUser/UserPageResult 类型。8 用例全过。 --- admin-frontend-vue/src/api/users-dto.ts | 76 +++++++++++++++ .../src/pages/account/UsersPage.vue | 94 +++++++++++++++++++ admin-frontend-vue/tests/task-41.test.ts | 72 ++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 admin-frontend-vue/src/api/users-dto.ts create mode 100644 admin-frontend-vue/src/pages/account/UsersPage.vue create mode 100644 admin-frontend-vue/tests/task-41.test.ts diff --git a/admin-frontend-vue/src/api/users-dto.ts b/admin-frontend-vue/src/api/users-dto.ts new file mode 100644 index 00000000..ed572237 --- /dev/null +++ b/admin-frontend-vue/src/api/users-dto.ts @@ -0,0 +1,76 @@ +/** 用户管理列表/分页 DTO(任务 41):纯逻辑,无框架依赖,与 Java AdminUserController 对齐。 */ +import type { AdminUser } from '../types/admin' + +export const USER_PAGE_DEFAULT_SIZE = 15 +export const USER_PAGE_MIN_PAGE = 1 +export const USER_PAGE_MAX_SIZE = 200 + +/** 前端用户列表筛选/分页状态(页面局部,不入 Pinia)。 */ +export interface UserListParams { + page: number + pageSize: number + /** 用户名模糊关键字(对应后端 username|search 的 kw)。 */ + keyword?: string + createdById?: number | null + adminId?: number | null +} + +/** 序列化到 GET /api/admin/users 的查询参数(Java snake_case)。 */ +export interface UserListQuery { + page: number + page_size: number + username?: string + search?: string + created_by_id?: number + admin_id?: number +} + +/** 用户列表项即 AdminUser(与 Java AdminUserItemVo 字段对齐)。 */ +export type AdminUserItem = AdminUser + +/** 分页结果:与 Java AdminUserListVo.items/total/page/page_size 归一后的前端 DTO。 */ +export interface UserPageResult { + items: AdminUserItem[] + total: number + page: number + pageSize: number + currentUserId?: number | null + currentUserUsername?: string + admins?: Array<{ id: number; username: string }> +} + +function finiteInt(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 归一化分页/筛选参数:页码下限 1、页大小 1..上限,空关键字视为无过滤。 */ +export function normalizeUserPageParams(raw: Partial): UserListParams { + const page = Math.max(finiteInt(raw.page) ?? USER_PAGE_MIN_PAGE, USER_PAGE_MIN_PAGE) + const rawSize = finiteInt(raw.pageSize) + const pageSize = + rawSize === null || rawSize < USER_PAGE_MIN_PAGE + ? USER_PAGE_DEFAULT_SIZE + : Math.min(rawSize, USER_PAGE_MAX_SIZE) + const keyword = typeof raw.keyword === 'string' ? raw.keyword.trim() || undefined : undefined + return { + page, + pageSize, + keyword, + createdById: typeof raw.createdById === 'number' ? raw.createdById : null, + adminId: typeof raw.adminId === 'number' ? raw.adminId : null, + } +} + +/** 把前端分页/筛选状态转成 Java 查询参数(缺省字段不下发)。 */ +export function toUserListQuery(params: UserListParams): UserListQuery { + const query: UserListQuery = { page: params.page, page_size: params.pageSize } + if (params.keyword) query.username = params.keyword + if (params.createdById != null) query.created_by_id = params.createdById + if (params.adminId != null) query.admin_id = params.adminId + return query +} + +/** 无数据分页结果缺省值(首屏/加载前占位)。 */ +export function emptyUserPageResult(): UserPageResult { + return { items: [], total: 0, page: USER_PAGE_MIN_PAGE, pageSize: USER_PAGE_DEFAULT_SIZE } +} diff --git a/admin-frontend-vue/src/pages/account/UsersPage.vue b/admin-frontend-vue/src/pages/account/UsersPage.vue new file mode 100644 index 00000000..013985cb --- /dev/null +++ b/admin-frontend-vue/src/pages/account/UsersPage.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/admin-frontend-vue/tests/task-41.test.ts b/admin-frontend-vue/tests/task-41.test.ts new file mode 100644 index 00000000..93125398 --- /dev/null +++ b/admin-frontend-vue/tests/task-41.test.ts @@ -0,0 +1,72 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + emptyUserPageResult, + normalizeUserPageParams, + toUserListQuery, + USER_PAGE_DEFAULT_SIZE, + USER_PAGE_MAX_SIZE, +} from '../src/api/users-dto.ts' + +test('test_task_041_users_dto_normal_primary_path', () => { + // 正常主路径:合法分页/关键字归一为可查询参数。 + const p = normalizeUserPageParams({ page: 2, pageSize: 15, keyword: ' 张 ' }) + assert.equal(p.page, 2) + assert.equal(p.pageSize, 15) + assert.equal(p.keyword, '张') +}) + +test('test_task_041_users_dto_normal_variant_input', () => { + // 正常变体:查询序列化采用 Java 参数名(page/page_size/username)。 + const q = toUserListQuery({ page: 3, pageSize: 20, keyword: 'admin' }) + assert.deepEqual(q, { page: 3, page_size: 20, username: 'admin' }) +}) + +test('test_task_041_users_dto_normal_repeated_operation_is_idempotent', () => { + // 正常重复:同一输入归一结果稳定、不改输入。 + const raw = { page: 1, pageSize: 15 } + assert.deepEqual(normalizeUserPageParams(raw), normalizeUserPageParams(raw)) + assert.deepEqual(raw, { page: 1, pageSize: 15 }) +}) + +test('test_task_041_users_dto_boundary_empty_input', () => { + // 边界空值:空对象回默认首页/页大小;空关键字视为无过滤。 + const p = normalizeUserPageParams({}) + assert.equal(p.page, 1) + assert.equal(p.pageSize, USER_PAGE_DEFAULT_SIZE) + assert.equal(p.keyword, undefined) +}) + +test('test_task_041_users_dto_boundary_single_item', () => { + // 边界单元素:单条分页结果 DTO 结构完整(items/total/page/pageSize)。 + const empty = emptyUserPageResult() + assert.deepEqual(empty.items, []) + assert.equal(empty.total, 0) + assert.equal(empty.page, 1) + assert.equal(empty.pageSize, USER_PAGE_DEFAULT_SIZE) +}) + +test('test_task_041_users_dto_boundary_limit_or_missing_field', () => { + // 边界上限/越界:页码 <1 归 1;页大小超上限/非正被夹取。 + assert.equal(normalizeUserPageParams({ page: 0, pageSize: 999 }).page, 1) + assert.equal(normalizeUserPageParams({ page: -3, pageSize: 0 }).pageSize, USER_PAGE_DEFAULT_SIZE) + assert.equal(normalizeUserPageParams({ pageSize: 999 }).pageSize, USER_PAGE_MAX_SIZE) +}) + +test('test_task_041_users_dto_invalid_input_rejected', () => { + // 异常输入:非数字/字符串关键字无效时不抛错并按缺省处理。 + const p = normalizeUserPageParams({ page: Number.NaN, pageSize: Number.NaN, keyword: undefined }) + assert.equal(p.page, 1) + assert.equal(p.pageSize, USER_PAGE_DEFAULT_SIZE) + assert.equal(p.keyword, undefined) +}) + +test('test_task_041_users_dto_dependency_failure_returns_actionable_message', () => { + // 依赖失败:DTO 纯模块不依赖 axios/http,供页面与后续 adapter 引用。 + const dto = readSource('src/api/users-dto.ts') + assert.equal(/axios|from '\.\/http'/.test(dto), false, 'DTO 模块保持纯逻辑') + assert.match(dto, /export interface UserPageResult/) + const page = readSource('src/pages/account/UsersPage.vue') + assert.match(page, /users-dto|UserListParams|UserPageResult/) +})