// 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(); const dispatch = useAppDispatch(); const navigate = useNavigate(); const { message } = AntdApp.useApp(); const passwordRef = useRef(null); const [companies, setCompanies] = useState([]); 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 (
Antler ERP 欢迎登录EBC平台
Enterprise Business Capability
企业业务能力平台
ERP

用户登录

form={form} onFinish={handleFinish} layout="vertical" requiredMark={false} disabled={submitting} > } placeholder="请输入你的用户名" size="large" autoComplete="username" /> } placeholder="请输入你的密码" size="large" autoComplete="current-password" />