import { type FormEvent, useEffect, useRef, useState, type ReactNode } from "react"; import { App, Button, Checkbox, Divider, Input, Modal } from "antd"; import { ArrowRight, Info, LockKeyhole, Mail, ShieldCheck, TriangleAlert, UserRound } from "lucide-react"; import { useNavigate, useSearchParams } from "react-router"; import { getAuthSession, getAuthSettings, linuxDOLoginURL, register, sendRegistrationEmailCode } from "@/services/api/auth"; import { LinuxDOIcon } from "./auth-scene"; import { ApiError } from "@/services/api/request"; type AuthSettings = Awaited>; // TODO: 由维护者填写正式的影策服务协议条款后再发布。 const SERVICE_AGREEMENT: { title: string; content: string }[] = []; export default function RegisterPage() { const navigate = useNavigate(); const [params] = useSearchParams(); const { message } = App.useApp(); const [settings, setSettings] = useState(null); const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [emailCode, setEmailCode] = useState(""); const [displayName, setDisplayName] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [agreementAccepted, setAgreementAccepted] = useState(false); const [agreementOpen, setAgreementOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const [sendingCode, setSendingCode] = useState(false); const [countdown, setCountdown] = useState(0); const [registerCountdown, setRegisterCountdown] = useState(0); const sending = useRef(false), registering = useRef(false); const next = safeNext(params.get("next")); useEffect(() => { let cancelled = false; void getAuthSettings() .then((value) => !cancelled && setSettings(value)) .catch((error) => !cancelled && message.error(error instanceof Error ? error.message : "读取注册设置失败")); return () => { cancelled = true; }; }, [message]); useEffect(() => { if (countdown <= 0) return; const timer = window.setInterval(() => setCountdown((value) => Math.max(0, value - 1)), 1000); return () => window.clearInterval(timer); }, [countdown]); const sendCode = async () => { if (sending.current || countdown > 0) return; if (!email.trim()) { message.warning("请先输入邮箱"); return; } sending.current = true; setSendingCode(true); try { await sendRegistrationEmailCode(email.trim()); setCountdown(60); message.success("验证码已发送,请检查邮箱"); } catch (error) { if (error instanceof ApiError && error.status === 429) setCountdown(Math.max(1, Math.ceil((error.retryAfterMs ?? 60000) / 1000))); message.error(error instanceof Error ? error.message : "发送验证码失败"); } finally { sending.current = false; setSendingCode(false); } }; const submit = async (event: FormEvent) => { event.preventDefault(); if (registering.current || registerCountdown > 0) return; if (!agreementAccepted) { message.warning("请先同意影策服务协议"); return; } if (password !== confirmPassword) { message.error("两次输入的密码不一致"); return; } registering.current = true; setSubmitting(true); try { await register({ username, email, emailCode, displayName, password, acceptedTerms: agreementAccepted }); const { applyUserSession } = await import("@/lib/user-session"); await applyUserSession(await getAuthSession()); if (!settings?.firstUser) window.sessionStorage.setItem("infinite-canvas:model-setup-guide", "1"); message.success(settings?.firstUser ? "管理员账号已创建" : "注册成功"); navigate(next, { replace: true }); } catch (error) { if (error instanceof ApiError && error.status === 429) setRegisterCountdown(Math.max(1, Math.ceil((error.retryAfterMs ?? 60000) / 1000))); message.error(error instanceof Error ? error.message : "注册失败"); } finally { registering.current = false; setSubmitting(false); } }; useEffect(() => { if (registerCountdown <= 0) return; const timer = window.setInterval(() => setRegisterCountdown((value) => Math.max(0, value - 1)), 1000); return () => window.clearInterval(timer); }, [registerCountdown]); const registrationClosed = settings?.registrationEnabled === false; const mailUnavailable = Boolean(settings && !settings.firstUser && settings.emailCodeRequired && !settings.emailEnabled); const disabled = registrationClosed || mailUnavailable; const requireCode = Boolean(settings && !settings.firstUser && settings.emailCodeRequired); return (
{settings?.firstUser ? ( } tone="blue"> 首个账号自动成为管理员,邮箱验证码暂不要求。 ) : null} {registrationClosed ? ( } tone="amber"> 当前已关闭普通注册,请联系管理员创建账号。 ) : null} {mailUnavailable ? ( } tone="amber"> 管理员尚未配置注册邮件,普通邮箱注册暂不可用。 ) : null}
} value={username} onChange={(event) => setUsername(event.target.value)} placeholder="3-32 位字符" autoComplete="username" required disabled={disabled} /> setDisplayName(event.target.value)} placeholder="不填则使用用户名" disabled={disabled} />
} value={email} onChange={(event) => setEmail(event.target.value)} placeholder="用于登录与安全验证" autoComplete="email" required={!settings?.firstUser} disabled={disabled} /> {requireCode ? (
} value={emailCode} onChange={(event) => setEmailCode(event.target.value.replace(/\D/g, "").slice(0, 6))} placeholder="6 位验证码" inputMode="numeric" autoComplete="one-time-code" required disabled={disabled} />
) : null}
} value={password} onChange={(event) => setPassword(event.target.value)} placeholder="至少 8 位" autoComplete="new-password" required disabled={disabled} /> } value={confirmPassword} onChange={(event) => setConfirmPassword(event.target.value)} placeholder="再次输入密码" autoComplete="new-password" required disabled={disabled} />
setAgreementAccepted(event.target.checked)}> 我已阅读并同意
{settings?.linuxdoEnabled ? ( <> 或 ) : null} setAgreementOpen(false)} footer={null} destroyOnHidden>
{SERVICE_AGREEMENT.length === 0 ?

服务协议内容待补充。

: null} {SERVICE_AGREEMENT.map((item) => (

{item.title}

{item.content}

))}
); } function AuthField({ label, children }: { label: string; children: ReactNode }) { return ( ); } function Notice({ icon, tone, children }: { icon: ReactNode; tone: "blue" | "amber"; children: ReactNode }) { return (
{icon} {children}
); } function safeNext(value: string | null) { if (!value || !value.startsWith("/") || value.startsWith("//")) return "/"; return value; }