feat(auth): 优化登录注册页面与人机验证展示

- 调整登录、注册页布局为更一致的卡片式结构,提升表单可读性与视觉层级。
- 为登录/注册按钮与状态区补充图标和文案,优化交互反馈。
- 让 Turnstile 验证组件支持自适应宽度并使用更合适的展示尺寸。
This commit is contained in:
LanQin_
2026-06-17 10:35:56 +08:00
parent d008638046
commit f0044e477c
3 changed files with 114 additions and 87 deletions
+42 -31
View File
@@ -1,6 +1,7 @@
import * as React from "react"
import { Link, Navigate } from "react-router-dom"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { ArrowRight, KeyRound, LockKeyhole } from "lucide-react"
import { api } from "@/lib/api"
import { useMe } from "@/hooks/use-me"
import { TurnstileBox } from "@/components/turnstile-box"
@@ -34,42 +35,52 @@ export function LoginPage() {
const turnstileRequired = !!publicSettings.data?.turnstileEnabled
if (me.data?.user) return <Navigate to="/" replace />
return (
<div className="grid min-h-screen place-items-center bg-background px-4">
<div className="w-full max-w-[360px]">
<div className="mb-10 text-center">
<h1 className="text-3xl font-bold tracking-tight">LanQin Email</h1>
<div className="flex min-h-screen items-center justify-center bg-muted/20 px-4 py-10">
<div className="w-full max-w-[420px]">
<div className="mb-7 text-center">
<h1 className="text-3xl font-semibold tracking-tight">LanQin Email</h1>
</div>
<form className="space-y-5" onSubmit={(e) => { e.preventDefault(); if (!challengeToken && turnstileRequired && !turnstileToken) { toast({ title: "请先完成人机验证" }); return }; login.mutate(new FormData(e.currentTarget)) }}>
{!challengeToken ? (
<>
<div className="rounded-lg border bg-background p-6 shadow-sm sm:p-7">
<div className="mb-6 flex items-center gap-2 text-sm font-medium text-muted-foreground">
{challengeToken ? <KeyRound className="h-4 w-4" /> : <LockKeyhole className="h-4 w-4" />}
{challengeToken ? "双因素验证" : "账号登录"}
</div>
<form className="space-y-5" onSubmit={(e) => { e.preventDefault(); if (!challengeToken && turnstileRequired && !turnstileToken) { toast({ title: "请先完成人机验证" }); return }; login.mutate(new FormData(e.currentTarget)) }}>
{!challengeToken ? (
<>
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium"></Label>
<Input id="email" name="email" type="email" autoComplete="username" required className="h-11 text-base" />
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm font-medium"></Label>
<PasswordInput id="password" name="password" autoComplete="current-password" required className="h-11 text-base" />
</div>
</>
) : (
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input id="email" name="email" type="email" autoComplete="username" required className="h-11 text-base" />
<Label htmlFor="twoFactorCode" className="text-sm font-medium"></Label>
<Input id="twoFactorCode" name="twoFactorCode" inputMode="numeric" autoComplete="one-time-code" minLength={6} maxLength={6} required className="h-11 text-center text-lg tracking-[0.35em]" />
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<PasswordInput id="password" name="password" autoComplete="current-password" required className="h-11 text-base" />
</div>
</>
) : (
<div className="space-y-2">
<Label htmlFor="twoFactorCode"></Label>
<Input id="twoFactorCode" name="twoFactorCode" inputMode="numeric" autoComplete="one-time-code" minLength={6} maxLength={6} required className="h-11 text-base" />
</div>
)}
{!challengeToken && turnstileRequired && (
<TurnstileBox siteKey={publicSettings.data?.turnstileSiteKey || ""} onToken={setTurnstileToken} />
)}
<Button className="h-11 w-full text-base" disabled={login.isPending}>
{login.isPending ? "登录中..." : challengeToken ? "验证登录" : "登录"}
</Button>
{challengeToken && <Button type="button" variant="ghost" className="w-full" onClick={() => setChallengeToken("")}></Button>}
{!challengeToken && (
<Button type="button" variant="ghost" className="w-full" asChild>
)}
{!challengeToken && turnstileRequired && (
<TurnstileBox siteKey={publicSettings.data?.turnstileSiteKey || ""} onToken={setTurnstileToken} />
)}
<Button className="h-11 w-full text-base" disabled={login.isPending}>
{login.isPending ? "登录中..." : challengeToken ? "验证登录" : "登录"}
{!login.isPending && <ArrowRight className="h-4 w-4" />}
</Button>
{challengeToken && <Button type="button" variant="ghost" className="w-full" onClick={() => setChallengeToken("")}></Button>}
</form>
</div>
{!challengeToken && (
<div className="mt-5 flex items-center justify-center gap-2 text-sm text-muted-foreground">
<span></span>
<Button type="button" variant="link" className="h-auto px-0 text-sm" asChild>
<Link to="/register"></Link>
</Button>
)}
</form>
</div>
)}
</div>
</div>
)