Files
juwan-frontend/app/(auth)/register/page.tsx
T
2026-02-28 15:45:05 +08:00

228 lines
7.6 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 { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { IconInput } from "@/components/ui/icon-input"
import { Label } from "@/components/ui/label"
import { register as registerApi, sendEmailVerificationCode } from "@/lib/api"
import { toApiError } from "@/lib/errors"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import { useAuthStore } from "@/store/auth"
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
import { Eye, EyeOff, Lock, Mail, Shield, User } from "lucide-react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
const registerSchema = z
.object({
username: z.string().min(1, "请输入用户名"),
email: z.string().email("请输入正确的邮箱地址"),
vcode: z.string().optional(),
password: z.string().min(6, "密码至少6位"),
confirmPassword: z.string(),
agreeTerms: z.boolean(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "两次密码不一致",
path: ["confirmPassword"],
})
.refine((data) => data.agreeTerms, {
message: "请同意用户协议和隐私政策",
path: ["agreeTerms"],
})
export default function RegisterPage() {
const router = useRouter()
const { login: storeLogin } = useAuthStore()
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [countdown, setCountdown] = useState(0)
const {
register,
handleSubmit,
control,
getValues,
trigger,
formState: { errors, isSubmitting },
} = useForm({
resolver: standardSchemaResolver(registerSchema),
defaultValues: { agreeTerms: false },
})
useEffect(() => {
if (countdown <= 0) return
const timer = setInterval(() => setCountdown((c) => c - 1), 1000)
return () => clearInterval(timer)
}, [countdown])
const handleSendCode = async () => {
const isValid = await trigger("email")
if (!isValid) return
const email = String(getValues("email") ?? "")
try {
await sendEmailVerificationCode({ email, scene: "register" })
setCountdown(60)
notifySuccess("验证码已发送到你的邮箱")
} catch (err) {
notifyInfo(toApiError(err).msg)
}
}
const onSubmit = async (data: z.infer<typeof registerSchema>) => {
try {
const user = await registerApi({
username: data.username,
email: data.email,
password: data.password,
vcode: data.vcode,
})
storeLogin(user, ["consumer"])
router.push("/")
} catch (err) {
notifyInfo(toApiError(err).msg)
}
}
return (
<>
<div className="mb-8">
<h2 className="text-2xl font-bold"></h2>
<p className="mt-2 text-sm text-muted-foreground"></p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="username"></Label>
<IconInput
id="username"
icon={<User />}
placeholder="输入用户名"
{...register("username")}
/>
{errors.username && <p className="text-xs text-destructive">{errors.username.message}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="email"></Label>
<IconInput id="email" icon={<Mail />} placeholder="输入邮箱地址" {...register("email")} />
{errors.email && <p className="text-xs text-destructive">{errors.email.message}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="vcode"></Label>
<div className="flex gap-2">
<div className="flex-1">
<IconInput
id="vcode"
icon={<Shield />}
placeholder="输入验证码(可选)"
{...register("vcode")}
/>
</div>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={countdown > 0}
className="w-[110px]"
>
{countdown > 0 ? `${countdown}s` : "发送验证码"}
</Button>
</div>
{errors.vcode && <p className="text-xs text-destructive">{errors.vcode.message}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="password"></Label>
<IconInput
id="password"
icon={<Lock />}
type={showPassword ? "text" : "password"}
placeholder="设置密码 (至少6位)"
rightElement={
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-muted-foreground hover:text-foreground"
tabIndex={-1}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
}
{...register("password")}
/>
{errors.password && <p className="text-xs text-destructive">{errors.password.message}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="confirmPassword"></Label>
<IconInput
id="confirmPassword"
icon={<Lock />}
type={showConfirmPassword ? "text" : "password"}
placeholder="再次输入密码"
rightElement={
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="text-muted-foreground hover:text-foreground"
tabIndex={-1}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
}
{...register("confirmPassword")}
/>
{errors.confirmPassword && (
<p className="text-xs text-destructive">{errors.confirmPassword.message}</p>
)}
</div>
<div className="flex items-start gap-2 pt-1">
<Controller
name="agreeTerms"
control={control}
render={({ field }) => (
<Checkbox
id="terms"
checked={field.value}
onCheckedChange={(v) => field.onChange(v === true)}
className="mt-0.5"
/>
)}
/>
<label htmlFor="terms" className="text-xs leading-relaxed text-muted-foreground">
{" "}
<Link href="/terms" className="text-primary hover:underline">
</Link>{" "}
{" "}
<Link href="/privacy" className="text-primary hover:underline">
</Link>
</label>
</div>
{errors.agreeTerms && (
<p className="text-xs text-destructive">{errors.agreeTerms.message}</p>
)}
<Button type="submit" className="mt-2 w-full" size="lg" disabled={isSubmitting}>
{isSubmitting ? "注册中..." : "注册"}
</Button>
</form>
<p className="mt-8 text-center text-sm text-muted-foreground">
{" "}
<Link href="/login" className="font-medium text-primary hover:underline">
</Link>
</p>
</>
)
}