feat: complete login and user management flow

This commit is contained in:
hectorzhao
2026-07-02 16:28:48 +08:00
parent 9c639d54b9
commit e26d8324c3
22 changed files with 1320 additions and 300 deletions
+78
View File
@@ -0,0 +1,78 @@
import { useEffect, useState } from 'react';
import { ShieldCheck } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { adminApi, clientApi, type CaptchaResponse } from '@/api/adminApi';
import { writeSession, type Portal } from '@/api/session';
import { Button, Input } from '@/components/ui';
type LoginPageProps = {
portal: Portal;
};
export function LoginPage({ portal }: LoginPageProps) {
const navigate = useNavigate();
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const [captchaText, setCaptchaText] = useState('');
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const isAdmin = portal === 'admin';
async function refreshCaptcha() {
setError('');
setCaptchaText('');
setCaptcha(await (isAdmin ? adminApi.getCaptcha() : clientApi.getCaptcha()));
}
useEffect(() => {
void refreshCaptcha();
}, [portal]);
async function submit() {
if (!captcha) return;
setLoading(true);
setError('');
try {
const session = await (isAdmin ? adminApi.login : clientApi.login)({
login,
password,
captchaId: captcha.captchaId,
captchaText,
});
writeSession(session);
navigate(isAdmin ? '/admin' : '/client', { replace: true });
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败');
await refreshCaptcha();
} finally {
setLoading(false);
}
}
return (
<main className="login-page">
<section className="login-panel">
<div className="login-brand">
<span><ShieldCheck size={28} /></span>
<div>
<h1>{isAdmin ? 'CMPP 运营端' : 'CMPP 客户端'}</h1>
<p>{isAdmin ? '平台管理员登录' : '企业管理员登录'}</p>
</div>
</div>
<div className="login-form">
<Input label="邮箱或手机号" onChange={(event) => setLogin(event.target.value)} placeholder="请输入邮箱或手机号" value={login} />
<Input label="密码" onChange={(event) => setPassword(event.target.value)} placeholder="请输入密码" type="password" value={password} />
<div className="login-captcha-row">
<Input label="图形验证码" onChange={(event) => setCaptchaText(event.target.value)} placeholder="请输入计算结果" value={captchaText} />
<button className="login-captcha" onClick={() => void refreshCaptcha()} type="button">
{captcha?.challenge ?? '刷新'}
</button>
</div>
{error ? <p className="login-error">{error}</p> : null}
<Button disabled={loading} onClick={submit}>{loading ? '登录中...' : '登录'}</Button>
</div>
</section>
</main>
);
}