Commit 85eeceb5c56ca2bfacb6012f876fcc26ebc68023
1 parent
93a18967
feat(usr): 用户列表搜索表单 + 分页表格 REQ-USR-003
Showing
3 changed files
with
140 additions
and
7 deletions
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 '../pages/usr/UserListPage' |
| 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('UserListPage', () => { |
| 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 | }) | ... | ... |