diff --git a/web/website/src/components/InviteCard.jsx b/web/website/src/components/InviteCard.jsx index ea76cf4..a39f052 100644 --- a/web/website/src/components/InviteCard.jsx +++ b/web/website/src/components/InviteCard.jsx @@ -18,7 +18,7 @@ * ——与主页行为一致,用户可用 Header 的语言下拉自行切换(切换会带上当前的邀请码路径,见 * Header.jsx 的 `pagePath` 机制)。 */ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Copy, Check, Download, UserPlus, Gift, ArrowRight } from 'lucide-react'; import { isLoggedIn } from '../lib/authState'; import { INVITE_CODE_RE } from '../lib/inviteCode'; @@ -163,12 +163,17 @@ export default function InviteCard({ lang = 'en' }) { const [code, setCode] = useState(null); const [copied, setCopied] = useState(false); const [loggedIn, setLoggedIn] = useState(false); + // onCopy 里 1.5s 后把 copied 复位的 setTimeout 存 ref,卸载时 clear—— + // 否则用户复制后立刻离开页面,定时器仍会在已卸载组件上触发 setState。 + const copyTimeoutRef = useRef(null); useEffect(() => { setCode(extractCode(window.location.pathname)); setLoggedIn(isLoggedIn()); }, []); + useEffect(() => () => clearTimeout(copyTimeoutRef.current), []); + const t = COPY[lang] || COPY.en; const dlHref = useMemo(() => `${langPath(lang)}#download`, [lang]); const registerHref = useMemo( @@ -181,7 +186,8 @@ export default function InviteCard({ lang = 'en' }) { try { await navigator.clipboard.writeText(code); setCopied(true); - setTimeout(() => setCopied(false), 1500); + clearTimeout(copyTimeoutRef.current); + copyTimeoutRef.current = setTimeout(() => setCopied(false), 1500); } catch { // 剪贴板权限失败时静默,用户仍可长按/选中手动复制。 } diff --git a/web/website/src/components/RegisterCard.jsx b/web/website/src/components/RegisterCard.jsx index 41d742a..77f4d60 100644 --- a/web/website/src/components/RegisterCard.jsx +++ b/web/website/src/components/RegisterCard.jsx @@ -311,6 +311,9 @@ export default function RegisterCard({ lang = 'en' }) { const [redirectIn, setRedirectIn] = useState(REDIRECT_SECS); const redirectRef = useRef(null); const codeInputRef = useRef(null); + // 卸载守卫:onSendCode/onRegister 里 await callApi 期间组件若已卸载(用户导航离开), + // 之后的 setState 直接跳过——避免 unmounted 组件 setState 的告警/竞态。 + const mountedRef = useRef(true); const t = COPY[lang] || COPY.en; // 后端错误字段只有 message_zh/message_en 两语;非中文一律取英文兜底(见组件顶部注释)。 @@ -334,6 +337,7 @@ export default function RegisterCard({ lang = 'en' }) { useEffect(() => () => clearInterval(cooldownRef.current), []); useEffect(() => () => clearInterval(redirectRef.current), []); + useEffect(() => () => { mountedRef.current = false; }, []); useEffect(() => { if (codeSent && codeInputRef.current) codeInputRef.current.focus(); @@ -381,6 +385,7 @@ export default function RegisterCard({ lang = 'en' }) { } setSendingCode(true); const r = await callApi('/v1/auth/code', { email: trimmed }); + if (!mountedRef.current) return; setSendingCode(false); if (!r.ok) { setCodeErr(r.network ? t.netErr : (r.error && r.error[msgKey]) || t.netErr); @@ -403,6 +408,7 @@ export default function RegisterCard({ lang = 'en' }) { const body = { email: trimmedEmail, code, password }; if (invite) body.invite_code = invite; const r = await callApi('/v1/auth/register', body); + if (!mountedRef.current) return; setSubmitting(false); if (!r.ok) { setRegErr(r.network ? t.netErr : (r.error && r.error[msgKey]) || t.netErr);