From 09968a802bcc946954b9ab470468c5d4df59bdc0 Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 17:35:11 +0800 Subject: [PATCH] feat(usr): 用户列表查询 API 与类型契约 listUsers REQ-USR-003 --- frontend/src/api/types.ts | 40 ++++++++++++++++++++++++++++++++++++++++ frontend/src/api/usrApi.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- frontend/tests/unit/usrApi.userlist.test.ts | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 frontend/tests/unit/usrApi.userlist.test.ts diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 0919dd7..1c01c02 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -27,3 +27,43 @@ export interface CompanyOption { sCompanyName: string; sVersion: string | null; } + +// === REQ-USR-003 用户列表查询契约(FE-03) === + +/** + * 用户列表行 VO(已在 api 层把 docs/05 中文键 `员工名`/`部门` 归一为 ASCII + * `employeeName`/`departmentName`,组件/列定义统一用 ASCII 键,D-PLAN-2 / spec D9)。 + */ +export interface UserVO { + id: number; + sUserName: string; + employeeName: string | null; + sUserNo: string | null; + departmentName: string | null; + sUserType: string; + sLanguage: string; + iIsVoid: number; + tLastLoginDate: string | null; + sCreator: string; + tCreateDate: string; +} + +/** 统一分页返回体(docs/04 § 1.4 / § 3.2) */ +export interface PageResult { + records: T[]; + total: number; + pageNum: number; + pageSize: number; +} + +/** + * GET /api/usr/users 查询参数(提交给后端的 query)。 + * `queryValue` 为空字符串时由 listUsers 省略(空为全部,BR3)。 + */ +export interface UserListQuery { + queryField?: string; + matchType?: string; + queryValue?: string; + pageNum: number; + pageSize: number; +} diff --git a/frontend/src/api/usrApi.ts b/frontend/src/api/usrApi.ts index 187092c..2ade4ba 100644 --- a/frontend/src/api/usrApi.ts +++ b/frontend/src/api/usrApi.ts @@ -1,6 +1,14 @@ // REQ-USR-004: USR 模块 API 封装(登录 + 版本下拉取数)。页面只调本文件,不散用 axios。 +// REQ-USR-003: 新增用户列表分页查询 listUsers(GET /api/usr/users,中文键归一 D-PLAN-2)。 import request from './request'; -import type { LoginPayload, LoginResult, CompanyOption } from './types'; +import type { + LoginPayload, + LoginResult, + CompanyOption, + UserVO, + PageResult, + UserListQuery, +} from './types'; // 响应拦截器已拆 Result.data,故此处返回类型即业务数据本体。 // axios 实例的方法类型仍标注为 AxiosResponse,运行时已被拦截器解包,用 as unknown 桥接。 @@ -14,3 +22,63 @@ export function login(payload: LoginPayload): Promise { export function fetchCompanies(): Promise { return request.get('/usr/companies') as unknown as Promise; } + +/** 后端原始用户行(docs/05 以中文键名 `员工名`/`部门` 给出,关联职员表派生) */ +interface RawUserRecord { + id: number; + sUserName: string; + 员工名?: string | null; + employeeName?: string | null; + sUserNo: string | null; + 部门?: string | null; + departmentName?: string | null; + sUserType: string; + sLanguage: string; + iIsVoid: number; + tLastLoginDate: string | null; + sCreator: string; + tCreateDate: string; +} + +/** 把后端中文键 `员工名`/`部门` 归一为 ASCII `employeeName`/`departmentName`(D-PLAN-2 / spec D9) */ +function normalizeUserRecord(raw: RawUserRecord): UserVO { + return { + id: raw.id, + sUserName: raw.sUserName, + employeeName: raw.employeeName ?? raw['员工名'] ?? null, + sUserNo: raw.sUserNo, + departmentName: raw.departmentName ?? raw['部门'] ?? null, + sUserType: raw.sUserType, + sLanguage: raw.sLanguage, + iIsVoid: raw.iIsVoid, + tLastLoginDate: raw.tLastLoginDate, + sCreator: raw.sCreator, + tCreateDate: raw.tCreateDate, + }; +} + +/** + * GET /api/usr/users —— 用户列表分页查询(只读,BR5)。 + * query 走 axios params;`queryValue` 为空字符串时省略(空为全部,BR3)。 + * 响应拦截器已拆 Result.data,故此处拿到 PageResult 本体; + * 对 records 逐项做中文键→ASCII 归一后返回 PageResult(D-PLAN-2)。 + */ +export async function listUsers(query: UserListQuery): Promise> { + const params: Record = { + pageNum: query.pageNum, + pageSize: query.pageSize, + }; + if (query.queryField) params.queryField = query.queryField; + if (query.matchType) params.matchType = query.matchType; + if (query.queryValue) params.queryValue = query.queryValue; + + const page = (await (request.get('/usr/users', { params }) as unknown as Promise< + PageResult + >)); + return { + records: (page.records ?? []).map(normalizeUserRecord), + total: page.total, + pageNum: page.pageNum, + pageSize: page.pageSize, + }; +} diff --git a/frontend/tests/unit/usrApi.userlist.test.ts b/frontend/tests/unit/usrApi.userlist.test.ts new file mode 100644 index 0000000..f838c71 --- /dev/null +++ b/frontend/tests/unit/usrApi.userlist.test.ts @@ -0,0 +1,95 @@ +// REQ-USR-003: listUsers API 封装单测(GET /api/usr/users + 中文键归一 D-PLAN-2) +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// 桩掉底层 axios 实例(request.ts 的 default 导出),沿用 usrApi.test.ts 模式 +vi.mock('../../src/api/request', () => { + return { + default: { + post: vi.fn(), + get: vi.fn(), + }, + }; +}); + +import request from '../../src/api/request'; +import { listUsers } from '../../src/api/usrApi'; + +const mockedRequest = request as unknown as { + post: ReturnType; + get: ReturnType; +}; + +function pageBody(records: unknown[], total = 1, pageNum = 1, pageSize = 10) { + return { records, total, pageNum, pageSize }; +} + +describe('usrApi.listUsers', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('listUsers gets /usr/users with query params', async () => { + mockedRequest.get.mockResolvedValue(pageBody([], 0, 2, 20)); + await listUsers({ + queryField: '用户名', + matchType: '包含', + queryValue: '李', + pageNum: 2, + pageSize: 20, + }); + expect(mockedRequest.get).toHaveBeenCalledTimes(1); + const [url, config] = mockedRequest.get.mock.calls[0]; + expect(url).toBe('/usr/users'); + expect(config.params).toMatchObject({ + queryField: '用户名', + matchType: '包含', + queryValue: '李', + pageNum: 2, + pageSize: 20, + }); + }); + + it('listUsers omits empty queryValue', async () => { + mockedRequest.get.mockResolvedValue(pageBody([], 0, 1, 10)); + await listUsers({ + queryField: '用户名', + matchType: '包含', + queryValue: '', + pageNum: 1, + pageSize: 10, + }); + const [, config] = mockedRequest.get.mock.calls[0]; + expect(config.params).not.toHaveProperty('queryValue'); + }); + + it('listUsers normalizes chinese keys 员工名/部门 to employeeName/departmentName', async () => { + mockedRequest.get.mockResolvedValue( + pageBody( + [ + { + id: 1, + sUserName: 'a', + 员工名: '张三', + 部门: '技术', + sUserNo: 'a', + sUserType: '超级管理员', + sLanguage: '中文', + iIsVoid: 0, + tLastLoginDate: null, + sCreator: 'x', + tCreateDate: 't', + }, + ], + 1, + 2, + 20, + ), + ); + const ret = await listUsers({ pageNum: 2, pageSize: 20 }); + expect(ret.records[0].employeeName).toBe('张三'); + expect(ret.records[0].departmentName).toBe('技术'); + expect(ret.total).toBe(1); + expect(ret.pageNum).toBe(2); + expect(ret.pageSize).toBe(20); + }); +}); -- libgit2 0.22.2