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