Commit 9c0ba386f5f9279c8cfc2eadb1f13e4bb2baae4b
1 parent
1bee98b1
fix(frontend): 修复 review round 1 must-fix
- HIGH: UserFormPage edit 模式 prefill canEditDocument from backend(之前硬编码 false 会静默覆盖) - MED: UsersTable 加 a11y 键盘可达(tabIndex/role=button/onKeyDown)+ 操作列编辑链接 - MED: UsersTable 列头加 sorter (username/userCode/lastLoginDate/createdDate) - MED: UserFormPage employeeId 三态映射(toCreate/toUpdate helpers,spec § 八) - MED: api/users.ts UpdateUserReq/CreateUserReq.employeeId 类型补 | null - LOW: UserPermissionPanel 补缺 2 个 disabled Tab (process/driver) 与 prototype 对齐 - LOW: UsersListPage 默认 sortField/sortOrder 显式发给后端 - LOW: usersConstants QUERY_FIELD_OPTIONS 补 lastLoginDate - TEST: 补 canEditDocument prefill 测试用例 + msw handler 返回该字段 REQ_ID: FE-02
Showing
8 changed files
with
64 additions
and
11 deletions
frontend/src/api/users.ts
| ... | ... | @@ -15,6 +15,7 @@ export interface UserListItem { |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | 17 | export interface UserDetail extends UserListItem { |
| 18 | + canEditDocument?: boolean; | |
| 18 | 19 | employeeId?: number | null; |
| 19 | 20 | permissionCategoryIds: number[]; |
| 20 | 21 | updatedBy?: string | null; |
| ... | ... | @@ -46,7 +47,7 @@ export interface CreateUserReq { |
| 46 | 47 | userType: 'NORMAL' | 'SUPER_ADMIN'; |
| 47 | 48 | language: 'zh-CN' | 'en-US' | 'zh-TW'; |
| 48 | 49 | canEditDocument: boolean; |
| 49 | - employeeId?: number; | |
| 50 | + employeeId?: number | null; | |
| 50 | 51 | permissionCategoryIds?: number[]; |
| 51 | 52 | } |
| 52 | 53 | |
| ... | ... | @@ -55,7 +56,7 @@ export interface UpdateUserReq { |
| 55 | 56 | userType?: 'NORMAL' | 'SUPER_ADMIN'; |
| 56 | 57 | language?: 'zh-CN' | 'en-US' | 'zh-TW'; |
| 57 | 58 | canEditDocument?: boolean; |
| 58 | - employeeId?: number; | |
| 59 | + employeeId?: number | null; | |
| 59 | 60 | isDeleted?: boolean; |
| 60 | 61 | permissionCategoryIds?: number[]; |
| 61 | 62 | } | ... | ... |
frontend/src/pages/users/UserFormPage.test.tsx
| ... | ... | @@ -84,6 +84,15 @@ describe('UserFormPage (edit)', () => { |
| 84 | 84 | }); |
| 85 | 85 | }); |
| 86 | 86 | |
| 87 | + it('prefills canEditDocument from backend (not hardcoded false)', async () => { | |
| 88 | + renderForm('edit', '/users/1'); | |
| 89 | + // backend mock 返回 canEditDocument=true;UI 应反映为 checkbox.checked=true | |
| 90 | + await waitFor(() => { | |
| 91 | + const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement; | |
| 92 | + expect(checkbox?.checked).toBe(true); | |
| 93 | + }); | |
| 94 | + }); | |
| 95 | + | |
| 87 | 96 | it('unknown userId (40401) shows 404 result', async () => { |
| 88 | 97 | renderForm('edit', '/users/99999'); |
| 89 | 98 | await waitFor(() => expect(screen.getByTestId('user-not-found')).toBeInTheDocument()); | ... | ... |
frontend/src/pages/users/UserFormPage.tsx
| ... | ... | @@ -47,7 +47,7 @@ export default function UserFormPage({ mode }: Props) { |
| 47 | 47 | userCode: detail.userCode, |
| 48 | 48 | userType: detail.userType, |
| 49 | 49 | language: detail.language as FormValues['language'], |
| 50 | - canEditDocument: false, // detail VO 当前不返回,默认 false | |
| 50 | + canEditDocument: detail.canEditDocument ?? false, | |
| 51 | 51 | employeeId: detail.employeeId ?? undefined, |
| 52 | 52 | }); |
| 53 | 53 | setPermissionCategoryIds(detail.permissionCategoryIds ?? []); |
| ... | ... | @@ -69,6 +69,13 @@ export default function UserFormPage({ mode }: Props) { |
| 69 | 69 | }; |
| 70 | 70 | }, [mode, userId, form]); |
| 71 | 71 | |
| 72 | + // spec § 八三态 employeeId 映射: | |
| 73 | + // - 新增模式:values.employeeId === undefined 或 0 → 不传 / 显式 null;正整数 → 传 ID | |
| 74 | + // - 编辑模式:undefined → 不变(PATCH 缺省 = 保留原值);0 → 显式发 0(解除关联约定);正整数 → 传 ID | |
| 75 | + const toCreateEmployeeId = (v: number | undefined): number | null | undefined => | |
| 76 | + v == null || v === 0 ? null : v; | |
| 77 | + const toUpdateEmployeeId = (v: number | undefined): number | undefined => v; | |
| 78 | + | |
| 72 | 79 | const handleSubmit = async (values: FormValues) => { |
| 73 | 80 | setSubmitting(true); |
| 74 | 81 | setErrorMessage(null); |
| ... | ... | @@ -85,7 +92,7 @@ export default function UserFormPage({ mode }: Props) { |
| 85 | 92 | userType: values.userType!, |
| 86 | 93 | language: values.language!, |
| 87 | 94 | canEditDocument: !!values.canEditDocument, |
| 88 | - employeeId: values.employeeId, | |
| 95 | + employeeId: toCreateEmployeeId(values.employeeId), | |
| 89 | 96 | permissionCategoryIds, |
| 90 | 97 | }); |
| 91 | 98 | message.success('新增用户成功'); |
| ... | ... | @@ -95,7 +102,7 @@ export default function UserFormPage({ mode }: Props) { |
| 95 | 102 | userType: values.userType, |
| 96 | 103 | language: values.language, |
| 97 | 104 | canEditDocument: values.canEditDocument, |
| 98 | - employeeId: values.employeeId, | |
| 105 | + employeeId: toUpdateEmployeeId(values.employeeId), | |
| 99 | 106 | permissionCategoryIds, |
| 100 | 107 | }; |
| 101 | 108 | await usersApi.update(userId, patch); | ... | ... |
frontend/src/pages/users/UserPermissionPanel.tsx
| ... | ... | @@ -28,6 +28,8 @@ export default function UserPermissionPanel({ value, onChange, disabled = false |
| 28 | 28 | { key: 'customer', label: '客户查看权限', disabled: true, children: null }, |
| 29 | 29 | { key: 'supplier', label: '供应商查看权限', disabled: true, children: null }, |
| 30 | 30 | { key: 'person', label: '人员查看权限', disabled: true, children: null }, |
| 31 | + { key: 'process', label: '工序查看权限', disabled: true, children: null }, | |
| 32 | + { key: 'driver', label: '司机查看权限', disabled: true, children: null }, | |
| 31 | 33 | ]} |
| 32 | 34 | /> |
| 33 | 35 | </div> | ... | ... |
frontend/src/pages/users/UsersListPage.tsx
| ... | ... | @@ -12,7 +12,12 @@ import UsersTable from './UsersTable'; |
| 12 | 12 | |
| 13 | 13 | export default function UsersListPage() { |
| 14 | 14 | const navigate = useNavigate(); |
| 15 | - const [query, setQuery] = useState<UsersListQuery>({ page: 1, size: 20 }); | |
| 15 | + const [query, setQuery] = useState<UsersListQuery>({ | |
| 16 | + page: 1, | |
| 17 | + size: 20, | |
| 18 | + sortField: 'tCreateDate', | |
| 19 | + sortOrder: 'desc', | |
| 20 | + }); | |
| 16 | 21 | const [records, setRecords] = useState<UserListItem[]>([]); |
| 17 | 22 | const [total, setTotal] = useState(0); |
| 18 | 23 | const [loading, setLoading] = useState(false); | ... | ... |
frontend/src/pages/users/UsersTable.tsx
| 1 | -import { Table, Tag } from 'antd'; | |
| 1 | +import { Table, Tag, Button } from 'antd'; | |
| 2 | 2 | import type { ColumnsType } from 'antd/es/table'; |
| 3 | 3 | import type { UserListItem } from '../../api/users'; |
| 4 | 4 | |
| ... | ... | @@ -28,9 +28,9 @@ export default function UsersTable({ |
| 28 | 28 | width: 60, |
| 29 | 29 | render: (_: unknown, __: unknown, idx: number) => (page - 1) * size + idx + 1, |
| 30 | 30 | }, |
| 31 | - { title: '用户名', dataIndex: 'username', key: 'username' }, | |
| 31 | + { title: '用户名', dataIndex: 'username', key: 'username', sorter: true }, | |
| 32 | 32 | { title: '员工名', dataIndex: 'employeeName', key: 'employeeName' }, |
| 33 | - { title: '用户号', dataIndex: 'userCode', key: 'userCode' }, | |
| 33 | + { title: '用户号', dataIndex: 'userCode', key: 'userCode', sorter: true }, | |
| 34 | 34 | { title: '部门', dataIndex: 'departmentName', key: 'departmentName' }, |
| 35 | 35 | { |
| 36 | 36 | title: '用户类型', |
| ... | ... | @@ -46,9 +46,27 @@ export default function UsersTable({ |
| 46 | 46 | render: (v: boolean) => |
| 47 | 47 | v ? <Tag color="error">作废</Tag> : <Tag color="success">启用</Tag>, |
| 48 | 48 | }, |
| 49 | - { title: '登录日期', dataIndex: 'lastLoginDate', key: 'lastLoginDate' }, | |
| 49 | + { title: '登录日期', dataIndex: 'lastLoginDate', key: 'lastLoginDate', sorter: true }, | |
| 50 | 50 | { title: '制单人', dataIndex: 'createdBy', key: 'createdBy' }, |
| 51 | - { title: '制单日期', dataIndex: 'createdDate', key: 'createdDate' }, | |
| 51 | + { title: '制单日期', dataIndex: 'createdDate', key: 'createdDate', sorter: true }, | |
| 52 | + { | |
| 53 | + title: '操作', | |
| 54 | + key: 'action', | |
| 55 | + width: 80, | |
| 56 | + render: (_: unknown, row: UserListItem) => ( | |
| 57 | + <Button | |
| 58 | + type="link" | |
| 59 | + size="small" | |
| 60 | + data-testid={`edit-link-${row.userId}`} | |
| 61 | + onClick={(e) => { | |
| 62 | + e.stopPropagation(); | |
| 63 | + onRowClick(row); | |
| 64 | + }} | |
| 65 | + > | |
| 66 | + 编辑 | |
| 67 | + </Button> | |
| 68 | + ), | |
| 69 | + }, | |
| 52 | 70 | ]; |
| 53 | 71 | |
| 54 | 72 | return ( |
| ... | ... | @@ -59,7 +77,16 @@ export default function UsersTable({ |
| 59 | 77 | loading={loading} |
| 60 | 78 | onRow={(row) => ({ |
| 61 | 79 | onClick: () => onRowClick(row), |
| 80 | + onKeyDown: (e: React.KeyboardEvent) => { | |
| 81 | + if (e.key === 'Enter' || e.key === ' ') { | |
| 82 | + e.preventDefault(); | |
| 83 | + onRowClick(row); | |
| 84 | + } | |
| 85 | + }, | |
| 62 | 86 | style: { cursor: 'pointer' }, |
| 87 | + tabIndex: 0, | |
| 88 | + role: 'button', | |
| 89 | + 'aria-label': `查看用户 ${row.username}`, | |
| 63 | 90 | 'data-testid': `user-row-${row.userId}`, |
| 64 | 91 | })} |
| 65 | 92 | pagination={{ | ... | ... |
frontend/src/pages/users/usersConstants.ts
| ... | ... | @@ -16,6 +16,7 @@ export const QUERY_FIELD_OPTIONS = [ |
| 16 | 16 | { value: 'departmentName', label: '部门' }, |
| 17 | 17 | { value: 'userType', label: '用户类型' }, |
| 18 | 18 | { value: 'isDeleted', label: '作废' }, |
| 19 | + { value: 'lastLoginDate', label: '登录日期' }, | |
| 19 | 20 | { value: 'createdBy', label: '制单人' }, |
| 20 | 21 | ] as const; |
| 21 | 22 | ... | ... |
frontend/src/test-utils/msw-handlers.ts