From 3790472b4c7638fa2c6cf836e12558f7f241c086 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 16:39:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=88=91=E7=9A=84=E6=94=B6?= =?UTF-8?q?=E8=97=8F/=E6=9F=A5=E6=89=BE=E8=81=8A=E5=A4=A9=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=8A=A0=E8=BD=BD=E5=A4=B1=E8=B4=A5=E6=97=B6=E5=8D=A1?= =?UTF-8?q?=E6=AD=BB=E5=9C=A8"=E5=8A=A0=E8=BD=BD=E4=B8=AD=E2=80=A6"?= =?UTF-8?q?=EF=BC=8C=E6=B2=A1=E6=9C=89=E4=BB=BB=E4=BD=95=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E6=88=96=E9=87=8D=E8=AF=95=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:Favorites 和 History(content-library-view.jsx)两个组件的 load() 里,请求失败的 .catch() 只调用 onToast() 弹一条 2 秒自动消失的提示,从未把 items 从初始值 null 变成别的 状态;页面渲染逻辑是"items === null 就显示'加载中…'",两者叠加导致一次真实失败之后页面永远 停在"加载中…",等用户反应过来去看的时候提示早就消失了,分不清"还在加载"和"永远不会加载完"。 这个坑本文件自己的 DownloadPage、group-management-view.jsx 的 GroupFiles、 contact-management-view.jsx 的 Profile 三处早就各自修过(都有一个独立的 loadError 状态), 唯独 Favorites/History 这两个用得最多的列表页当时漏掉了。 真实触发场景:用两个全新账号互加好友、发送图片/视频/文件/语音/文本等真实消息并收藏后, 发现收藏一条较大的(out-of-band)文件附件之后,GET /favorites?type=all 从此稳定返回 500 INTERNAL_ERROR(已确认是纯后端问题,不在前端能修的范围内,另行记录)——但这个后端 问题一暴露出来,"我的收藏"默认落地的"全部"tab 就会用真实数据 100% 复现这里描述的前端缺口: 打开就卡在"加载中…",没有任何办法知道发生了什么、也无法重试。 修复:Favorites/History 都新增 loadError 状态,非 append(首次/切换 tab、类型、搜索词)请求 失败时置为 true、成功时置回 false,切换 type/topic/view/query 时重置;渲染时 items === null 且 loadError 为真则显示"加载失败,请重试"而不是死等的"加载中…"。"加载更多"(append)请求 失败保持原样只弹 toast,不影响已经显示出来的列表。 验证:复现上述 500 场景(GET /favorites?type=all 持续 500),修复前"我的收藏"全部 tab 永久停在"加载中…";重新构建后同一账号同一坏数据状态下正确显示"加载失败,请重试"。 --- src/views/content-library-view.jsx | 34 ++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/views/content-library-view.jsx b/src/views/content-library-view.jsx index a020c34a..e7233e3e 100644 --- a/src/views/content-library-view.jsx +++ b/src/views/content-library-view.jsx @@ -290,6 +290,16 @@ function Favorites({view, hashParams, tinode, onToast}) { const [items, setItems] = React.useState(() => readCachedData('favorites:' + type) || null); const [cursor, setCursor] = React.useState(''); const [hasMore, setHasMore] = React.useState(false); + // A failed initial load (no cached list to fall back on) previously left this stuck on "加载中…" + // forever -- the catch handler only fired a toast, which fades in 2s, and `items` never left null + // so the loading branch below never had anything else to show. Same fix already applied to this + // file's own DownloadPage/GroupFiles(group-management-view.jsx)/Profile(contact-management-view.jsx) + // for the identical reason; Favorites/History were missed. Concretely hit live: favoriting a large + // (out-of-band) file attachment makes the *server* 500 on GET /favorites?type=all from then on + // (a real backend bug, tracked separately -- not something the frontend can prevent), and every + // subsequent visit to "全部" silently hung on the loading spinner with only a 2s toast, no way to + // tell "still loading" from "never going to load", no retry affordance. + const [loadError, setLoadError] = React.useState(false); const [fileUrls, setFileUrls] = React.useState({}); const [sourceNames, setSourceNames] = React.useState({}); // An in-flight request isn't guaranteed to resolve before a newer one its own type/query change @@ -302,6 +312,7 @@ function Favorites({view, hashParams, tinode, onToast}) { const requestId = ++requestIdRef.current; getFavorites({type, query, cursor: append ? cursor : ''}).then(data => { if (requestIdRef.current !== requestId) return; + setLoadError(false); setItems(prev => { const list = append ? (prev || []).concat(data.items || []) : (data.items || []); if (!query) writeCachedData('favorites:' + type, list); @@ -309,9 +320,14 @@ function Favorites({view, hashParams, tinode, onToast}) { }); setCursor(data.next_cursor || ''); setHasMore(!!data.next_cursor); - }).catch(err => onToast(err.message || '加载失败')); + }).catch(err => { + if (requestIdRef.current !== requestId) return; + onToast(err.message || '加载失败'); + if (!append) setLoadError(true); + }); }, [type, query, cursor]); React.useEffect(() => { + setLoadError(false); setItems((!query && readCachedData('favorites:' + type)) || null); load(false); }, [type, query]); @@ -380,7 +396,7 @@ function Favorites({view, hashParams, tinode, onToast}) { // 类型列表内的勾选,没有跨 tab 累加的设计)。 onClick={() => { setType(id); setSelected([]); if (TYPE_ROUTE[id] && !selecting) navigateReplace(TYPE_ROUTE[id]); }}>{label})}
- {items === null ?

加载中…

: + {items === null ?

{loadError ? '加载失败,请重试' : '加载中…'}

: list.length ? <>{list.map(item => toggle(item.favorite_id)} @@ -614,6 +630,10 @@ function History({view, hashParams, myUserId, onToast}) { const [items, setItems] = React.useState(() => (cacheKey && readCachedData(cacheKey)) || null); const [cursor, setCursor] = React.useState(''); const [hasMore, setHasMore] = React.useState(false); + // Same "stuck on 加载中… forever" gap as Favorites' loadError above, same fix -- a failed initial + // load with nothing cached to fall back on left this on the loading branch permanently, with only + // a 2s toast (already faded by the time anyone looks back) as the only sign anything went wrong. + const [loadError, setLoadError] = React.useState(false); const [mediaUrls, setMediaUrls] = React.useState({}); const [senderNames, setSenderNames] = React.useState({}); // Same stale-response guard as Favorites' load() above and for the same reason -- a slower @@ -628,15 +648,21 @@ function History({view, hashParams, myUserId, onToast}) { getTopicMedia(topic, type, append ? cursor : ''); call.then(data => { if (requestIdRef.current !== requestId) return; + setLoadError(false); setItems(prev => { const list = append ? (prev || []).concat(data.items || []) : (data.items || []); if (!query && cacheKey) writeCachedData(cacheKey, list); return list; }); setCursor(data.next_cursor || ''); setHasMore(!!data.next_cursor); - }).catch(err => onToast(err.message || '加载失败')); + }).catch(err => { + if (requestIdRef.current !== requestId) return; + onToast(err.message || '加载失败'); + if (!append) setLoadError(true); + }); }, [topic, view, query, cursor]); React.useEffect(() => { + setLoadError(false); setItems((!query && cacheKey && readCachedData(cacheKey)) || null); load(false); }, [topic, view, query]); @@ -687,7 +713,7 @@ function History({view, hashParams, myUserId, onToast}) { {view === 'history-search' ? : null}
- {items === null ?

加载中…

: !list.length ? + {items === null ?

{loadError ? '加载失败,请重试' : '加载中…'}

: !list.length ?
{view === 'history-search' ? 'search_off' : 'inbox'}暂无内容
: view === 'history-search' ?
{/* 同一处"问题13"修复暴露的缺口,History 自己的一份,只影响 type==='text':非 text 类型