fix: 登录页支持 Token 和账号密码两种方式

- 添加 Tab 切换,同时支持 Token 登录和账号密码登录
- Token 登录兼容原有后端 API
- 账号密码登录为 Phase 3d 的多管理员功能准备

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Wang Zhuoxuan 2026-04-08 15:46:09 +08:00
parent 0a31f8634e
commit 2a58fbcbae

View File

@ -4,40 +4,68 @@ import { useForm } from "react-hook-form"
import { z } from "zod/v4" import { z } from "zod/v4"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Eye, EyeOff } from "lucide-react" import { Eye, EyeOff } from "lucide-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { apiClient } from "@/lib/api-client"
import { loginAdmin } from "@/lib/api/admin-api" import { loginAdmin } from "@/lib/api/admin-api"
import { useAuth } from "@/hooks/use-auth" import { useAuth } from "@/hooks/use-auth"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import type { LoginResponse } from "@/types/api"
import type { AdminSession } from "@/types/admin" import type { AdminSession } from "@/types/admin"
const loginSchema = z.object({ // Token 登录表单
const tokenLoginSchema = z.object({
token: z.string().min(1, "请输入 Admin Token"),
})
type TokenLoginForm = z.infer<typeof tokenLoginSchema>
// 用户名密码登录表单
const passwordLoginSchema = z.object({
username: z.string().min(1, "请输入用户名"), username: z.string().min(1, "请输入用户名"),
password: z.string().min(1, "请输入密码"), password: z.string().min(1, "请输入密码"),
}) })
type LoginForm = z.infer<typeof loginSchema> type PasswordLoginForm = z.infer<typeof passwordLoginSchema>
export default function LoginPage() { export default function LoginPage() {
const navigate = useNavigate() const navigate = useNavigate()
const { login } = useAuth() const { login } = useAuth()
const [error, setError] = useState("") const [error, setError] = useState("")
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
const [loginType, setLoginType] = useState<"token" | "password">("token")
const { // Token 登录表单
register, const tokenForm = useForm<TokenLoginForm>({
handleSubmit, resolver: zodResolver(tokenLoginSchema),
formState: { errors, isSubmitting }, defaultValues: { token: "" },
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
defaultValues: {
username: "",
password: "",
},
}) })
async function onSubmit(data: LoginForm) { // 密码登录表单
const passwordForm = useForm<PasswordLoginForm>({
resolver: zodResolver(passwordLoginSchema),
defaultValues: { username: "", password: "" },
})
// Token 登录
async function handleTokenLogin(data: TokenLoginForm) {
setError("")
try {
const response = await apiClient
.post("auth/login", { json: { token: data.token } })
.json<LoginResponse>()
login(response.jwt, response.admin)
navigate("/")
} catch {
setError("Token 登录失败,请检查是否正确")
}
}
// 密码登录
async function handlePasswordLogin(data: PasswordLoginForm) {
setError("") setError("")
try { try {
const response = await loginAdmin(data) const response = await loginAdmin(data)
@ -62,56 +90,96 @@ export default function LoginPage() {
<Card className="w-full max-w-sm"> <Card className="w-full max-w-sm">
<CardHeader className="text-center"> <CardHeader className="text-center">
<CardTitle className="text-xl"></CardTitle> <CardTitle className="text-xl"></CardTitle>
<CardDescription>使</CardDescription> <CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <Tabs value={loginType} onValueChange={(val) => { setLoginType(val as "token" | "password"); setError("") }}>
<div className="space-y-2"> <TabsList className="grid w-full grid-cols-2">
<Label htmlFor="username"></Label> <TabsTrigger value="token">Token </TabsTrigger>
<Input <TabsTrigger value="password"></TabsTrigger>
id="username" </TabsList>
placeholder="请输入用户名"
autoComplete="username" {/* Token 登录 */}
{...register("username")} <TabsContent value="token" className="mt-4">
/> <form onSubmit={tokenForm.handleSubmit(handleTokenLogin)} className="space-y-4">
{errors.username && ( <div className="space-y-2">
<p className="text-sm text-destructive">{errors.username.message}</p> <Label htmlFor="token">Admin Token</Label>
)} <Input
</div> id="token"
type="password"
placeholder="请输入 Token"
{...tokenForm.register("token")}
/>
{tokenForm.formState.errors.token && (
<p className="text-sm text-destructive">{tokenForm.formState.errors.token.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="请输入密码"
autoComplete="current-password"
{...register("password")}
/>
<Button <Button
type="button" type="submit"
variant="ghost" className="w-full"
size="icon-xs" disabled={tokenForm.formState.isSubmitting}
className="absolute right-1 top-1/2 -translate-y-1/2"
onClick={() => setShowPassword(!showPassword)}
> >
{showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />} {tokenForm.formState.isSubmitting ? "登录中..." : "登录"}
</Button> </Button>
</div> </form>
{errors.password && ( </TabsContent>
<p className="text-sm text-destructive">{errors.password.message}</p>
)}
</div>
{error && ( {/* 密码登录 */}
<p className="text-sm text-destructive">{error}</p> <TabsContent value="password" className="mt-4">
)} <form onSubmit={passwordForm.handleSubmit(handlePasswordLogin)} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username"></Label>
<Input
id="username"
placeholder="请输入用户名"
autoComplete="username"
{...passwordForm.register("username")}
/>
{passwordForm.formState.errors.username && (
<p className="text-sm text-destructive">{passwordForm.formState.errors.username.message}</p>
)}
</div>
<Button type="submit" className="w-full" disabled={isSubmitting}> <div className="space-y-2">
{isSubmitting ? "登录中..." : "登录"} <Label htmlFor="password"></Label>
</Button> <div className="relative">
</form> <Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="请输入密码"
autoComplete="current-password"
{...passwordForm.register("password")}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute right-1 top-1/2 -translate-y-1/2"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</Button>
</div>
{passwordForm.formState.errors.password && (
<p className="text-sm text-destructive">{passwordForm.formState.errors.password.message}</p>
)}
</div>
<Button
type="submit"
className="w-full"
disabled={passwordForm.formState.isSubmitting}
>
{passwordForm.formState.isSubmitting ? "登录中..." : "登录"}
</Button>
</form>
</TabsContent>
</Tabs>
{error && (
<p className="text-sm text-destructive text-center mt-4">{error}</p>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>