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