From ec4b3e0f222b990a8c9ac0ce1237ba1a9aa62510 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 13 Jul 2026 15:53:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E6=B3=A8=E5=86=8C=E5=8D=A1?= =?UTF-8?q?=E7=89=87=20async=20=E5=9B=9E=E8=B0=83=E5=8A=A0=E5=8D=B8?= =?UTF-8?q?=E8=BD=BD=E5=AE=88=E5=8D=AB(I8);=E6=A0=B8=E5=AE=9E=20I7=20res.t?= =?UTF-8?q?ext()=20=E5=B7=B2=E5=9C=A8=20try=20=E5=86=85(=E6=97=A0=E9=9C=80?= =?UTF-8?q?=E6=94=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RegisterCard.jsx onSendCode/onRegister 里 await callApi 期间组件若已卸载(用户导航离开 注册页),之后的 setState 会打到已卸载组件——加 mountedRef 守卫,对齐文件内既有 cooldownRef/redirectRef 的 unmount 清理写法。InviteCard.jsx onCopy 的 1.5s setTimeout 从未 clear,存 ref 并在卸载时 clear。 I7(callApi 里 res.text()/res.json() 读 body 阶段断连,声称在 try 之外)经核实: callApi 自创建(65ce266)起,res.text()+JSON.parse 就已在独立 try/catch 内(279-286行), 断连会被捕获、data 置 null,不会产生 unhandled rejection;未做改动。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu --- web/website/src/components/InviteCard.jsx | 10 ++++++++-- web/website/src/components/RegisterCard.jsx | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) 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);