feat(web): 初始化前端项目

This commit is contained in:
R524809
2026-09-03 13:11:38 +08:00
parent 195b6d25e9
commit df40ddde8c
31 changed files with 948 additions and 848 deletions
+67 -63
View File
@@ -10,76 +10,80 @@ import { authToken } from '@/services/authToken';
import { emailFormatRule } from '@/utils/validators';
interface LoginFormValues {
email: string;
password: string;
email: string;
password: string;
}
/** 登录页,契约见 docs/api/auth.md §3 */
const LoginPage = () => {
const navigate = useNavigate();
const { message } = AntdApp.useApp();
const [submitting, setSubmitting] = useState(false);
const navigate = useNavigate();
const { message } = AntdApp.useApp();
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,
});
authToken.set(session.access_token);
message.success('登录成功');
navigate('/online-products', { replace: true });
} catch (error) {
// 契约约定登录失败统一返回 401 INVALID_CREDENTIALS,不区分邮箱不存在或密码错误
if (axios.isAxiosError(error) && error.response?.status === 401) {
message.error('邮箱或密码不正确');
} else {
message.error(apiErrorMessage(error));
}
} finally {
setSubmitting(false);
}
};
const handleFinish = async (values: LoginFormValues) => {
setSubmitting(true);
try {
const session = await authApi.login({
email: values.email.trim(),
password: values.password,
});
authToken.set(session.access_token);
message.success('登录成功');
navigate('/online-products', { replace: true });
} catch (error) {
// 契约约定登录失败统一返回 401 INVALID_CREDENTIALS,不区分邮箱不存在或密码错误
if (axios.isAxiosError(error) && error.response?.status === 401) {
message.error('邮箱或密码不正确');
} else {
message.error(apiErrorMessage(error));
}
} finally {
setSubmitting(false);
}
};
return (
<AuthLayout
title="登录"
subtitle="登录 Atlas Seller Kit,继续管理你的商品"
footer={
<>
还没有账户?<Link to="/register">立即注册</Link>
</>
}
>
<Form<LoginFormValues> layout="vertical" requiredMark={false} onFinish={handleFinish}>
<Form.Item
name="email"
label="邮箱"
rules={[{ required: true, message: '请输入邮箱' }, emailFormatRule]}
return (
<AuthLayout
title="登录"
subtitle="登录 Atlas Seller Kit,继续管理你的商品"
footer={
<>
还没有账户?<Link to="/register">立即注册</Link>
</>
}
>
<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>
);
<Form<LoginFormValues> 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;
+113 -113
View File
@@ -10,126 +10,126 @@ import { authToken } from '@/services/authToken';
import { emailFormatRule } from '@/utils/validators';
interface RegisterFormValues {
display_name?: string;
email: string;
password: string;
password_confirmation: string;
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 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);
}
};
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 个字符' }]}
return (
<AuthLayout
title="注册"
subtitle="创建账户后自动创建个人工作空间,注册即登录"
footer={
<>
已有账户?<Link to="/login">直接登录</Link>
</>
}
>
<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>
);
<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;