Files
jiang13-bbs/frontend/app/register/RegisterClient.tsx
freefire 46e0cdc0b3 feat: 媒体库 blob 去重存储与品牌/注册等前端调整
新增:
- blob_store 二进制去重存储 + blob_dedup 测试

其它:
- 后端 settings/auth/media_library/upload/operations 服务与 handler 调整
- 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
2026-10-03 02:38:11 +08:00

257 lines
8.0 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 { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { UserPlus } from "lucide-react";
import { apiRegister, apiMe, apiOperations } from "@/lib/api";
import { hasAuthCookieHint } from "@/lib/cookies";
/** 关闭注册时的说明页(提交失败切到关闭态时复用) */
export function RegisterClosedPanel({ notice }: { notice: string }) {
return (
<div className="max-w-md mx-auto pt-8 sm:pt-14">
<div className="panel p-8 text-center">
<h1 className="text-lg font-extrabold" style={{ color: "var(--ink)" }}>
暂不开放注册
</h1>
<p className="meta mt-2 text-[13px]">
{notice || "站点当前已关闭新用户注册,已有账号可直接登录。"}
</p>
<Link href="/login" className="btn btn-primary mt-6">
去登录
</Link>
</div>
</div>
);
}
export default function RegisterClient({
verifyEmail: initialVerifyEmail,
registerNotice,
}: {
verifyEmail: boolean;
registerNotice: string;
}) {
const router = useRouter();
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [registerClosed, setRegisterClosed] = useState(false);
const [code, setCode] = useState("");
const [notice, setNotice] = useState(registerNotice);
const [sending, setSending] = useState(false);
const [sentMessage, setSentMessage] = useState("");
// 重发倒计时(秒);>0 期间按钮锁定不可点
const [countdown, setCountdown] = useState(0);
const countdownTimer = useRef<ReturnType<typeof setInterval> | null>(null);
const verifyEmail = initialVerifyEmail;
// 卸载时清理倒计时,避免泄漏
useEffect(() => {
return () => {
if (countdownTimer.current) clearInterval(countdownTimer.current);
};
}, []);
const startCountdown = () => {
if (countdownTimer.current) clearInterval(countdownTimer.current);
setCountdown(60);
countdownTimer.current = setInterval(() => {
setCountdown((c) => {
if (c <= 1) {
if (countdownTimer.current) clearInterval(countdownTimer.current);
countdownTimer.current = null;
return 0;
}
return c - 1;
});
}, 1000);
};
// 已登录用户不必再注册,直接回首页
useEffect(() => {
if (!hasAuthCookieHint()) return;
apiMe()
.then((res) => {
if (res.user) router.replace("/");
})
.catch(() => {});
}, [router]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await apiRegister(username, email, password, code);
if (res.user) {
router.push("/");
router.refresh();
} else if (res.id) {
router.push("/login");
} else {
const msg = res.error || "注册失败";
if (msg.includes("关闭注册")) {
// 热关闭:补拉提示文案后切关闭态
try {
const s = await apiOperations<{ register_notice: string }>("/api/site-state");
setNotice(s.register_notice || notice);
} catch {
/* 沿用 SSR 提示 */
}
setRegisterClosed(true);
}
setError(msg);
}
} finally {
setLoading(false);
}
};
if (registerClosed) {
return <RegisterClosedPanel notice={notice} />;
}
return (
<div className="max-w-md mx-auto pt-8 sm:pt-14">
<div className="mb-7 text-center">
<span
className="inline-flex w-14 h-14 rounded-2xl items-center justify-center mb-4"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
aria-hidden
>
<UserPlus size={26} />
</span>
<h1 className="text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
创建账号
</h1>
<p className="text-sm mt-2" style={{ color: "var(--ink-2)" }}>
加入社区,开始你的第一帖
</p>
</div>
<form onSubmit={handleSubmit} className="panel p-6 sm:p-7 space-y-4">
{error && (
<div className="alert-error" role="alert">
{error}
</div>
)}
<div>
<label className="field-label" htmlFor="reg-username">
用户名
</label>
<input
id="reg-username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
minLength={3}
maxLength={32}
autoFocus
autoComplete="username"
className="field"
placeholder="3–32 位"
/>
</div>
<div>
<label className="field-label" htmlFor="reg-email">
{verifyEmail ? "邮箱(必填)" : "邮箱(可选)"}
</label>
<input
id="reg-email"
required={verifyEmail}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
className="field"
placeholder="you@example.com"
/>
</div>
{verifyEmail && (
<div>
<label className="field-label" htmlFor="reg-code">
邮箱验证码
</label>
<div className="flex gap-2 items-stretch">
<input
id="reg-code"
className="field min-w-0 flex-1"
value={code}
onChange={(e) => setCode(e.target.value)}
required
autoComplete="one-time-code"
inputMode="numeric"
placeholder="6 位验证码"
/>
<button
type="button"
className="btn btn-line shrink-0"
disabled={sending || countdown > 0 || !email}
onClick={async () => {
// 点击即锁定,防止重复发送
setSending(true);
setError("");
try {
const r = await apiOperations<{ message: string }>("/api/auth/code", "POST", {
email,
purpose: "register",
});
setSentMessage(r.message);
startCountdown();
} catch (err) {
setError((err as Error).message);
} finally {
setSending(false);
}
}}
>
{sending
? "正在排队…"
: countdown > 0
? `${countdown}s 后可重发`
: "发送验证码"}
</button>
</div>
{sentMessage && (
<p className="meta mt-2" role="status">
{sentMessage}
</p>
)}
</div>
)}
<div>
<label className="field-label" htmlFor="reg-password">
密码
</label>
<input
id="reg-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
autoComplete="new-password"
className="field"
placeholder="至少 6 位"
/>
</div>
<button type="submit" disabled={loading} className="btn btn-primary w-full justify-center mt-2">
{loading ? "注册中..." : "注册"}
</button>
</form>
<p className="meta text-center mt-5">
已有账号?
<Link href="/login" className="ml-1 font-semibold" style={{ color: "var(--accent)" }}>
去登录
</Link>
</p>
</div>
);
}