Commit 9c0ba386f5f9279c8cfc2eadb1f13e4bb2baae4b

Authored by zichun
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
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 &#39;./UsersTable&#39;;
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
... ... @@ -148,6 +148,7 @@ export const handlers = [
148 148 language: 'zh-CN',
149 149 isDeleted: false,
150 150 lastLoginDate: '2026-05-15T08:00:00',
  151 + canEditDocument: true,
151 152 employeeId: 1,
152 153 permissionCategoryIds: [1, 2],
153 154 createdBy: 'admin',
... ...