feat(web-auth): implement email code login pages

This commit is contained in:
2026-04-05 02:35:55 +08:00
parent 579d63d39d
commit fe4f7909e3
7 changed files with 300 additions and 9 deletions
+128
View File
@@ -0,0 +1,128 @@
import { useMemo, useState } from "react";
import type { FormEvent } from "react";
import { Button } from "@/components/ui/button";
import { loginWithEmailCode, sendEmailCode, type EmailLoginResult } from "@/services/auth-api";
type EmailLoginPageProps = {
onLoginSuccess: (payload: EmailLoginResult) => void;
};
export function EmailLoginPage({ onLoginSuccess }: EmailLoginPageProps) {
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
const [sendingCode, setSendingCode] = useState(false);
const [loggingIn, setLoggingIn] = useState(false);
const [codeCooldown, setCodeCooldown] = useState(0);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const canSendCode = useMemo(() => {
return email.trim().length > 0 && !sendingCode && codeCooldown <= 0;
}, [codeCooldown, email, sendingCode]);
const canLogin = useMemo(() => {
return email.trim().length > 0 && code.trim().length === 6 && !loggingIn;
}, [code, email, loggingIn]);
async function handleSendCode(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
if (!canSendCode) {
return;
}
try {
setSendingCode(true);
setError(null);
setMessage(null);
const result = await sendEmailCode(email.trim());
setMessage(`验证码已发送,有效期 ${result.expiresInSeconds}`);
let remain = 60;
setCodeCooldown(remain);
const timer = window.setInterval(() => {
remain -= 1;
setCodeCooldown(remain);
if (remain <= 0) {
window.clearInterval(timer);
}
}, 1000);
} catch (err) {
setError(err instanceof Error ? err.message : "发送验证码失败");
} finally {
setSendingCode(false);
}
}
async function handleLogin(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
if (!canLogin) {
return;
}
try {
setLoggingIn(true);
setError(null);
setMessage(null);
const result = await loginWithEmailCode(email.trim(), code.trim());
onLoginSuccess(result);
} catch (err) {
setError(err instanceof Error ? err.message : "登录失败");
} finally {
setLoggingIn(false);
}
}
return (
<div className="mx-auto w-full max-w-md rounded-xl border border-[#d7e2db] bg-white p-6 shadow-sm">
<h1 className="text-2xl font-semibold text-[#122117]"></h1>
<p className="mt-2 text-sm text-[#3a5a4a]"></p>
<form className="mt-6 space-y-3" onSubmit={handleSendCode}>
<label className="block text-sm font-medium text-[#244236]" htmlFor="email">
</label>
<input
id="email"
type="email"
className="w-full rounded-md border border-[#bfd0c7] px-3 py-2 text-sm outline-none focus:border-[#0a7a5a]"
placeholder="you@example.com"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<Button type="submit" disabled={!canSendCode} className="w-full">
{sendingCode
? "发送中..."
: codeCooldown > 0
? `${codeCooldown}s 后可重发`
: "发送验证码"}
</Button>
</form>
<form className="mt-4 space-y-3" onSubmit={handleLogin}>
<label className="block text-sm font-medium text-[#244236]" htmlFor="code">
</label>
<input
id="code"
type="text"
inputMode="numeric"
maxLength={6}
className="w-full rounded-md border border-[#bfd0c7] px-3 py-2 text-sm outline-none focus:border-[#0a7a5a]"
placeholder="6位数字验证码"
value={code}
onChange={(event) => setCode(event.target.value)}
/>
<Button
type="submit"
disabled={!canLogin}
className="w-full bg-[#0a7a5a] text-white hover:bg-[#0a7a5a]/90"
>
{loggingIn ? "登录中..." : "立即登录"}
</Button>
</form>
{message ? <p className="mt-4 text-sm text-[#0a7a5a]">{message}</p> : null}
{error ? <p className="mt-2 text-sm text-[#b42318]">{error}</p> : null}
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import type { EmailLoginResult } from "@/services/auth-api";
type TodoShellPageProps = {
session: EmailLoginResult | null;
};
export function TodoShellPage({ session }: TodoShellPageProps) {
return (
<div className="rounded-xl border border-[#d7e2db] bg-white p-6 shadow-sm">
<h1 className="text-2xl font-semibold text-[#122117]">TodoList </h1>
<p className="mt-2 text-sm text-[#3a5a4a]">
{session
? `当前登录用户:${session.user.email}`
: "当前未建立会话,后续提交会补齐会话恢复和路由守卫。"}
</p>
</div>
);
}