From d65d6d23964f9d936bd11779032e95b15db428a8 Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Mon, 2 Mar 2026 14:54:19 +0800 Subject: [PATCH] fix(sns): add overview stats status and fallback resilience --- electron/services/snsService.ts | 49 +++++++++++++++++++++++++++------ src/pages/SnsPage.scss | 16 +++++++++++ src/pages/SnsPage.tsx | 49 ++++++++++++++++++++++++++------- 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index e6e144c..08d47a8 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -497,6 +497,8 @@ class SnsService { } let { totalPosts, totalFriends } = await this.getExportStatsFromTableCount() + let fallbackAttempted = false + let fallbackError = '' // 某些环境下 SnsTimeLine 统计查询会返回 0,这里在允许时回退到与导出同源的 timeline 接口统计。 if ( @@ -504,23 +506,52 @@ class SnsService { (totalPosts <= 0 || totalFriends <= 0) && now - this.lastTimelineFallbackAt >= this.timelineFallbackCooldownMs ) { - this.lastTimelineFallbackAt = now - const timelineStats = await this.getExportStatsFromTimeline() - if (timelineStats.totalPosts > 0) { - totalPosts = timelineStats.totalPosts + fallbackAttempted = true + try { + const timelineStats = await this.getExportStatsFromTimeline() + this.lastTimelineFallbackAt = Date.now() + if (timelineStats.totalPosts > 0) { + totalPosts = timelineStats.totalPosts + } + if (timelineStats.totalFriends > 0) { + totalFriends = timelineStats.totalFriends + } + } catch (error) { + fallbackError = String(error) + console.error('[SnsService] getExportStats timeline fallback failed:', error) } - if (timelineStats.totalFriends > 0) { - totalFriends = timelineStats.totalFriends + } + + const normalizedStats = { + totalPosts: Math.max(0, Number(totalPosts || 0)), + totalFriends: Math.max(0, Number(totalFriends || 0)) + } + const computedHasData = normalizedStats.totalPosts > 0 || normalizedStats.totalFriends > 0 + const cacheHasData = !!this.exportStatsCache && (this.exportStatsCache.totalPosts > 0 || this.exportStatsCache.totalFriends > 0) + + // 计算结果全 0 时,优先使用已有非零缓存,避免瞬时异常覆盖有效统计。 + if (!computedHasData && cacheHasData && this.exportStatsCache) { + return { + success: true, + data: { + totalPosts: this.exportStatsCache.totalPosts, + totalFriends: this.exportStatsCache.totalFriends + } } } + // 当主查询结果全 0 且回退统计执行失败时,返回失败给前端显示明确状态(而非错误地展示 0)。 + if (!computedHasData && fallbackAttempted && fallbackError) { + return { success: false, error: fallbackError } + } + this.exportStatsCache = { - totalPosts, - totalFriends, + totalPosts: normalizedStats.totalPosts, + totalFriends: normalizedStats.totalFriends, updatedAt: Date.now() } - return { success: true, data: { totalPosts, totalFriends } } + return { success: true, data: normalizedStats } } catch (e) { if (this.exportStatsCache) { return { diff --git a/src/pages/SnsPage.scss b/src/pages/SnsPage.scss index 5729cf2..4b4253f 100644 --- a/src/pages/SnsPage.scss +++ b/src/pages/SnsPage.scss @@ -74,6 +74,22 @@ &.loading { opacity: 0.7; } + + &.error { + color: #d94f45; + } + } + + .feed-stats-retry { + border: none; + background: transparent; + color: inherit; + font-size: 13px; + padding: 0; + line-height: 1.4; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; } .header-actions { diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index 19b2eb9..99ddd4a 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -27,6 +27,8 @@ interface SnsOverviewStats { latestTime: number | null } +type OverviewStatsStatus = 'loading' | 'ready' | 'error' + export default function SnsPage() { const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(false) @@ -38,7 +40,7 @@ export default function SnsPage() { earliestTime: null, latestTime: null }) - const [overviewStatsLoading, setOverviewStatsLoading] = useState(false) + const [overviewStatsStatus, setOverviewStatsStatus] = useState('loading') // Filter states const [searchKeyword, setSearchKeyword] = useState('') @@ -78,6 +80,7 @@ export default function SnsPage() { const [loadingNewer, setLoadingNewer] = useState(false) const postsRef = useRef([]) const overviewStatsRef = useRef(overviewStats) + const overviewStatsStatusRef = useRef(overviewStatsStatus) const selectedUsernamesRef = useRef(selectedUsernames) const searchKeywordRef = useRef(searchKeyword) const jumpTargetDateRef = useRef(jumpTargetDate) @@ -92,6 +95,9 @@ export default function SnsPage() { useEffect(() => { overviewStatsRef.current = overviewStats }, [overviewStats]) + useEffect(() => { + overviewStatsStatusRef.current = overviewStatsStatus + }, [overviewStatsStatus]) useEffect(() => { selectedUsernamesRef.current = selectedUsernames }, [selectedUsernames]) @@ -141,14 +147,17 @@ export default function SnsPage() { try { const scopeKey = await ensureSnsCacheScopeKey() if (!scopeKey) return + const existingCache = await configService.getSnsPageCache(scopeKey) let postsToStore = patch?.posts ?? postsRef.current if (!patch?.posts && postsToStore.length === 0) { - const existingCache = await configService.getSnsPageCache(scopeKey) if (existingCache && Array.isArray(existingCache.posts) && existingCache.posts.length > 0) { postsToStore = existingCache.posts as SnsPost[] } } - const overviewToStore = patch?.overviewStats ?? overviewStatsRef.current + const overviewToStore = patch?.overviewStats + ?? (overviewStatsStatusRef.current === 'ready' + ? overviewStatsRef.current + : existingCache?.overviewStats ?? overviewStatsRef.current) await configService.setSnsPageCache(scopeKey, { overviewStats: overviewToStore, posts: postsToStore.slice(0, SNS_PAGE_CACHE_POST_LIMIT) @@ -167,12 +176,18 @@ export default function SnsPage() { const cachedOverview = cached.overviewStats if (cachedOverview) { + const cachedTotalPosts = Math.max(0, Number(cachedOverview.totalPosts || 0)) + const cachedTotalFriends = Math.max(0, Number(cachedOverview.totalFriends || 0)) + const hasCachedPosts = Array.isArray(cached.posts) && cached.posts.length > 0 + const hasOverviewData = cachedTotalPosts > 0 || cachedTotalFriends > 0 setOverviewStats({ - totalPosts: Math.max(0, Number(cachedOverview.totalPosts || 0)), - totalFriends: Math.max(0, Number(cachedOverview.totalFriends || 0)), + totalPosts: cachedTotalPosts, + totalFriends: cachedTotalFriends, earliestTime: cachedOverview.earliestTime ?? null, latestTime: cachedOverview.latestTime ?? null }) + // 只有明确有统计值(或确实无帖子)时才把缓存视为 ready,避免历史异常 0 卡住显示。 + setOverviewStatsStatus(hasOverviewData || !hasCachedPosts ? 'ready' : 'loading') } if (Array.isArray(cached.posts) && cached.posts.length > 0) { @@ -197,7 +212,7 @@ export default function SnsPage() { }, [ensureSnsCacheScopeKey]) const loadOverviewStats = useCallback(async () => { - setOverviewStatsLoading(true) + setOverviewStatsStatus('loading') try { const statsResult = await window.electronAPI.sns.getExportStats() if (!statsResult.success || !statsResult.data) { @@ -232,14 +247,28 @@ export default function SnsPage() { latestTime } setOverviewStats(nextOverviewStats) + setOverviewStatsStatus('ready') void persistSnsPageCache({ overviewStats: nextOverviewStats }) } catch (error) { console.error('Failed to load SNS overview stats:', error) - } finally { - setOverviewStatsLoading(false) + setOverviewStatsStatus('error') } }, [persistSnsPageCache]) + const renderOverviewStats = () => { + if (overviewStatsStatus === 'error') { + return ( + + ) + } + if (overviewStatsStatus === 'loading') { + return '统计中...' + } + return `共 ${overviewStats.totalPosts} 条 | ${formatDateOnly(overviewStats.earliestTime)} ~ ${formatDateOnly(overviewStats.latestTime)} | ${overviewStats.totalFriends} 位好友` + } + const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => { const { reset = false, direction = 'older' } = options if (loadingRef.current) return @@ -513,8 +542,8 @@ export default function SnsPage() {

朋友圈

-
- 共 {overviewStats.totalPosts} 条 | {formatDateOnly(overviewStats.earliestTime)} ~ {formatDateOnly(overviewStats.latestTime)} | {overviewStats.totalFriends} 位好友 +
+ {renderOverviewStats()}