Commit 85eeceb5c56ca2bfacb6012f876fcc26ebc68023

Authored by zichun
1 parent 93a18967

feat(usr): 用户列表搜索表单 + 分页表格 REQ-USR-003

frontend/src/api/usr.ts
... ... @@ -39,3 +39,36 @@ export function getPermissionGroups(): Promise<PermissionGroupVO[]> {
39 39 export function createUser(req: UserCreateReq): Promise<UserCreateResp> {
40 40 return request.post('/usr/users', req)
41 41 }
  42 +
  43 +export interface UserListQueryReq {
  44 + queryField?: string
  45 + matchType?: string
  46 + queryValue?: string
  47 + page?: number
  48 + pageSize?: number
  49 +}
  50 +
  51 +export interface UserListItemVO {
  52 + sId: string
  53 + sUsername: string
  54 + sUserCode: string
  55 + sUserType: string
  56 + sLanguage: string
  57 + bIsDisabled: number
  58 + tLastLoginDate: string | null
  59 + sCreatorUsername: string | null
  60 + tCreateDate: string
  61 + sStaffName: string | null
  62 + sDepartment: string | null
  63 +}
  64 +
  65 +export interface PageVO<T> {
  66 + total: number
  67 + page: number
  68 + pageSize: number
  69 + list: T[]
  70 +}
  71 +
  72 +export function getUserList(params?: UserListQueryReq): Promise<PageVO<UserListItemVO>> {
  73 + return request.get('/usr/users', { params })
  74 +}
... ...
frontend/src/pages/usr/UserListPage.tsx
1   -import { useState } from 'react'
2   -import { Table } from 'antd'
  1 +import { useState, useEffect } from 'react'
  2 +import { Table, Select, Input, Button, Space } from 'antd'
  3 +import type { ColumnsType } from 'antd/es/table'
3 4 import { PermButton } from '../../components/PermButton'
4 5 import UserFormDrawer from './UserFormDrawer'
  6 +import { getUserList } from '../../api/usr'
  7 +import type { PageVO, UserListItemVO } from '../../api/usr'
  8 +
  9 +const QUERY_FIELDS = [
  10 + { value: 'username', label: '用户名' },
  11 + { value: 'staffName', label: '员工名' },
  12 + { value: 'userCode', label: '用户号' },
  13 + { value: 'department', label: '部门' },
  14 + { value: 'userType', label: '用户类型' },
  15 + { value: 'disabled', label: '作废' },
  16 + { value: 'lastLoginDate', label: '登录日期' },
  17 + { value: 'creator', label: '制单人' },
  18 +]
  19 +
  20 +const MATCH_TYPES = [
  21 + { value: 'contains', label: '包含' },
  22 + { value: 'notContains', label: '不包含' },
  23 + { value: 'equals', label: '等于' },
  24 +]
  25 +
  26 +const columns: ColumnsType<UserListItemVO> = [
  27 + { title: '用户名', dataIndex: 'sUsername' },
  28 + { title: '员工名', dataIndex: 'sStaffName' },
  29 + { title: '用户号', dataIndex: 'sUserCode' },
  30 + { title: '部门', dataIndex: 'sDepartment' },
  31 + { title: '用户类型', dataIndex: 'sUserType' },
  32 + { title: '语言', dataIndex: 'sLanguage' },
  33 + { title: '作废', dataIndex: 'bIsDisabled', render: (v: number) => v ? '是' : '否' },
  34 + { title: '登录日期', dataIndex: 'tLastLoginDate' },
  35 + { title: '制单人', dataIndex: 'sCreatorUsername' },
  36 + { title: '制单日期', dataIndex: 'tCreateDate' },
  37 +]
5 38  
6 39 export default function UserListPage() {
7 40 const [drawerOpen, setDrawerOpen] = useState(false)
  41 + const [data, setData] = useState<PageVO<UserListItemVO> | null>(null)
  42 + const [queryField, setQueryField] = useState('username')
  43 + const [matchType, setMatchType] = useState('contains')
  44 + const [queryValue, setQueryValue] = useState('')
  45 + const [currentPage, setCurrentPage] = useState(1)
  46 +
  47 + const load = (pg = 1) => {
  48 + setCurrentPage(pg)
  49 + getUserList({ queryField, matchType, queryValue, page: pg, pageSize: 20 }).then(setData)
  50 + }
  51 +
  52 + useEffect(() => {
  53 + load()
  54 + }, [])
8 55  
9 56 return (
10 57 <div>
11   - <div style={{ marginBottom: 16 }}>
  58 + <Space style={{ marginBottom: 16 }} wrap>
  59 + <Select
  60 + value={queryField}
  61 + onChange={setQueryField}
  62 + options={QUERY_FIELDS}
  63 + style={{ width: 120 }}
  64 + />
  65 + <Select
  66 + value={matchType}
  67 + onChange={setMatchType}
  68 + options={MATCH_TYPES}
  69 + style={{ width: 100 }}
  70 + />
  71 + <Input
  72 + value={queryValue}
  73 + onChange={e => setQueryValue(e.target.value)}
  74 + placeholder="查询值"
  75 + style={{ width: 160 }}
  76 + />
  77 + <Button type="primary" onClick={() => load(1)}>搜索</Button>
12 78 <PermButton
13 79 permission="usr:create"
14 80 type="primary"
... ... @@ -16,12 +82,22 @@ export default function UserListPage() {
16 82 >
17 83 新增
18 84 </PermButton>
19   - </div>
20   - <Table dataSource={[]} columns={[]} rowKey="sId" />
  85 + </Space>
  86 + <Table
  87 + dataSource={data?.list ?? []}
  88 + columns={columns}
  89 + rowKey="sId"
  90 + pagination={{
  91 + total: data?.total ?? 0,
  92 + pageSize: 20,
  93 + current: currentPage,
  94 + onChange: load,
  95 + }}
  96 + />
21 97 <UserFormDrawer
22 98 open={drawerOpen}
23 99 onClose={() => setDrawerOpen(false)}
24   - onSuccess={() => setDrawerOpen(false)}
  100 + onSuccess={() => { setDrawerOpen(false); load(1) }}
25 101 />
26 102 </div>
27 103 )
... ...
frontend/src/test/UserListPage.test.tsx
... ... @@ -10,7 +10,8 @@ import UserListPage from &#39;../pages/usr/UserListPage&#39;
10 10 vi.mock('../api/usr', () => ({
11 11 getStaffs: vi.fn().mockResolvedValue([]),
12 12 getPermissionGroups: vi.fn().mockResolvedValue([]),
13   - createUser: vi.fn()
  13 + createUser: vi.fn(),
  14 + getUserList: vi.fn().mockResolvedValue({ total: 0, page: 1, pageSize: 20, list: [] })
14 15 }))
15 16  
16 17 vi.mock('../api/request', () => ({
... ... @@ -61,4 +62,27 @@ describe(&#39;UserListPage&#39;, () =&gt; {
61 62 await userEvent.click(screen.getByRole('button', { name: /新\s*增/ }))
62 63 await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument())
63 64 })
  65 +
  66 + it('initialLoad_rendersTableRows', async () => {
  67 + const { getUserList } = await import('../api/usr')
  68 + vi.mocked(getUserList).mockResolvedValueOnce({
  69 + total: 1, page: 1, pageSize: 20,
  70 + list: [{
  71 + sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户',
  72 + sLanguage: '中文', bIsDisabled: 0, tLastLoginDate: null,
  73 + sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00',
  74 + sStaffName: '张三', sDepartment: '研发部'
  75 + }]
  76 + })
  77 + renderPage('超级管理员')
  78 + await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument())
  79 + expect(screen.getByText('张三')).toBeInTheDocument()
  80 + })
  81 +
  82 + it('searchButton_callsGetUserList', async () => {
  83 + const { getUserList } = await import('../api/usr')
  84 + renderPage('超级管理员')
  85 + await userEvent.click(screen.getByRole('button', { name: /搜\s*索|搜索/ }))
  86 + await waitFor(() => expect(vi.mocked(getUserList)).toHaveBeenCalledTimes(2))
  87 + })
64 88 })
... ...