167 lines
6.3 KiB
TypeScript
167 lines
6.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { App as AntdApp, Button, Form, Input } from 'antd';
|
|
import { LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
|
|
import axios from 'axios';
|
|
import AuthLayout from '@/layouts/AuthLayout';
|
|
import { apiErrorMessage } from '@/services/api';
|
|
import { authApi } from '@/services/auth';
|
|
import { authToken } from '@/services/authToken';
|
|
import { emailFormatRule } from '@/utils/validators';
|
|
|
|
interface RegisterFormValues {
|
|
display_name?: string;
|
|
email: string;
|
|
password: string;
|
|
password_confirmation: string;
|
|
}
|
|
|
|
/** 注册页,契约见 docs/api/auth.md §2;密码长度与复杂度由服务端唯一校验,前端不自行放宽或收窄 */
|
|
const RegisterPage = () => {
|
|
const navigate = useNavigate();
|
|
const { message } = AntdApp.useApp();
|
|
const [form] = Form.useForm<RegisterFormValues>();
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const handleFinish = async (values: RegisterFormValues) => {
|
|
setSubmitting(true);
|
|
try {
|
|
const session = await authApi.register({
|
|
email: values.email.trim(),
|
|
password: values.password,
|
|
password_confirmation: values.password_confirmation,
|
|
display_name: values.display_name?.trim() || undefined,
|
|
});
|
|
// 注册成功即在同一事务创建用户、个人工作空间和登录会话
|
|
authToken.set(session.access_token);
|
|
message.success('注册成功,已进入你的工作空间');
|
|
navigate('/online-products', { replace: true });
|
|
} catch (error) {
|
|
// 重复邮箱返回 409 EMAIL_ALREADY_REGISTERED,落到邮箱字段上提示
|
|
if (axios.isAxiosError(error) && error.response?.status === 409) {
|
|
form.setFields([
|
|
{ name: 'email', errors: ['该邮箱已注册,请直接登录'] },
|
|
]);
|
|
} else {
|
|
message.error(apiErrorMessage(error));
|
|
}
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AuthLayout
|
|
title="注册"
|
|
subtitle="创建账户后自动创建个人工作空间,注册即登录"
|
|
footer={
|
|
<>
|
|
已有账户?<Link to="/login">直接登录</Link>
|
|
</>
|
|
}
|
|
>
|
|
<Form<RegisterFormValues>
|
|
form={form}
|
|
layout="vertical"
|
|
requiredMark={false}
|
|
onFinish={handleFinish}
|
|
>
|
|
<Form.Item
|
|
name="display_name"
|
|
label="显示名称"
|
|
extra="选填,最多 80 个字符"
|
|
rules={[{ max: 80, message: '显示名称最多 80 个字符' }]}
|
|
>
|
|
<Input
|
|
prefix={
|
|
<UserOutlined
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
/>
|
|
}
|
|
placeholder="选填"
|
|
maxLength={80}
|
|
autoComplete="nickname"
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="email"
|
|
label="邮箱"
|
|
rules={[
|
|
{ required: true, message: '请输入邮箱' },
|
|
emailFormatRule,
|
|
]}
|
|
>
|
|
<Input
|
|
prefix={
|
|
<MailOutlined
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
/>
|
|
}
|
|
placeholder="请输入邮箱"
|
|
autoComplete="email"
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="password"
|
|
label="密码"
|
|
extra="长度与复杂度规则以服务端安全校验为准"
|
|
rules={[{ required: true, message: '请输入密码' }]}
|
|
>
|
|
<Input.Password
|
|
prefix={
|
|
<LockOutlined
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
/>
|
|
}
|
|
placeholder="请输入密码"
|
|
autoComplete="new-password"
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="password_confirmation"
|
|
label="确认密码"
|
|
dependencies={['password']}
|
|
rules={[
|
|
{ required: true, message: '请再次输入密码' },
|
|
({ getFieldValue }) => ({
|
|
validator(_, value) {
|
|
if (
|
|
!value ||
|
|
getFieldValue('password') === value
|
|
) {
|
|
return Promise.resolve();
|
|
}
|
|
return Promise.reject(
|
|
new Error('两次输入的密码不一致'),
|
|
);
|
|
},
|
|
}),
|
|
]}
|
|
>
|
|
<Input.Password
|
|
prefix={
|
|
<LockOutlined
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
/>
|
|
}
|
|
placeholder="请再次输入密码"
|
|
autoComplete="new-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 RegisterPage;
|