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,3 +39,36 @@ export function getPermissionGroups(): Promise<PermissionGroupVO[]> { | ||
| 39 | export function createUser(req: UserCreateReq): Promise<UserCreateResp> { | 39 | export function createUser(req: UserCreateReq): Promise<UserCreateResp> { |
| 40 | return request.post('/usr/users', req) | 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 | import { PermButton } from '../../components/PermButton' | 4 | import { PermButton } from '../../components/PermButton' |
| 4 | import UserFormDrawer from './UserFormDrawer' | 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 | export default function UserListPage() { | 39 | export default function UserListPage() { |
| 7 | const [drawerOpen, setDrawerOpen] = useState(false) | 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 | return ( | 56 | return ( |
| 10 | <div> | 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 | <PermButton | 78 | <PermButton |
| 13 | permission="usr:create" | 79 | permission="usr:create" |
| 14 | type="primary" | 80 | type="primary" |
| @@ -16,12 +82,22 @@ export default function UserListPage() { | @@ -16,12 +82,22 @@ export default function UserListPage() { | ||
| 16 | > | 82 | > |
| 17 | 新增 | 83 | 新增 |
| 18 | </PermButton> | 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 | <UserFormDrawer | 97 | <UserFormDrawer |
| 22 | open={drawerOpen} | 98 | open={drawerOpen} |
| 23 | onClose={() => setDrawerOpen(false)} | 99 | onClose={() => setDrawerOpen(false)} |
| 24 | - onSuccess={() => setDrawerOpen(false)} | 100 | + onSuccess={() => { setDrawerOpen(false); load(1) }} |
| 25 | /> | 101 | /> |
| 26 | </div> | 102 | </div> |
| 27 | ) | 103 | ) |
frontend/src/test/UserListPage.test.tsx
| @@ -10,7 +10,8 @@ import UserListPage from '../pages/usr/UserListPage' | @@ -10,7 +10,8 @@ import UserListPage from '../pages/usr/UserListPage' | ||
| 10 | vi.mock('../api/usr', () => ({ | 10 | vi.mock('../api/usr', () => ({ |
| 11 | getStaffs: vi.fn().mockResolvedValue([]), | 11 | getStaffs: vi.fn().mockResolvedValue([]), |
| 12 | getPermissionGroups: vi.fn().mockResolvedValue([]), | 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 | vi.mock('../api/request', () => ({ | 17 | vi.mock('../api/request', () => ({ |
| @@ -61,4 +62,27 @@ describe('UserListPage', () => { | @@ -61,4 +62,27 @@ describe('UserListPage', () => { | ||
| 61 | await userEvent.click(screen.getByRole('button', { name: /新\s*增/ })) | 62 | await userEvent.click(screen.getByRole('button', { name: /新\s*增/ })) |
| 62 | await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument()) | 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 | }) |