121 lines
4.6 KiB
TypeScript
121 lines
4.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import { App as AntdApp, Button, Form, Input } from 'antd';
|
|
import { LockOutlined, MailOutlined } from '@ant-design/icons';
|
|
import axios from 'axios';
|
|
import AuthLayout from '@/layouts/AuthLayout';
|
|
import { apiErrorDetails, apiErrorMessage } from '@/services/api';
|
|
import { authApi } from '@/services/auth';
|
|
import { useSession } from '@/services/session';
|
|
import { emailFormatRule } from '@/utils/validators';
|
|
|
|
interface LoginFormValues {
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
/** 登录页,契约见 docs/api/auth.md §3 */
|
|
const LoginPage = () => {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { message } = AntdApp.useApp();
|
|
const { applyAuthSession } = useSession();
|
|
const [form] = Form.useForm<LoginFormValues>();
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const handleFinish = async (values: LoginFormValues) => {
|
|
setSubmitting(true);
|
|
try {
|
|
const session = await authApi.login({
|
|
email: values.email.trim(),
|
|
password: values.password,
|
|
});
|
|
applyAuthSession(session);
|
|
message.success('登录成功');
|
|
const continuation = new URLSearchParams(location.search).get('continue');
|
|
if (continuation) {
|
|
const target = new URL(continuation);
|
|
if (target.origin === (import.meta.env.VITE_API_PUBLIC_ORIGIN ?? 'http://localhost:8200') && target.pathname === '/api/extension/authorize') {
|
|
window.location.assign(target.toString());
|
|
return;
|
|
}
|
|
}
|
|
// 守卫记住的来源路径优先,默认进入在线商品首页
|
|
const from = (location.state as { from?: string } | null)?.from;
|
|
navigate(from && from.startsWith('/') ? from : '/online-products', {
|
|
replace: true,
|
|
});
|
|
} catch (error) {
|
|
// 契约约定登录失败统一返回 401 INVALID_CREDENTIALS,不区分邮箱不存在或密码错误
|
|
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
|
message.error('邮箱或密码不正确');
|
|
return;
|
|
}
|
|
// 422 VALIDATION_FAILED 等带字段明细的错误,逐项落到对应表单项
|
|
const details = apiErrorDetails(error);
|
|
if (details.length > 0) {
|
|
form.setFields(
|
|
details.map((item) => ({
|
|
name: item.field as keyof LoginFormValues,
|
|
errors: [item.message],
|
|
}))
|
|
);
|
|
} else {
|
|
message.error(apiErrorMessage(error));
|
|
}
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AuthLayout
|
|
title="登录"
|
|
subtitle="登录 Atlas Seller Kit,继续管理你的商品"
|
|
footer={
|
|
<>
|
|
还没有账户?<Link to="/register">立即注册</Link>
|
|
</>
|
|
}
|
|
>
|
|
<Form<LoginFormValues>
|
|
form={form}
|
|
layout="vertical"
|
|
requiredMark={false}
|
|
onFinish={handleFinish}
|
|
>
|
|
<Form.Item
|
|
name="email"
|
|
label="邮箱"
|
|
rules={[{ required: true, message: '请输入邮箱' }, emailFormatRule]}
|
|
>
|
|
<Input
|
|
prefix={<MailOutlined style={{ color: 'var(--text-secondary)' }} />}
|
|
placeholder="请输入邮箱"
|
|
autoComplete="email"
|
|
autoFocus
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="password"
|
|
label="密码"
|
|
rules={[{ required: true, message: '请输入密码' }]}
|
|
>
|
|
<Input.Password
|
|
prefix={<LockOutlined style={{ color: 'var(--text-secondary)' }} />}
|
|
placeholder="请输入密码"
|
|
autoComplete="current-password"
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item style={{ marginBottom: 0, marginTop: 8 }}>
|
|
<Button type="primary" htmlType="submit" block loading={submitting}>
|
|
登录
|
|
</Button>
|
|
</Form.Item>
|
|
</Form>
|
|
</AuthLayout>
|
|
);
|
|
};
|
|
|
|
export default LoginPage;
|