Files
dukang/apps/admin-web/src/pages/LoginPage.tsx
T
2026-08-04 21:38:49 +08:00

148 lines
4.8 KiB
TypeScript

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, Card, Form, Input, Tabs, message, Typography } from 'antd';
import { MOCK_SMS_FIXED_CODE } from '@dukang/shared-types';
import { saveAuth, request } from '../lib/api';
type LoginResult = { accessToken: string; refreshToken: string };
export default function LoginPage() {
const navigate = useNavigate();
const [smsForm] = Form.useForm();
const [passwordForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [codeCooldown, setCodeCooldown] = useState(0);
const phone = Form.useWatch('phone', smsForm);
async function sendCode() {
if (!phone) {
message.warning('请先输入手机号');
return;
}
await request('/admin/auth/sms/send', {
method: 'POST',
body: JSON.stringify({ phone, scene: 'HQ_LOGIN' }),
});
message.success('验证码已发送');
setCodeCooldown(60);
const timer = setInterval(() => {
setCodeCooldown((c) => {
if (c <= 1) {
clearInterval(timer);
return 0;
}
return c - 1;
});
}, 1000);
}
async function finishLogin(data: LoginResult) {
saveAuth(data);
message.success('登录成功');
navigate('/');
}
async function onSmsFinish(values: { phone: string; code: string }) {
setLoading(true);
try {
const data = await request<LoginResult>('/admin/auth/login/sms', {
method: 'POST',
body: JSON.stringify(values),
});
await finishLogin(data);
} catch (e) {
message.error(e instanceof Error ? e.message : '登录失败');
} finally {
setLoading(false);
}
}
async function onPasswordFinish(values: { loginName: string; password: string }) {
setLoading(true);
try {
const data = await request<LoginResult>('/admin/auth/login/password', {
method: 'POST',
body: JSON.stringify(values),
});
await finishLogin(data);
} catch (e) {
message.error(e instanceof Error ? e.message : '登录失败');
} finally {
setLoading(false);
}
}
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f5f5f5',
}}
>
<Card style={{ width: 400 }}>
<Typography.Title level={3} style={{ textAlign: 'center', marginBottom: 24 }}>
HQ 管理后台
</Typography.Title>
<Tabs
items={[
{
key: 'password',
label: '账号密码',
children: (
<Form
form={passwordForm}
layout="vertical"
onFinish={onPasswordFinish}
initialValues={{ loginName: 'admin' }}
>
<Form.Item name="loginName" label="账号" rules={[{ required: true, message: '请输入账号' }]}>
<Input placeholder="admin" autoComplete="username" />
</Form.Item>
<Form.Item name="password" label="密码" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password placeholder="请输入密码" autoComplete="current-password" />
</Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}>
登录
</Button>
</Form>
),
},
{
key: 'sms',
label: '短信验证码',
children: (
<Form
form={smsForm}
layout="vertical"
onFinish={onSmsFinish}
initialValues={{ phone: '13600000001', code: MOCK_SMS_FIXED_CODE }}
>
<Form.Item name="phone" label="手机号" rules={[{ required: true, message: '请输入手机号' }]}>
<Input placeholder="13600000001" maxLength={11} />
</Form.Item>
<Form.Item name="code" label="验证码" rules={[{ required: true, message: '请输入验证码' }]}>
<Input
placeholder={MOCK_SMS_FIXED_CODE}
addonAfter={
<Button type="link" size="small" disabled={codeCooldown > 0} onClick={() => void sendCode()}>
{codeCooldown > 0 ? `${codeCooldown}s` : '获取验证码'}
</Button>
}
/>
</Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}>
登录
</Button>
</Form>
),
},
]}
/>
</Card>
</div>
);
}