/** * 认证表单岛:登录 / 注册 / 找回 / 重置 / 改密,按 `mode` 切换。 * 默认登录方式:用户名 + 密码。 * Provider 包在组件内部。 */ import { useState } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { trpc, createTRPCReactClient } from "../lib/trpc"; import { go } from "../lib/nav"; import type { Locale } from "@app/i18n"; import { t } from "@app/i18n"; import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, toast, } from "@app/ui"; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false }, }, }); const trpcClient = createTRPCReactClient(); export type AuthFormMode = "login" | "register" | "forgot" | "reset" | "change"; export interface AuthFormProps { locale: Locale; mode: AuthFormMode; /** 登录成功后的跳转地址 */ next?: string | undefined; /** 重置页预填令牌 */ defaultToken?: string | undefined; } export default function AuthForm(props: AuthFormProps) { return ( ); } function AuthFormInner({ locale, mode, next, defaultToken }: AuthFormProps) { const [username, setUsername] = useState(""); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [token, setToken] = useState(defaultToken ?? ""); const [resetToken, setResetToken] = useState(null); const login = trpc.auth.login.useMutation({ onSuccess: () => { toast.success(t(locale, "auth.msg.loginOk")); go(next?.startsWith("/") ? next : home(locale)); }, onError: (err) => toast.error(err.message || t(locale, "auth.err.generic")), }); const register = trpc.auth.register.useMutation({ onSuccess: () => { toast.success(t(locale, "auth.msg.registerOk")); go(next?.startsWith("/") ? next : home(locale)); }, onError: (err) => toast.error(err.message || t(locale, "auth.err.generic")), }); const requestReset = trpc.auth.requestPasswordReset.useMutation({ onSuccess: (data) => { toast.success(t(locale, "auth.msg.resetRequested")); if (data.resetToken) { setResetToken(data.resetToken); setToken(data.resetToken); } }, onError: (err) => toast.error(err.message || t(locale, "auth.err.generic")), }); const resetPassword = trpc.auth.resetPassword.useMutation({ onSuccess: () => { toast.success(t(locale, "auth.msg.resetOk")); go(`${home(locale)}/auth/login`); }, onError: (err) => toast.error(err.message || t(locale, "auth.err.generic")), }); const changePassword = trpc.auth.changePassword.useMutation({ onSuccess: () => { toast.success(t(locale, "auth.msg.passwordOk")); setCurrentPassword(""); setNewPassword(""); }, onError: (err) => toast.error(err.message || t(locale, "auth.err.generic")), }); const busy = login.isPending || register.isPending || requestReset.isPending || resetPassword.isPending || changePassword.isPending; function onSubmit(e: React.FormEvent) { e.preventDefault(); if (busy) return; if (mode === "login") { login.mutate({ username, password }); } else if (mode === "register") { register.mutate({ username, password, name: name.trim() || undefined, email: email.trim() || undefined, }); } else if (mode === "forgot") { requestReset.mutate({ email }); } else if (mode === "reset") { resetPassword.mutate({ token, newPassword }); } else if (mode === "change") { changePassword.mutate({ currentPassword, newPassword }); } } const titles: Record = { login: { title: t(locale, "auth.login.heading"), desc: t(locale, "auth.login.subtitle"), }, register: { title: t(locale, "auth.register.heading"), desc: t(locale, "auth.register.subtitle"), }, forgot: { title: t(locale, "auth.forgot.heading"), desc: t(locale, "auth.forgot.subtitle"), }, reset: { title: t(locale, "auth.reset.heading"), desc: t(locale, "auth.reset.subtitle"), }, change: { title: t(locale, "auth.settings.changeHeading"), desc: t(locale, "auth.settings.hint"), }, }; const homeBase = home(locale); return ( {titles[mode].title} {titles[mode].desc} {(mode === "login" || mode === "register") && ( {t(locale, "auth.field.username")} setUsername(e.target.value)} /> )} {mode === "register" && ( {t(locale, "auth.field.name")} setName(e.target.value)} /> )} {(mode === "register" || mode === "forgot") && ( {mode === "forgot" ? t(locale, "auth.forgot.emailLabel") : t(locale, "auth.field.email")} setEmail(e.target.value)} /> )} {mode === "login" && ( {t(locale, "auth.field.password")} setPassword(e.target.value)} /> )} {mode === "register" && ( {t(locale, "auth.field.password")} setPassword(e.target.value)} /> {t(locale, "auth.passwordHint")} )} {mode === "change" && ( <> {t(locale, "auth.field.currentPassword")} setCurrentPassword(e.target.value)} /> {t(locale, "auth.field.newPassword")} setNewPassword(e.target.value)} /> {t(locale, "auth.passwordHint")} > )} {mode === "reset" && ( <> {t(locale, "auth.field.token")} setToken(e.target.value)} /> {t(locale, "auth.field.newPassword")} setNewPassword(e.target.value)} /> {t(locale, "auth.passwordHint")} > )} {mode === "login" && t(locale, "auth.login.submit")} {mode === "register" && t(locale, "auth.register.submit")} {mode === "forgot" && t(locale, "auth.forgot.submit")} {mode === "reset" && t(locale, "auth.reset.submit")} {mode === "change" && t(locale, "auth.settings.changeSubmit")} {mode === "forgot" && resetToken && ( {t(locale, "auth.forgot.tokenHint")} {resetToken} {t(locale, "auth.forgot.toReset")} )} {mode === "login" && ( <> {t(locale, "auth.forgot.title")} {t(locale, "auth.login.noAccount")}{" "} {t(locale, "auth.login.toRegister")} > )} {mode === "register" && ( {t(locale, "auth.register.hasAccount")}{" "} {t(locale, "auth.register.toLogin")} )} {(mode === "forgot" || mode === "reset") && ( {t(locale, "auth.forgot.toLogin")} )} ); } function home(locale: Locale): string { return locale === "zh-CN" ? "/zh-CN" : "/en"; }
{t(locale, "auth.passwordHint")}
{t(locale, "auth.forgot.tokenHint")}
{resetToken}