mosaicmap/app/login/login-form.tsx
2025-08-18 18:47:30 +08:00

260 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { z } from "zod"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { useUser } from "../user-context"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { GalleryVerticalEnd } from "lucide-react"
const schema = z.object({
username: z.string().min(1, "用户名不能为空"),
password: z.string().min(1, "密码不能为空"),
})
export function LoginForm({
className,
...props
}: React.ComponentProps<"div">) {
const { login } = useUser();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [step, setStep] = useState<'username' | 'password'>('username');
const [username, setUsername] = useState('');
const usernameForm = useForm<{ username: string }>({
resolver: zodResolver(z.object({ username: z.string().min(1, "用户名不能为空") })),
defaultValues: { username: "" },
});
const passwordForm = useForm<{ username: string; password: string }>({
resolver: zodResolver(z.object({
username: z.string(),
password: z.string().min(1, "密码不能为空")
})),
defaultValues: { username: "", password: "" },
});
async function onUsernameSubmit(values: { username: string }) {
setUsername(values.username);
setStep('password');
setError(null);
// 设置密码表单的用户名字段
passwordForm.setValue('username', values.username);
}
async function onPasswordSubmit(values: { username: string; password: string }) {
try {
setIsLoading(true);
setError(null);
await login({ username, password: values.password });
router.push('/');
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败,请重试');
} finally {
setIsLoading(false);
}
}
function goBackToUsername() {
setStep('username');
setError(null);
passwordForm.reset();
}
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
{step === 'username' ? (
<Form {...usernameForm}>
<form onSubmit={usernameForm.handleSubmit(onUsernameSubmit)}>
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2">
<a
href="#"
className="flex flex-col items-center gap-2 font-medium"
>
<div className="flex size-8 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-6" />
</div>
<span className="sr-only">Acme Inc.</span>
</a>
<h1 className="text-xl font-bold"> Acme Inc.</h1>
<div className="text-center text-sm">
{" "}
<a href="#" className="underline underline-offset-4">
</a>
</div>
</div>
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
{error}
</div>
)}
<div className="flex flex-col gap-6">
<div className="grid gap-3">
<FormField
control={usernameForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="username"></FormLabel>
<FormControl>
<Input
id="username"
type="text"
placeholder="请输入用户名"
{...field}
required
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" className="w-full">
</Button>
</div>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-background text-muted-foreground relative z-10 px-2">
</span>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<Button variant="outline" type="button" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
Continue with Apple
</Button>
<Button variant="outline" type="button" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
Continue with Google
</Button>
</div>
</div>
</form>
</Form>
) : (
<Form {...passwordForm}>
<form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}>
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2">
<a
href="#"
className="flex flex-col items-center gap-2 font-medium"
>
<div className="flex size-8 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-6" />
</div>
<span className="sr-only">Acme Inc.</span>
</a>
<h1 className="text-xl font-bold"></h1>
<div className="text-center text-sm">
<span className="font-medium">{username}</span>
</div>
</div>
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
{error}
</div>
)}
<div className="flex flex-col gap-6">
<div className="grid gap-3">
<FormField
control={passwordForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="username"></FormLabel>
<FormControl>
<Input
id="username"
type="text"
{...field}
readOnly
className="bg-muted cursor-not-allowed"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid gap-3">
<FormField
control={passwordForm.control}
name="password"
disabled={isLoading}
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="password"></FormLabel>
<FormControl>
<Input
id="password"
type="password"
placeholder="请输入密码"
{...field}
required
autoFocus
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "登录中..." : "登录"}
</Button>
</div>
<div className="text-center">
<Button
type="button"
variant="ghost"
onClick={goBackToUsername}
className="text-sm underline-offset-4 hover:underline"
>
</Button>
</div>
</div>
</form>
</Form>
)}
<div className="text-muted-foreground text-center text-xs text-balance">
By clicking continue, you agree to our{" "}
<a href="#" className="underline underline-offset-4 hover:text-primary">
Terms of Service
</a>{" "}
and{" "}
<a href="#" className="underline underline-offset-4 hover:text-primary">
Privacy Policy
</a>.
</div>
</div>
)
}