229 lines
7.4 KiB
TypeScript
229 lines
7.4 KiB
TypeScript
"use client";
|
||
|
||
import { useState, type FormEvent } from "react";
|
||
import { Copyable } from "../Copyable";
|
||
import styles from "../vip.module.css";
|
||
|
||
type WelcomeBlockProps = {
|
||
userName: string | null;
|
||
};
|
||
|
||
function getStorage(key: string): string | null {
|
||
if (typeof window === "undefined") return null;
|
||
return localStorage.getItem(key);
|
||
}
|
||
|
||
/** 匿名 user_id 格式:user + 数字 */
|
||
function isAnonymousUserId(val: string | null): boolean {
|
||
return !!val && /^user\d+$/.test(val);
|
||
}
|
||
|
||
export function WelcomeBlock({ userName }: WelcomeBlockProps) {
|
||
const displayName = userName || getStorage("userName") || "会员";
|
||
const userId = getStorage("userId");
|
||
const needLinkEmail = isAnonymousUserId(userId);
|
||
|
||
const [email, setEmail] = useState("");
|
||
const [password, setPassword] = useState("");
|
||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [linked, setLinked] = useState(false);
|
||
|
||
const handleLinkSubmit = async (e: FormEvent) => {
|
||
e.preventDefault();
|
||
setError(null);
|
||
setLoading(true);
|
||
try {
|
||
if (password !== passwordConfirm) {
|
||
setError("两次密码不一致");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
if (password.length < 8) {
|
||
setError("密码至少 8 位");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
const res = await fetch("/api/auth/link-email", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
email: email.trim(),
|
||
password,
|
||
anonymousUserId: userId,
|
||
}),
|
||
});
|
||
const data = await res.json();
|
||
if (!data?.ok) {
|
||
setError(data?.error || "操作失败");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
|
||
pbSaveAuth(data.token, { id: data.record.id, email: data.record.email });
|
||
await fetch("/api/auth/sync-session", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ token: data.token, record: data.record }),
|
||
credentials: "include",
|
||
});
|
||
if (typeof window !== "undefined") {
|
||
localStorage.setItem("userId", data.record.id);
|
||
localStorage.setItem("userName", data.record.email || data.record.id);
|
||
}
|
||
setLinked(true);
|
||
window.location.reload();
|
||
} catch {
|
||
setError("操作失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (needLinkEmail && !linked) {
|
||
return (
|
||
<section className={styles.section} style={{ paddingTop: "1.5rem" }}>
|
||
<h1
|
||
className="animate__animated animate__fadeInDown"
|
||
style={{
|
||
fontSize: "clamp(1.5rem, 4vw, 2rem)",
|
||
fontWeight: 600,
|
||
margin: 0,
|
||
animationDelay: "0.1s",
|
||
animationFillMode: "both",
|
||
}}
|
||
>
|
||
欢迎加入,请补充邮箱
|
||
</h1>
|
||
<p
|
||
className="animate__animated animate__fadeInDown"
|
||
style={{
|
||
fontSize: "0.95rem",
|
||
color: "var(--muted-foreground)",
|
||
margin: "0.25rem 0 1rem",
|
||
animationDelay: "0.15s",
|
||
animationFillMode: "both",
|
||
}}
|
||
>
|
||
补充邮箱后可在各站点通用登录,Memos 账号密码为 12345678
|
||
</p>
|
||
<form onSubmit={handleLinkSubmit} className={styles.loginCard} style={{ maxWidth: "400px" }}>
|
||
<input
|
||
type="email"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
placeholder="your@email.com"
|
||
required
|
||
disabled={loading}
|
||
style={{
|
||
width: "100%",
|
||
padding: "0.75rem 1rem",
|
||
fontSize: "1rem",
|
||
border: "1px solid var(--border)",
|
||
borderRadius: "8px",
|
||
background: "var(--background)",
|
||
color: "var(--foreground)",
|
||
marginBottom: "0.75rem",
|
||
}}
|
||
/>
|
||
<input
|
||
type="password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
placeholder="至少 8 位"
|
||
required
|
||
minLength={8}
|
||
disabled={loading}
|
||
style={{
|
||
width: "100%",
|
||
padding: "0.75rem 1rem",
|
||
fontSize: "1rem",
|
||
border: "1px solid var(--border)",
|
||
borderRadius: "8px",
|
||
background: "var(--background)",
|
||
color: "var(--foreground)",
|
||
marginBottom: "0.75rem",
|
||
}}
|
||
/>
|
||
<input
|
||
type="password"
|
||
value={passwordConfirm}
|
||
onChange={(e) => setPasswordConfirm(e.target.value)}
|
||
placeholder="再次输入密码"
|
||
required
|
||
minLength={8}
|
||
disabled={loading}
|
||
style={{
|
||
width: "100%",
|
||
padding: "0.75rem 1rem",
|
||
fontSize: "1rem",
|
||
border: "1px solid var(--border)",
|
||
borderRadius: "8px",
|
||
background: "var(--background)",
|
||
color: "var(--foreground)",
|
||
marginBottom: "0.75rem",
|
||
}}
|
||
/>
|
||
{error && (
|
||
<p style={{ fontSize: "0.85rem", color: "#ef4444", marginBottom: "0.75rem" }}>{error}</p>
|
||
)}
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className={`${styles.btnPrimary} ${styles.btnLarge}`}
|
||
style={{ width: "100%" }}
|
||
>
|
||
{loading ? "处理中…" : "确认并关联"}
|
||
</button>
|
||
</form>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<section className={styles.section} style={{ paddingTop: "1.5rem" }}>
|
||
<h1
|
||
className="animate__animated animate__fadeInDown"
|
||
style={{
|
||
fontSize: "clamp(1.5rem, 4vw, 2rem)",
|
||
fontWeight: 600,
|
||
margin: 0,
|
||
animationDelay: "0.1s",
|
||
animationFillMode: "both",
|
||
}}
|
||
>
|
||
欢迎回来,<Copyable text={displayName}>{displayName}</Copyable>
|
||
</h1>
|
||
<p
|
||
className="animate__animated animate__fadeInDown"
|
||
style={{
|
||
fontSize: "0.95rem",
|
||
color: "var(--muted-foreground)",
|
||
margin: "0.25rem 0 0",
|
||
animationDelay: "0.15s",
|
||
animationFillMode: "both",
|
||
}}
|
||
>
|
||
以下是您的会员资产与推荐动作
|
||
</p>
|
||
<div className={styles.loginCard}>
|
||
<div className={styles.loginCardTitle}>星球登录信息</div>
|
||
<div className={styles.loginCardRow}>
|
||
<span className={styles.loginCardLabel}>网址</span>
|
||
<Copyable text="https://qun.hackrobot.cn">qun.hackrobot.cn</Copyable>
|
||
<a href="https://qun.hackrobot.cn" target="_blank" rel="noopener noreferrer" className={styles.loginCardLink}>打开</a>
|
||
</div>
|
||
<div className={styles.loginCardRow}>
|
||
<span className={styles.loginCardLabel}>账号</span>
|
||
<Copyable text={displayName}><span className={styles.loginCardValue}>{displayName}</span></Copyable>
|
||
</div>
|
||
<div className={styles.loginCardRow}>
|
||
<span className={styles.loginCardLabel}>密码</span>
|
||
<Copyable text="12345678"><span className={styles.loginCardValue}>12345678</span></Copyable>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|