Commit 09968a802bcc946954b9ab470468c5d4df59bdc0

Authored by zichun
1 parent 7cfb6ee7

feat(usr): 用户列表查询 API 与类型契约 listUsers REQ-USR-003

frontend/src/api/types.ts
... ... @@ -27,3 +27,43 @@ export interface CompanyOption {
27 27 sCompanyName: string;
28 28 sVersion: string | null;
29 29 }
  30 +
  31 +// === REQ-USR-003 用户列表查询契约(FE-03) ===
  32 +
  33 +/**
  34 + * 用户列表行 VO(已在 api 层把 docs/05 中文键 `员工名`/`部门` 归一为 ASCII
  35 + * `employeeName`/`departmentName`,组件/列定义统一用 ASCII 键,D-PLAN-2 / spec D9)。
  36 + */
  37 +export interface UserVO {
  38 + id: number;
  39 + sUserName: string;
  40 + employeeName: string | null;
  41 + sUserNo: string | null;
  42 + departmentName: string | null;
  43 + sUserType: string;
  44 + sLanguage: string;
  45 + iIsVoid: number;
  46 + tLastLoginDate: string | null;
  47 + sCreator: string;
  48 + tCreateDate: string;
  49 +}
  50 +
  51 +/** 统一分页返回体(docs/04 § 1.4 / § 3.2) */
  52 +export interface PageResult<T> {
  53 + records: T[];
  54 + total: number;
  55 + pageNum: number;
  56 + pageSize: number;
  57 +}
  58 +
  59 +/**
  60 + * GET /api/usr/users 查询参数(提交给后端的 query)。
  61 + * `queryValue` 为空字符串时由 listUsers 省略(空为全部,BR3)。
  62 + */
  63 +export interface UserListQuery {
  64 + queryField?: string;
  65 + matchType?: string;
  66 + queryValue?: string;
  67 + pageNum: number;
  68 + pageSize: number;
  69 +}
... ...
frontend/src/api/usrApi.ts
1 1 // REQ-USR-004: USR 模块 API 封装(登录 + 版本下拉取数)。页面只调本文件,不散用 axios。
  2 +// REQ-USR-003: 新增用户列表分页查询 listUsers(GET /api/usr/users,中文键归一 D-PLAN-2)。
2 3 import request from './request';
3   -import type { LoginPayload, LoginResult, CompanyOption } from './types';
  4 +import type {
  5 + LoginPayload,
  6 + LoginResult,
  7 + CompanyOption,
  8 + UserVO,
  9 + PageResult,
  10 + UserListQuery,
  11 +} from './types';
4 12  
5 13 // 响应拦截器已拆 Result.data,故此处返回类型即业务数据本体。
6 14 // axios 实例的方法类型仍标注为 AxiosResponse,运行时已被拦截器解包,用 as unknown 桥接。
... ... @@ -14,3 +22,63 @@ export function login(payload: LoginPayload): Promise&lt;LoginResult&gt; {
14 22 export function fetchCompanies(): Promise<CompanyOption[]> {
15 23 return request.get('/usr/companies') as unknown as Promise<CompanyOption[]>;
16 24 }
  25 +
  26 +/** 后端原始用户行(docs/05 以中文键名 `员工名`/`部门` 给出,关联职员表派生) */
  27 +interface RawUserRecord {
  28 + id: number;
  29 + sUserName: string;
  30 + 员工名?: string | null;
  31 + employeeName?: string | null;
  32 + sUserNo: string | null;
  33 + 部门?: string | null;
  34 + departmentName?: string | null;
  35 + sUserType: string;
  36 + sLanguage: string;
  37 + iIsVoid: number;
  38 + tLastLoginDate: string | null;
  39 + sCreator: string;
  40 + tCreateDate: string;
  41 +}
  42 +
  43 +/** 把后端中文键 `员工名`/`部门` 归一为 ASCII `employeeName`/`departmentName`(D-PLAN-2 / spec D9) */
  44 +function normalizeUserRecord(raw: RawUserRecord): UserVO {
  45 + return {
  46 + id: raw.id,
  47 + sUserName: raw.sUserName,
  48 + employeeName: raw.employeeName ?? raw['员工名'] ?? null,
  49 + sUserNo: raw.sUserNo,
  50 + departmentName: raw.departmentName ?? raw['部门'] ?? null,
  51 + sUserType: raw.sUserType,
  52 + sLanguage: raw.sLanguage,
  53 + iIsVoid: raw.iIsVoid,
  54 + tLastLoginDate: raw.tLastLoginDate,
  55 + sCreator: raw.sCreator,
  56 + tCreateDate: raw.tCreateDate,
  57 + };
  58 +}
  59 +
  60 +/**
  61 + * GET /api/usr/users —— 用户列表分页查询(只读,BR5)。
  62 + * query 走 axios params;`queryValue` 为空字符串时省略(空为全部,BR3)。
  63 + * 响应拦截器已拆 Result.data,故此处拿到 PageResult<RawUserRecord> 本体;
  64 + * 对 records 逐项做中文键→ASCII 归一后返回 PageResult<UserVO>(D-PLAN-2)。
  65 + */
  66 +export async function listUsers(query: UserListQuery): Promise<PageResult<UserVO>> {
  67 + const params: Record<string, unknown> = {
  68 + pageNum: query.pageNum,
  69 + pageSize: query.pageSize,
  70 + };
  71 + if (query.queryField) params.queryField = query.queryField;
  72 + if (query.matchType) params.matchType = query.matchType;
  73 + if (query.queryValue) params.queryValue = query.queryValue;
  74 +
  75 + const page = (await (request.get('/usr/users', { params }) as unknown as Promise<
  76 + PageResult<RawUserRecord>
  77 + >));
  78 + return {
  79 + records: (page.records ?? []).map(normalizeUserRecord),
  80 + total: page.total,
  81 + pageNum: page.pageNum,
  82 + pageSize: page.pageSize,
  83 + };
  84 +}
... ...
frontend/tests/unit/usrApi.userlist.test.ts 0 → 100644
  1 +// REQ-USR-003: listUsers API 封装单测(GET /api/usr/users + 中文键归一 D-PLAN-2)
  2 +import { describe, it, expect, vi, beforeEach } from 'vitest';
  3 +
  4 +// 桩掉底层 axios 实例(request.ts 的 default 导出),沿用 usrApi.test.ts 模式
  5 +vi.mock('../../src/api/request', () => {
  6 + return {
  7 + default: {
  8 + post: vi.fn(),
  9 + get: vi.fn(),
  10 + },
  11 + };
  12 +});
  13 +
  14 +import request from '../../src/api/request';
  15 +import { listUsers } from '../../src/api/usrApi';
  16 +
  17 +const mockedRequest = request as unknown as {
  18 + post: ReturnType<typeof vi.fn>;
  19 + get: ReturnType<typeof vi.fn>;
  20 +};
  21 +
  22 +function pageBody(records: unknown[], total = 1, pageNum = 1, pageSize = 10) {
  23 + return { records, total, pageNum, pageSize };
  24 +}
  25 +
  26 +describe('usrApi.listUsers', () => {
  27 + beforeEach(() => {
  28 + vi.clearAllMocks();
  29 + });
  30 +
  31 + it('listUsers gets /usr/users with query params', async () => {
  32 + mockedRequest.get.mockResolvedValue(pageBody([], 0, 2, 20));
  33 + await listUsers({
  34 + queryField: '用户名',
  35 + matchType: '包含',
  36 + queryValue: '李',
  37 + pageNum: 2,
  38 + pageSize: 20,
  39 + });
  40 + expect(mockedRequest.get).toHaveBeenCalledTimes(1);
  41 + const [url, config] = mockedRequest.get.mock.calls[0];
  42 + expect(url).toBe('/usr/users');
  43 + expect(config.params).toMatchObject({
  44 + queryField: '用户名',
  45 + matchType: '包含',
  46 + queryValue: '李',
  47 + pageNum: 2,
  48 + pageSize: 20,
  49 + });
  50 + });
  51 +
  52 + it('listUsers omits empty queryValue', async () => {
  53 + mockedRequest.get.mockResolvedValue(pageBody([], 0, 1, 10));
  54 + await listUsers({
  55 + queryField: '用户名',
  56 + matchType: '包含',
  57 + queryValue: '',
  58 + pageNum: 1,
  59 + pageSize: 10,
  60 + });
  61 + const [, config] = mockedRequest.get.mock.calls[0];
  62 + expect(config.params).not.toHaveProperty('queryValue');
  63 + });
  64 +
  65 + it('listUsers normalizes chinese keys 员工名/部门 to employeeName/departmentName', async () => {
  66 + mockedRequest.get.mockResolvedValue(
  67 + pageBody(
  68 + [
  69 + {
  70 + id: 1,
  71 + sUserName: 'a',
  72 + 员工名: '张三',
  73 + 部门: '技术',
  74 + sUserNo: 'a',
  75 + sUserType: '超级管理员',
  76 + sLanguage: '中文',
  77 + iIsVoid: 0,
  78 + tLastLoginDate: null,
  79 + sCreator: 'x',
  80 + tCreateDate: 't',
  81 + },
  82 + ],
  83 + 1,
  84 + 2,
  85 + 20,
  86 + ),
  87 + );
  88 + const ret = await listUsers({ pageNum: 2, pageSize: 20 });
  89 + expect(ret.records[0].employeeName).toBe('张三');
  90 + expect(ret.records[0].departmentName).toBe('技术');
  91 + expect(ret.total).toBe(1);
  92 + expect(ret.pageNum).toBe(2);
  93 + expect(ret.pageSize).toBe(20);
  94 + });
  95 +});
... ...