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,6 +15,7 @@ export interface UserListItem {
15 } 15 }
16 16
17 export interface UserDetail extends UserListItem { 17 export interface UserDetail extends UserListItem {
  18 + canEditDocument?: boolean;
18 employeeId?: number | null; 19 employeeId?: number | null;
19 permissionCategoryIds: number[]; 20 permissionCategoryIds: number[];
20 updatedBy?: string | null; 21 updatedBy?: string | null;
@@ -46,7 +47,7 @@ export interface CreateUserReq { @@ -46,7 +47,7 @@ export interface CreateUserReq {
46 userType: 'NORMAL' | 'SUPER_ADMIN'; 47 userType: 'NORMAL' | 'SUPER_ADMIN';
47 language: 'zh-CN' | 'en-US' | 'zh-TW'; 48 language: 'zh-CN' | 'en-US' | 'zh-TW';
48 canEditDocument: boolean; 49 canEditDocument: boolean;
49 - employeeId?: number; 50 + employeeId?: number | null;
50 permissionCategoryIds?: number[]; 51 permissionCategoryIds?: number[];
51 } 52 }
52 53
@@ -55,7 +56,7 @@ export interface UpdateUserReq { @@ -55,7 +56,7 @@ export interface UpdateUserReq {
55 userType?: 'NORMAL' | 'SUPER_ADMIN'; 56 userType?: 'NORMAL' | 'SUPER_ADMIN';
56 language?: 'zh-CN' | 'en-US' | 'zh-TW'; 57 language?: 'zh-CN' | 'en-US' | 'zh-TW';
57 canEditDocument?: boolean; 58 canEditDocument?: boolean;
58 - employeeId?: number; 59 + employeeId?: number | null;
59 isDeleted?: boolean; 60 isDeleted?: boolean;
60 permissionCategoryIds?: number[]; 61 permissionCategoryIds?: number[];
61 } 62 }
frontend/src/pages/users/UserFormPage.test.tsx
@@ -84,6 +84,15 @@ describe('UserFormPage (edit)', () => { @@ -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 it('unknown userId (40401) shows 404 result', async () => { 96 it('unknown userId (40401) shows 404 result', async () => {
88 renderForm('edit', '/users/99999'); 97 renderForm('edit', '/users/99999');
89 await waitFor(() => expect(screen.getByTestId('user-not-found')).toBeInTheDocument()); 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,7 +47,7 @@ export default function UserFormPage({ mode }: Props) {
47 userCode: detail.userCode, 47 userCode: detail.userCode,
48 userType: detail.userType, 48 userType: detail.userType,
49 language: detail.language as FormValues['language'], 49 language: detail.language as FormValues['language'],
50 - canEditDocument: false, // detail VO 当前不返回,默认 false 50 + canEditDocument: detail.canEditDocument ?? false,
51 employeeId: detail.employeeId ?? undefined, 51 employeeId: detail.employeeId ?? undefined,
52 }); 52 });
53 setPermissionCategoryIds(detail.permissionCategoryIds ?? []); 53 setPermissionCategoryIds(detail.permissionCategoryIds ?? []);
@@ -69,6 +69,13 @@ export default function UserFormPage({ mode }: Props) { @@ -69,6 +69,13 @@ export default function UserFormPage({ mode }: Props) {
69 }; 69 };
70 }, [mode, userId, form]); 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 const handleSubmit = async (values: FormValues) => { 79 const handleSubmit = async (values: FormValues) => {
73 setSubmitting(true); 80 setSubmitting(true);
74 setErrorMessage(null); 81 setErrorMessage(null);
@@ -85,7 +92,7 @@ export default function UserFormPage({ mode }: Props) { @@ -85,7 +92,7 @@ export default function UserFormPage({ mode }: Props) {
85 userType: values.userType!, 92 userType: values.userType!,
86 language: values.language!, 93 language: values.language!,
87 canEditDocument: !!values.canEditDocument, 94 canEditDocument: !!values.canEditDocument,
88 - employeeId: values.employeeId, 95 + employeeId: toCreateEmployeeId(values.employeeId),
89 permissionCategoryIds, 96 permissionCategoryIds,
90 }); 97 });
91 message.success('新增用户成功'); 98 message.success('新增用户成功');
@@ -95,7 +102,7 @@ export default function UserFormPage({ mode }: Props) { @@ -95,7 +102,7 @@ export default function UserFormPage({ mode }: Props) {
95 userType: values.userType, 102 userType: values.userType,
96 language: values.language, 103 language: values.language,
97 canEditDocument: values.canEditDocument, 104 canEditDocument: values.canEditDocument,
98 - employeeId: values.employeeId, 105 + employeeId: toUpdateEmployeeId(values.employeeId),
99 permissionCategoryIds, 106 permissionCategoryIds,
100 }; 107 };
101 await usersApi.update(userId, patch); 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,6 +28,8 @@ export default function UserPermissionPanel({ value, onChange, disabled = false
28 { key: 'customer', label: '客户查看权限', disabled: true, children: null }, 28 { key: 'customer', label: '客户查看权限', disabled: true, children: null },
29 { key: 'supplier', label: '供应商查看权限', disabled: true, children: null }, 29 { key: 'supplier', label: '供应商查看权限', disabled: true, children: null },
30 { key: 'person', label: '人员查看权限', disabled: true, children: null }, 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 </div> 35 </div>
frontend/src/pages/users/UsersListPage.tsx
@@ -12,7 +12,12 @@ import UsersTable from &#39;./UsersTable&#39;; @@ -12,7 +12,12 @@ import UsersTable from &#39;./UsersTable&#39;;
12 12
13 export default function UsersListPage() { 13 export default function UsersListPage() {
14 const navigate = useNavigate(); 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 const [records, setRecords] = useState<UserListItem[]>([]); 21 const [records, setRecords] = useState<UserListItem[]>([]);
17 const [total, setTotal] = useState(0); 22 const [total, setTotal] = useState(0);
18 const [loading, setLoading] = useState(false); 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 import type { ColumnsType } from 'antd/es/table'; 2 import type { ColumnsType } from 'antd/es/table';
3 import type { UserListItem } from '../../api/users'; 3 import type { UserListItem } from '../../api/users';
4 4
@@ -28,9 +28,9 @@ export default function UsersTable({ @@ -28,9 +28,9 @@ export default function UsersTable({
28 width: 60, 28 width: 60,
29 render: (_: unknown, __: unknown, idx: number) => (page - 1) * size + idx + 1, 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 { title: '员工名', dataIndex: 'employeeName', key: 'employeeName' }, 32 { title: '员工名', dataIndex: 'employeeName', key: 'employeeName' },
33 - { title: '用户号', dataIndex: 'userCode', key: 'userCode' }, 33 + { title: '用户号', dataIndex: 'userCode', key: 'userCode', sorter: true },
34 { title: '部门', dataIndex: 'departmentName', key: 'departmentName' }, 34 { title: '部门', dataIndex: 'departmentName', key: 'departmentName' },
35 { 35 {
36 title: '用户类型', 36 title: '用户类型',
@@ -46,9 +46,27 @@ export default function UsersTable({ @@ -46,9 +46,27 @@ export default function UsersTable({
46 render: (v: boolean) => 46 render: (v: boolean) =>
47 v ? <Tag color="error">作废</Tag> : <Tag color="success">启用</Tag>, 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 { title: '制单人', dataIndex: 'createdBy', key: 'createdBy' }, 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 return ( 72 return (
@@ -59,7 +77,16 @@ export default function UsersTable({ @@ -59,7 +77,16 @@ export default function UsersTable({
59 loading={loading} 77 loading={loading}
60 onRow={(row) => ({ 78 onRow={(row) => ({
61 onClick: () => onRowClick(row), 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 style: { cursor: 'pointer' }, 86 style: { cursor: 'pointer' },
  87 + tabIndex: 0,
  88 + role: 'button',
  89 + 'aria-label': `查看用户 ${row.username}`,
63 'data-testid': `user-row-${row.userId}`, 90 'data-testid': `user-row-${row.userId}`,
64 })} 91 })}
65 pagination={{ 92 pagination={{
frontend/src/pages/users/usersConstants.ts
@@ -16,6 +16,7 @@ export const QUERY_FIELD_OPTIONS = [ @@ -16,6 +16,7 @@ export const QUERY_FIELD_OPTIONS = [
16 { value: 'departmentName', label: '部门' }, 16 { value: 'departmentName', label: '部门' },
17 { value: 'userType', label: '用户类型' }, 17 { value: 'userType', label: '用户类型' },
18 { value: 'isDeleted', label: '作废' }, 18 { value: 'isDeleted', label: '作废' },
  19 + { value: 'lastLoginDate', label: '登录日期' },
19 { value: 'createdBy', label: '制单人' }, 20 { value: 'createdBy', label: '制单人' },
20 ] as const; 21 ] as const;
21 22
frontend/src/test-utils/msw-handlers.ts
@@ -148,6 +148,7 @@ export const handlers = [ @@ -148,6 +148,7 @@ export const handlers = [
148 language: 'zh-CN', 148 language: 'zh-CN',
149 isDeleted: false, 149 isDeleted: false,
150 lastLoginDate: '2026-05-15T08:00:00', 150 lastLoginDate: '2026-05-15T08:00:00',
  151 + canEditDocument: true,
151 employeeId: 1, 152 employeeId: 1,
152 permissionCategoryIds: [1, 2], 153 permissionCategoryIds: [1, 2],
153 createdBy: 'admin', 154 createdBy: 'admin',