LoginPage.tsx
7.6 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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// REQ-USR-004: 登录页(复刻 prototype #screen-login 三段式布局 + 真实对接后端)
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button, Form, Input, Select, App as AntdApp, type InputRef } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useAppDispatch } from '../../../store/hooks';
import { setCredentials } from '../../../store/slices/authSlice';
import { login, fetchCompanies } from '../../../api/usrApi';
import type { CompanyOption, LoginPayload } from '../../../api/types';
import { ApiError } from '../../../api/request';
import {
LOGIN_SUCCESS_TEXT,
resolveLoginErrorText,
CLEAR_PASSWORD_CODES,
} from './loginMessages';
import styles from './Login.module.css';
interface LoginFormValues {
sUserName: string;
password: string;
companyId: number;
}
// 版本下拉 label 规则(D8):有 sVersion → 「公司名(版本)」,否则仅公司名;value 恒取 id
function toOption(c: CompanyOption) {
return {
value: c.id,
label: c.sVersion ? `${c.sCompanyName}(${c.sVersion})` : c.sCompanyName,
};
}
export default function LoginPage() {
const [form] = Form.useForm<LoginFormValues>();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { message } = AntdApp.useApp();
const passwordRef = useRef<InputRef>(null);
const [companies, setCompanies] = useState<CompanyOption[]>([]);
const [companiesLoading, setCompaniesLoading] = useState(false);
const [companiesError, setCompaniesError] = useState(false);
const [submitting, setSubmitting] = useState(false);
const loadCompanies = useCallback(async () => {
setCompaniesLoading(true);
setCompaniesError(false);
try {
const list = await fetchCompanies();
setCompanies(list);
// 仅一项时默认选中(spec § 6.3)
if (list.length === 1) {
form.setFieldValue('companyId', list[0].id);
}
} catch {
setCompaniesError(true);
setCompanies([]);
message.error('版本加载失败');
} finally {
setCompaniesLoading(false);
}
}, [form, message]);
// 页面挂载即预加载版本下拉(spec § 3 companiesLoading)
useEffect(() => {
void loadCompanies();
}, [loadCompanies]);
const options = useMemo(() => companies.map(toOption), [companies]);
const isEmpty = !companiesLoading && !companiesError && companies.length === 0;
const handleFinish = async (values: LoginFormValues) => {
if (submitting) return; // 防重复提交(BR10)
setSubmitting(true);
const payload: LoginPayload = {
sUserName: values.sUserName,
password: values.password,
companyId: values.companyId,
};
try {
const { token, user } = await login(payload);
dispatch(setCredentials({ token, user }));
message.success(LOGIN_SUCCESS_TEXT);
navigate('/', { replace: true });
} catch (err) {
const code = err instanceof ApiError ? err.code : -1;
message.error(resolveLoginErrorText(code));
if (CLEAR_PASSWORD_CODES.has(code)) {
form.setFieldValue('password', '');
// 字段在 submitting 期间禁用,待 submitting 复位后再聚焦密码框(D5)
setSubmitting(false);
// 等待禁用状态解除后聚焦,避免聚焦到 disabled 输入失败
setTimeout(() => passwordRef.current?.focus(), 0);
return;
}
} finally {
setSubmitting(false);
}
};
const versionPlaceholder = companiesLoading ? '加载版本中…' : '请选择版本';
return (
<div className={styles.wrap}>
<div className={styles.head}>
<span className={styles.logo}>
<svg viewBox="0 0 64 64" width="42" height="42" fill="#0e1216" aria-hidden="true">
<path d="M14 10c2 4 1 8-1 11 3-1 7 0 10 3 1-4 4-7 8-7-3 3-4 7-3 11l4 1c-1 3 0 6 3 8-3 0-6 1-8 4-1-3-4-5-8-5 2-3 2-7 0-10-3 1-7 0-10-3 3 0 5-2 6-5l-1-8z" />
<path d="M48 14c-2 3-2 6-1 9-2-2-5-2-8-1 1 3 1 6-1 9 3 0 5 2 6 5 1-3 4-5 7-5-2-3-2-6 0-9 2 1 5 1 7-1-2 0-4-1-5-3-1-2-3-4-5-4z" />
<path d="M28 38c2 3 5 5 9 5 1 4 4 7 8 8-3 2-5 5-5 9-3-2-7-3-11-2 1-3 1-7-1-10-3 0-6-1-8-4 3-1 6-3 8-6z" />
</svg>
</span>
<span className={styles.brandName}>Antler ERP</span>
<span className={styles.brandSub}>欢迎登录EBC平台</span>
</div>
<div className={styles.hero}>
<div className={styles.heroText}>
<div className={styles.heroEn}>Enterprise Business Capability</div>
<div className={styles.heroZh}>企业业务能力平台</div>
<div className={styles.heroErp}>ERP</div>
</div>
<div className={styles.card}>
<h3 className={styles.cardTitle}>用户登录</h3>
<Form<LoginFormValues>
form={form}
onFinish={handleFinish}
layout="vertical"
requiredMark={false}
disabled={submitting}
>
<Form.Item
name="sUserName"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="请输入你的用户名"
size="large"
autoComplete="username"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
ref={passwordRef}
prefix={<LockOutlined />}
placeholder="请输入你的密码"
size="large"
autoComplete="current-password"
/>
</Form.Item>
<Form.Item
name="companyId"
rules={[{ required: true, message: '请选择版本' }]}
>
<Select
placeholder={versionPlaceholder}
size="large"
loading={companiesLoading}
disabled={companiesLoading}
options={options}
notFoundContent={isEmpty ? '暂无可用版本' : undefined}
/>
</Form.Item>
{isEmpty && (
<div className={styles.emptyHint}>未获取到可登录版本,请联系管理员</div>
)}
{companiesError && (
<div className={styles.retryBox}>
<span>版本加载失败</span>
<Button
type="link"
size="small"
disabled={false}
onClick={() => void loadCompanies()}
>
点击重试
</Button>
</div>
)}
<Form.Item>
<Button
type="primary"
htmlType="submit"
block
size="large"
loading={submitting}
className={styles.submitBtn}
>
登 录
</Button>
</Form.Item>
</Form>
</div>
</div>
<div className={styles.foot}>
🛠 ©Copyright Antler Software | 印刷智慧工厂 | 印刷MES | 印刷ERP | 印刷电商平台 |
文件智能处理 | 印前自动化 | 400-880-6237
<span className={styles.footIcp}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="#3a6cb6" aria-hidden="true">
<path d="M12 2l9 4v6c0 5-4 9-9 10-5-1-9-5-9-10V6z" />
</svg>
沪ICP备14034791号-1
</span>
</div>
</div>
);
}