Commit 704d75a49e0bca7a6ab09986c0e7b0d3d54cbee4
1 parent
8f600c60
feat(usr): 前端编辑用户功能(API+抽屉+列表操作列)REQ-USR-002
- usr.ts 追加 UserUpdateReq/Resp + updateUser() - UserFormDrawer 支持 userId/initialData 编辑模式 - UserListPage 追加操作列 + editingUser 状态 - 测试覆盖编辑抽屉开启与 updateUser 调用
Showing
4 changed files
with
147 additions
and
33 deletions
frontend/src/api/usr.ts
| @@ -72,3 +72,22 @@ export interface PageVO<T> { | @@ -72,3 +72,22 @@ export interface PageVO<T> { | ||
| 72 | export function getUserList(params?: UserListQueryReq): Promise<PageVO<UserListItemVO>> { | 72 | export function getUserList(params?: UserListQueryReq): Promise<PageVO<UserListItemVO>> { |
| 73 | return request.get('/usr/users', { params }) | 73 | return request.get('/usr/users', { params }) |
| 74 | } | 74 | } |
| 75 | + | ||
| 76 | +export interface UserUpdateReq { | ||
| 77 | + userType: string | ||
| 78 | + language: string | ||
| 79 | + canEditDoc: boolean | ||
| 80 | + isDisabled: boolean | ||
| 81 | + employeeId: string | null | ||
| 82 | + permGroupIds: string[] | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +export interface UserUpdateResp { | ||
| 86 | + userId: string | ||
| 87 | + username: string | ||
| 88 | + updatedAt: string | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +export function updateUser(userId: string, req: UserUpdateReq): Promise<UserUpdateResp> { | ||
| 92 | + return request.put(`/usr/users/${userId}`, req) | ||
| 93 | +} |
frontend/src/pages/usr/UserFormDrawer.tsx
| @@ -3,25 +3,50 @@ import { | @@ -3,25 +3,50 @@ import { | ||
| 3 | Drawer, Form, Input, Select, Checkbox, Button, message, Table | 3 | Drawer, Form, Input, Select, Checkbox, Button, message, Table |
| 4 | } from 'antd' | 4 | } from 'antd' |
| 5 | import type { ColumnsType } from 'antd/es/table' | 5 | import type { ColumnsType } from 'antd/es/table' |
| 6 | -import { getStaffs, getPermissionGroups, createUser, StaffVO, PermissionGroupVO, UserCreateReq } from '../../api/usr' | 6 | +import { getStaffs, getPermissionGroups, createUser, updateUser, StaffVO, PermissionGroupVO, UserCreateReq, UserUpdateReq } from '../../api/usr' |
| 7 | + | ||
| 8 | +interface InitialData { | ||
| 9 | + userType: string | ||
| 10 | + language: string | ||
| 11 | + canEditDoc: boolean | ||
| 12 | + isDisabled: boolean | ||
| 13 | + employeeId?: string | null | ||
| 14 | +} | ||
| 7 | 15 | ||
| 8 | interface Props { | 16 | interface Props { |
| 9 | open: boolean | 17 | open: boolean |
| 10 | onClose: () => void | 18 | onClose: () => void |
| 11 | onSuccess: () => void | 19 | onSuccess: () => void |
| 20 | + userId?: string | ||
| 21 | + initialData?: InitialData | ||
| 12 | } | 22 | } |
| 13 | 23 | ||
| 14 | -export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | 24 | +export default function UserFormDrawer({ open, onClose, onSuccess, userId, initialData }: Props) { |
| 15 | const [form] = Form.useForm() | 25 | const [form] = Form.useForm() |
| 16 | const [staffs, setStaffs] = useState<StaffVO[]>([]) | 26 | const [staffs, setStaffs] = useState<StaffVO[]>([]) |
| 17 | const [permGroups, setPermGroups] = useState<PermissionGroupVO[]>([]) | 27 | const [permGroups, setPermGroups] = useState<PermissionGroupVO[]>([]) |
| 18 | const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]) | 28 | const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]) |
| 19 | const [submitting, setSubmitting] = useState(false) | 29 | const [submitting, setSubmitting] = useState(false) |
| 20 | 30 | ||
| 31 | + const isEditMode = !!userId | ||
| 32 | + | ||
| 21 | useEffect(() => { | 33 | useEffect(() => { |
| 22 | if (open) { | 34 | if (open) { |
| 23 | getStaffs().then(setStaffs).catch(() => {}) | 35 | getStaffs().then(setStaffs).catch(() => {}) |
| 24 | getPermissionGroups().then(setPermGroups).catch(() => {}) | 36 | getPermissionGroups().then(setPermGroups).catch(() => {}) |
| 37 | + if (isEditMode && initialData) { | ||
| 38 | + form.setFieldsValue({ | ||
| 39 | + userType: initialData.userType, | ||
| 40 | + language: initialData.language, | ||
| 41 | + canEditDoc: initialData.canEditDoc, | ||
| 42 | + isDisabled: initialData.isDisabled, | ||
| 43 | + employeeId: initialData.employeeId ?? null, | ||
| 44 | + }) | ||
| 45 | + setSelectedPermIds([]) | ||
| 46 | + } else { | ||
| 47 | + form.resetFields() | ||
| 48 | + setSelectedPermIds([]) | ||
| 49 | + } | ||
| 25 | } | 50 | } |
| 26 | }, [open]) | 51 | }, [open]) |
| 27 | 52 | ||
| @@ -49,17 +74,30 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | @@ -49,17 +74,30 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | ||
| 49 | try { | 74 | try { |
| 50 | const values = await form.validateFields() | 75 | const values = await form.validateFields() |
| 51 | setSubmitting(true) | 76 | setSubmitting(true) |
| 52 | - const req: UserCreateReq = { | ||
| 53 | - userCode: values.userCode, | ||
| 54 | - username: values.username, | ||
| 55 | - userType: values.userType, | ||
| 56 | - language: values.language, | ||
| 57 | - canEditDoc: values.canEditDoc ?? false, | ||
| 58 | - employeeId: values.employeeId ?? null, | ||
| 59 | - permGroupIds: selectedPermIds | 77 | + if (isEditMode) { |
| 78 | + const req: UserUpdateReq = { | ||
| 79 | + userType: values.userType, | ||
| 80 | + language: values.language, | ||
| 81 | + canEditDoc: values.canEditDoc ?? false, | ||
| 82 | + isDisabled: values.isDisabled ?? false, | ||
| 83 | + employeeId: values.employeeId ?? null, | ||
| 84 | + permGroupIds: selectedPermIds | ||
| 85 | + } | ||
| 86 | + await updateUser(userId, req) | ||
| 87 | + message.success('修改用户成功') | ||
| 88 | + } else { | ||
| 89 | + const req: UserCreateReq = { | ||
| 90 | + userCode: values.userCode, | ||
| 91 | + username: values.username, | ||
| 92 | + userType: values.userType, | ||
| 93 | + language: values.language, | ||
| 94 | + canEditDoc: values.canEditDoc ?? false, | ||
| 95 | + employeeId: values.employeeId ?? null, | ||
| 96 | + permGroupIds: selectedPermIds | ||
| 97 | + } | ||
| 98 | + await createUser(req) | ||
| 99 | + message.success('新增用户成功') | ||
| 60 | } | 100 | } |
| 61 | - await createUser(req) | ||
| 62 | - message.success('新增用户成功') | ||
| 63 | form.resetFields() | 101 | form.resetFields() |
| 64 | setSelectedPermIds([]) | 102 | setSelectedPermIds([]) |
| 65 | onSuccess() | 103 | onSuccess() |
| @@ -74,7 +112,7 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | @@ -74,7 +112,7 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | ||
| 74 | 112 | ||
| 75 | return ( | 113 | return ( |
| 76 | <Drawer | 114 | <Drawer |
| 77 | - title="新增用户" | 115 | + title={isEditMode ? '修改用户' : '新增用户'} |
| 78 | open={open} | 116 | open={open} |
| 79 | onClose={onClose} | 117 | onClose={onClose} |
| 80 | width={520} | 118 | width={520} |
| @@ -86,12 +124,16 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | @@ -86,12 +124,16 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | ||
| 86 | } | 124 | } |
| 87 | > | 125 | > |
| 88 | <Form form={form} layout="vertical"> | 126 | <Form form={form} layout="vertical"> |
| 89 | - <Form.Item name="userCode" label="用户号" rules={[{ required: true, message: '请输入用户号' }]}> | ||
| 90 | - <Input placeholder="请输入用户号" /> | ||
| 91 | - </Form.Item> | ||
| 92 | - <Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}> | ||
| 93 | - <Input placeholder="请输入用户名" /> | ||
| 94 | - </Form.Item> | 127 | + {!isEditMode && ( |
| 128 | + <> | ||
| 129 | + <Form.Item name="userCode" label="用户号" rules={[{ required: true, message: '请输入用户号' }]}> | ||
| 130 | + <Input placeholder="请输入用户号" /> | ||
| 131 | + </Form.Item> | ||
| 132 | + <Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}> | ||
| 133 | + <Input placeholder="请输入用户名" /> | ||
| 134 | + </Form.Item> | ||
| 135 | + </> | ||
| 136 | + )} | ||
| 95 | <Form.Item name="userType" label="用户类型" rules={[{ required: true }]} initialValue="普通用户"> | 137 | <Form.Item name="userType" label="用户类型" rules={[{ required: true }]} initialValue="普通用户"> |
| 96 | <Select options={[{ value: '普通用户', label: '普通用户' }, { value: '超级管理员', label: '超级管理员' }]} /> | 138 | <Select options={[{ value: '普通用户', label: '普通用户' }, { value: '超级管理员', label: '超级管理员' }]} /> |
| 97 | </Form.Item> | 139 | </Form.Item> |
| @@ -101,6 +143,11 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | @@ -101,6 +143,11 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { | ||
| 101 | <Form.Item name="canEditDoc" valuePropName="checked"> | 143 | <Form.Item name="canEditDoc" valuePropName="checked"> |
| 102 | <Checkbox>可编辑文档</Checkbox> | 144 | <Checkbox>可编辑文档</Checkbox> |
| 103 | </Form.Item> | 145 | </Form.Item> |
| 146 | + {isEditMode && ( | ||
| 147 | + <Form.Item name="isDisabled" valuePropName="checked"> | ||
| 148 | + <Checkbox>作废</Checkbox> | ||
| 149 | + </Form.Item> | ||
| 150 | + )} | ||
| 104 | <Form.Item name="employeeId" label="关联员工"> | 151 | <Form.Item name="employeeId" label="关联员工"> |
| 105 | <Select | 152 | <Select |
| 106 | allowClear | 153 | allowClear |
frontend/src/pages/usr/UserListPage.tsx
| @@ -23,22 +23,11 @@ const MATCH_TYPES = [ | @@ -23,22 +23,11 @@ const MATCH_TYPES = [ | ||
| 23 | { value: 'equals', label: '等于' }, | 23 | { value: 'equals', label: '等于' }, |
| 24 | ] | 24 | ] |
| 25 | 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 | -] | ||
| 38 | - | ||
| 39 | // REQ-USR-003: 查询用户 | 26 | // REQ-USR-003: 查询用户 |
| 27 | +// REQ-USR-002: 修改用户 | ||
| 40 | export default function UserListPage() { | 28 | export default function UserListPage() { |
| 41 | const [drawerOpen, setDrawerOpen] = useState(false) | 29 | const [drawerOpen, setDrawerOpen] = useState(false) |
| 30 | + const [editingUser, setEditingUser] = useState<UserListItemVO | null>(null) | ||
| 42 | const [data, setData] = useState<PageVO<UserListItemVO> | null>(null) | 31 | const [data, setData] = useState<PageVO<UserListItemVO> | null>(null) |
| 43 | const [queryField, setQueryField] = useState('username') | 32 | const [queryField, setQueryField] = useState('username') |
| 44 | const [matchType, setMatchType] = useState('contains') | 33 | const [matchType, setMatchType] = useState('contains') |
| @@ -54,6 +43,32 @@ export default function UserListPage() { | @@ -54,6 +43,32 @@ export default function UserListPage() { | ||
| 54 | load() | 43 | load() |
| 55 | }, []) | 44 | }, []) |
| 56 | 45 | ||
| 46 | + const columns: ColumnsType<UserListItemVO> = [ | ||
| 47 | + { title: '用户名', dataIndex: 'sUsername' }, | ||
| 48 | + { title: '员工名', dataIndex: 'sStaffName' }, | ||
| 49 | + { title: '用户号', dataIndex: 'sUserCode' }, | ||
| 50 | + { title: '部门', dataIndex: 'sDepartment' }, | ||
| 51 | + { title: '用户类型', dataIndex: 'sUserType' }, | ||
| 52 | + { title: '语言', dataIndex: 'sLanguage' }, | ||
| 53 | + { title: '作废', dataIndex: 'bIsDisabled', render: (v: number) => v ? '是' : '否' }, | ||
| 54 | + { title: '登录日期', dataIndex: 'tLastLoginDate' }, | ||
| 55 | + { title: '制单人', dataIndex: 'sCreatorUsername' }, | ||
| 56 | + { title: '制单日期', dataIndex: 'tCreateDate' }, | ||
| 57 | + { | ||
| 58 | + title: '操作', | ||
| 59 | + key: 'action', | ||
| 60 | + render: (_, record) => ( | ||
| 61 | + <PermButton | ||
| 62 | + permission="usr:edit" | ||
| 63 | + type="link" | ||
| 64 | + onClick={() => setEditingUser(record)} | ||
| 65 | + > | ||
| 66 | + 修改 | ||
| 67 | + </PermButton> | ||
| 68 | + ) | ||
| 69 | + }, | ||
| 70 | + ] | ||
| 71 | + | ||
| 57 | return ( | 72 | return ( |
| 58 | <div> | 73 | <div> |
| 59 | <Space style={{ marginBottom: 16 }} wrap> | 74 | <Space style={{ marginBottom: 16 }} wrap> |
| @@ -100,6 +115,19 @@ export default function UserListPage() { | @@ -100,6 +115,19 @@ export default function UserListPage() { | ||
| 100 | onClose={() => setDrawerOpen(false)} | 115 | onClose={() => setDrawerOpen(false)} |
| 101 | onSuccess={() => { setDrawerOpen(false); load(1) }} | 116 | onSuccess={() => { setDrawerOpen(false); load(1) }} |
| 102 | /> | 117 | /> |
| 118 | + <UserFormDrawer | ||
| 119 | + open={editingUser !== null} | ||
| 120 | + userId={editingUser?.sId} | ||
| 121 | + initialData={editingUser ? { | ||
| 122 | + userType: editingUser.sUserType, | ||
| 123 | + language: editingUser.sLanguage, | ||
| 124 | + canEditDoc: false, | ||
| 125 | + isDisabled: editingUser.bIsDisabled === 1, | ||
| 126 | + employeeId: null, | ||
| 127 | + } : undefined} | ||
| 128 | + onClose={() => setEditingUser(null)} | ||
| 129 | + onSuccess={() => { setEditingUser(null); load(1) }} | ||
| 130 | + /> | ||
| 103 | </div> | 131 | </div> |
| 104 | ) | 132 | ) |
| 105 | } | 133 | } |
frontend/src/test/UserListPage.test.tsx
| @@ -11,7 +11,8 @@ vi.mock('../api/usr', () => ({ | @@ -11,7 +11,8 @@ 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 | + getUserList: vi.fn().mockResolvedValue({ total: 0, page: 1, pageSize: 20, list: [] }), |
| 15 | + updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' }) | ||
| 15 | })) | 16 | })) |
| 16 | 17 | ||
| 17 | vi.mock('../api/request', () => ({ | 18 | vi.mock('../api/request', () => ({ |
| @@ -85,4 +86,23 @@ describe('UserListPage', () => { | @@ -85,4 +86,23 @@ describe('UserListPage', () => { | ||
| 85 | await userEvent.click(screen.getByRole('button', { name: /搜\s*索|搜索/ })) | 86 | await userEvent.click(screen.getByRole('button', { name: /搜\s*索|搜索/ })) |
| 86 | await waitFor(() => expect(vi.mocked(getUserList)).toHaveBeenCalledTimes(2)) | 87 | await waitFor(() => expect(vi.mocked(getUserList)).toHaveBeenCalledTimes(2)) |
| 87 | }) | 88 | }) |
| 89 | + | ||
| 90 | + it('editMode_submit_callsUpdateUser', async () => { | ||
| 91 | + const { getUserList, updateUser } = await import('../api/usr') | ||
| 92 | + vi.mocked(getUserList).mockResolvedValue({ | ||
| 93 | + total: 1, page: 1, pageSize: 20, | ||
| 94 | + list: [{ | ||
| 95 | + sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户', | ||
| 96 | + sLanguage: '中文', bIsDisabled: 0, tLastLoginDate: null, | ||
| 97 | + sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00', | ||
| 98 | + sStaffName: null, sDepartment: null | ||
| 99 | + }] | ||
| 100 | + }) | ||
| 101 | + renderPage('超级管理员') | ||
| 102 | + await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()) | ||
| 103 | + await userEvent.click(screen.getByRole('button', { name: /修改/ })) | ||
| 104 | + await waitFor(() => expect(screen.getByText('修改用户')).toBeInTheDocument()) | ||
| 105 | + await userEvent.click(screen.getByRole('button', { name: /确\s*认/ })) | ||
| 106 | + await waitFor(() => expect(vi.mocked(updateUser)).toHaveBeenCalledTimes(1)) | ||
| 107 | + }) | ||
| 88 | }) | 108 | }) |