feat(web): 初始化前端项目
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PagePlaceholder from '@/components/PagePlaceholder';
|
||||
|
||||
/** 采集箱:查看已采集的商品,进入编辑上架(业务模块尚未实现) */
|
||||
const CollectionPage = () => (
|
||||
<PagePlaceholder title="采集箱" description="查看已采集的商品,进入编辑上架,模块开发中。" />
|
||||
<PagePlaceholder title="采集箱" description="查看已采集的商品,进入编辑上架,模块开发中。" />
|
||||
);
|
||||
|
||||
export default CollectionPage;
|
||||
|
||||
@@ -2,10 +2,10 @@ import PagePlaceholder from '@/components/PagePlaceholder';
|
||||
|
||||
/** 在线商品:按 SKU 查看平台已上架商品与同步状态(业务模块尚未实现) */
|
||||
const OnlineProductsPage = () => (
|
||||
<PagePlaceholder
|
||||
title="在线商品"
|
||||
description="按 SKU 查看平台已上架商品与同步状态,模块开发中。"
|
||||
/>
|
||||
<PagePlaceholder
|
||||
title="在线商品"
|
||||
description="按 SKU 查看平台已上架商品与同步状态,模块开发中。"
|
||||
/>
|
||||
);
|
||||
|
||||
export default OnlineProductsPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PagePlaceholder from '@/components/PagePlaceholder';
|
||||
|
||||
/** 定价工具:独立试算与物流计价规则(业务模块尚未实现) */
|
||||
const PricingPage = () => (
|
||||
<PagePlaceholder title="定价工具" description="独立试算与物流计价规则,模块开发中。" />
|
||||
<PagePlaceholder title="定价工具" description="独立试算与物流计价规则,模块开发中。" />
|
||||
);
|
||||
|
||||
export default PricingPage;
|
||||
|
||||
@@ -3,14 +3,14 @@ import PagePlaceholder from '@/components/PagePlaceholder';
|
||||
|
||||
/** 商品 AI 编辑页:编辑 SPU/SKU、媒体、定价与首次发布(业务模块尚未实现) */
|
||||
const ProductEditPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<PagePlaceholder
|
||||
title={`商品编辑(${id ?? '未知 ID'})`}
|
||||
description="商品 AI 编辑页:SPU/SKU、媒体、定价与发布,模块开发中。"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<PagePlaceholder
|
||||
title={`商品编辑(${id ?? '未知 ID'})`}
|
||||
description="商品 AI 编辑页:SPU/SKU、媒体、定价与发布,模块开发中。"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductEditPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PagePlaceholder from '@/components/PagePlaceholder';
|
||||
|
||||
/** 店铺管理:绑定平台店铺授权与水印(业务模块尚未实现) */
|
||||
const ShopsPage = () => (
|
||||
<PagePlaceholder title="店铺管理" description="绑定平台店铺授权与水印,模块开发中。" />
|
||||
<PagePlaceholder title="店铺管理" description="绑定平台店铺授权与水印,模块开发中。" />
|
||||
);
|
||||
|
||||
export default ShopsPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PagePlaceholder from '@/components/PagePlaceholder';
|
||||
|
||||
/** 水印管理:维护水印模板并绑定到店铺(业务模块尚未实现) */
|
||||
const WatermarksPage = () => (
|
||||
<PagePlaceholder title="水印管理" description="维护水印模板并绑定到店铺,模块开发中。" />
|
||||
<PagePlaceholder title="水印管理" description="维护水印模板并绑定到店铺,模块开发中。" />
|
||||
);
|
||||
|
||||
export default WatermarksPage;
|
||||
|
||||
Reference in New Issue
Block a user