index.tsx
5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// REQ-USR-001 / REQ-USR-002: 用户单据页面容器(判 mode、装配 4 子组件、提交反馈与导航回流)
import { useEffect } from 'react';
import { Form, Spin, Button, Modal, App as AntdApp } from 'antd';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import type { UserVO } from '../../../api/types';
import UserDetailToolbar from './UserDetailToolbar';
import UserBasicForm from './UserBasicForm';
import PermissionTabs from './PermissionTabs';
import PermissionGroupList from './PermissionGroupList';
import { useUserDetail } from './useUserDetail';
import {
MODE_CREATE,
MODE_EDIT,
MSG_CREATE_SUCCESS,
MSG_EDIT_SUCCESS,
MSG_LOAD_DETAIL_FAIL,
MSG_CANCEL_CONFIRM,
PATH_USER_LIST,
PATH_USER_NEW,
TEXT_BACK_TO_LIST,
TEXT_RETRY,
type UserFormValues,
} from './constants';
import styles from './UserDetail.module.css';
/** Checkbox 受控值为 boolean,提交映射需 0/1(BR8) */
function normalizeFormValues(raw: UserFormValues): UserFormValues {
return {
...raw,
iCanModifyBill: (raw.iCanModifyBill ? 1 : 0) as 0 | 1,
};
}
export default function UserDetailPage() {
const navigate = useNavigate();
const params = useParams<{ id?: string }>();
const location = useLocation();
const { message } = AntdApp.useApp();
const [form] = Form.useForm<UserFormValues>();
const mode = params.id ? MODE_EDIT : MODE_CREATE;
const userId = params.id ? Number(params.id) : undefined;
const presetUser = (location.state as { user?: UserVO } | null)?.user ?? null;
const detail = useUserDetail({ mode, userId, presetUser });
const {
formValues,
employees,
permissions,
checkedPermissionIds,
readonlyCreator,
readonlyCreateTime,
loading,
submitting,
loadFailed,
selectEmployee,
togglePermission,
toggleAll,
submit,
reload,
} = detail;
// hook 持有的受控值回写到 AntD Form(create 默认 / edit 回填,BR1/BR2/BR6/BR17)
useEffect(() => {
form.setFieldsValue({
...formValues,
iCanModifyBill: (formValues.iCanModifyBill ? 1 : 0) as 0 | 1,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formValues]);
const handleSave = async () => {
try {
const values = await form.validateFields();
const ret = await submit(normalizeFormValues({ ...formValues, ...values }));
if (ret.ok) {
message.success(mode === MODE_CREATE ? MSG_CREATE_SUCCESS : MSG_EDIT_SUCCESS);
navigate(PATH_USER_LIST);
} else if (ret.fieldError) {
form.setFields([
{ name: ret.fieldError.field, errors: [ret.fieldError.message] },
]);
}
} catch {
// validateFields 失败:就近字段已展示错误,不发请求(BR12)
}
};
const handleCancel = () => {
if (form.isFieldsTouched()) {
Modal.confirm({
title: MSG_CANCEL_CONFIRM,
onOk: () => navigate(PATH_USER_LIST),
});
} else {
navigate(PATH_USER_LIST);
}
};
const handleNew = () => {
navigate(PATH_USER_NEW);
};
const handleSelectEmployee = (value: number | null) => {
selectEmployee(value);
};
// 预取/详情取数失败:整页重试入口(spec § 4 loadError)。
// edit 态额外给「返回列表」——edit 预填只能来自列表行经 navigate state 透传的
// presetUser(无 by-id 读端点,详见 useUserDetail),缺 state 时重试仍会回到 loadError,
// 需经列表双击重新携带 state 进入,故提供返回列表入口(spec § 4「edit 详情失败给整页重试或返回列表」)。
if (loadFailed) {
return (
<div className={styles.page}>
<div className={styles.loadError} data-testid="userdetail-loaderror">
<span className={styles.loadErrorText}>{MSG_LOAD_DETAIL_FAIL}</span>
<Button type="primary" onClick={() => reload()}>
{TEXT_RETRY}
</Button>
{mode === MODE_EDIT && (
<Button onClick={() => navigate(PATH_USER_LIST)}>
{TEXT_BACK_TO_LIST}
</Button>
)}
</div>
</div>
);
}
return (
<div className={styles.page} data-testid="userdetail-page">
<UserDetailToolbar
mode={mode}
submitting={submitting}
canSave={!loading}
onSave={() => void handleSave()}
onCancel={handleCancel}
onNew={handleNew}
/>
<Spin spinning={loading}>
<Form form={form} layout="vertical" component={false}>
<UserBasicForm
form={form}
mode={mode}
employees={employees}
readonlyCreator={readonlyCreator}
readonlyCreateTime={readonlyCreateTime}
onSelectEmployee={handleSelectEmployee}
/>
</Form>
<PermissionTabs>
<PermissionGroupList
permissions={permissions}
checkedIds={checkedPermissionIds}
onToggle={togglePermission}
onToggleAll={toggleAll}
/>
</PermissionTabs>
</Spin>
</div>
);
}