Commit 704d75a49e0bca7a6ab09986c0e7b0d3d54cbee4

Authored by zichun
1 parent 8f600c60

feat(usr): 前端编辑用户功能(API+抽屉+列表操作列)REQ-USR-002

- usr.ts 追加 UserUpdateReq/Resp + updateUser()
- UserFormDrawer 支持 userId/initialData 编辑模式
- UserListPage 追加操作列 + editingUser 状态
- 测试覆盖编辑抽屉开启与 updateUser 调用
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(&#39;../api/usr&#39;, () =&gt; ({ @@ -11,7 +11,8 @@ vi.mock(&#39;../api/usr&#39;, () =&gt; ({
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(&#39;UserListPage&#39;, () =&gt; { @@ -85,4 +86,23 @@ describe(&#39;UserListPage&#39;, () =&gt; {
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 })