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 | 72 | export function getUserList(params?: UserListQueryReq): Promise<PageVO<UserListItemVO>> { |
| 73 | 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 | 3 | Drawer, Form, Input, Select, Checkbox, Button, message, Table |
| 4 | 4 | } from 'antd' |
| 5 | 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 | 16 | interface Props { |
| 9 | 17 | open: boolean |
| 10 | 18 | onClose: () => void |
| 11 | 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 | 25 | const [form] = Form.useForm() |
| 16 | 26 | const [staffs, setStaffs] = useState<StaffVO[]>([]) |
| 17 | 27 | const [permGroups, setPermGroups] = useState<PermissionGroupVO[]>([]) |
| 18 | 28 | const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]) |
| 19 | 29 | const [submitting, setSubmitting] = useState(false) |
| 20 | 30 | |
| 31 | + const isEditMode = !!userId | |
| 32 | + | |
| 21 | 33 | useEffect(() => { |
| 22 | 34 | if (open) { |
| 23 | 35 | getStaffs().then(setStaffs).catch(() => {}) |
| 24 | 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 | 51 | }, [open]) |
| 27 | 52 | |
| ... | ... | @@ -49,17 +74,30 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { |
| 49 | 74 | try { |
| 50 | 75 | const values = await form.validateFields() |
| 51 | 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 | 101 | form.resetFields() |
| 64 | 102 | setSelectedPermIds([]) |
| 65 | 103 | onSuccess() |
| ... | ... | @@ -74,7 +112,7 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { |
| 74 | 112 | |
| 75 | 113 | return ( |
| 76 | 114 | <Drawer |
| 77 | - title="新增用户" | |
| 115 | + title={isEditMode ? '修改用户' : '新增用户'} | |
| 78 | 116 | open={open} |
| 79 | 117 | onClose={onClose} |
| 80 | 118 | width={520} |
| ... | ... | @@ -86,12 +124,16 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { |
| 86 | 124 | } |
| 87 | 125 | > |
| 88 | 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 | 137 | <Form.Item name="userType" label="用户类型" rules={[{ required: true }]} initialValue="普通用户"> |
| 96 | 138 | <Select options={[{ value: '普通用户', label: '普通用户' }, { value: '超级管理员', label: '超级管理员' }]} /> |
| 97 | 139 | </Form.Item> |
| ... | ... | @@ -101,6 +143,11 @@ export default function UserFormDrawer({ open, onClose, onSuccess }: Props) { |
| 101 | 143 | <Form.Item name="canEditDoc" valuePropName="checked"> |
| 102 | 144 | <Checkbox>可编辑文档</Checkbox> |
| 103 | 145 | </Form.Item> |
| 146 | + {isEditMode && ( | |
| 147 | + <Form.Item name="isDisabled" valuePropName="checked"> | |
| 148 | + <Checkbox>作废</Checkbox> | |
| 149 | + </Form.Item> | |
| 150 | + )} | |
| 104 | 151 | <Form.Item name="employeeId" label="关联员工"> |
| 105 | 152 | <Select |
| 106 | 153 | allowClear | ... | ... |
frontend/src/pages/usr/UserListPage.tsx
| ... | ... | @@ -23,22 +23,11 @@ const MATCH_TYPES = [ |
| 23 | 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 | 26 | // REQ-USR-003: 查询用户 |
| 27 | +// REQ-USR-002: 修改用户 | |
| 40 | 28 | export default function UserListPage() { |
| 41 | 29 | const [drawerOpen, setDrawerOpen] = useState(false) |
| 30 | + const [editingUser, setEditingUser] = useState<UserListItemVO | null>(null) | |
| 42 | 31 | const [data, setData] = useState<PageVO<UserListItemVO> | null>(null) |
| 43 | 32 | const [queryField, setQueryField] = useState('username') |
| 44 | 33 | const [matchType, setMatchType] = useState('contains') |
| ... | ... | @@ -54,6 +43,32 @@ export default function UserListPage() { |
| 54 | 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 | 72 | return ( |
| 58 | 73 | <div> |
| 59 | 74 | <Space style={{ marginBottom: 16 }} wrap> |
| ... | ... | @@ -100,6 +115,19 @@ export default function UserListPage() { |
| 100 | 115 | onClose={() => setDrawerOpen(false)} |
| 101 | 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 | 131 | </div> |
| 104 | 132 | ) |
| 105 | 133 | } | ... | ... |
frontend/src/test/UserListPage.test.tsx
| ... | ... | @@ -11,7 +11,8 @@ vi.mock('../api/usr', () => ({ |
| 11 | 11 | getStaffs: vi.fn().mockResolvedValue([]), |
| 12 | 12 | getPermissionGroups: vi.fn().mockResolvedValue([]), |
| 13 | 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 | 18 | vi.mock('../api/request', () => ({ |
| ... | ... | @@ -85,4 +86,23 @@ describe('UserListPage', () => { |
| 85 | 86 | await userEvent.click(screen.getByRole('button', { name: /搜\s*索|搜索/ })) |
| 86 | 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 | }) | ... | ... |