From c625756ab4e848c23b154bd5e8d875e88604e897 Mon Sep 17 00:00:00 2001 From: aits2026 Date: Thu, 5 Mar 2026 19:35:42 +0800 Subject: [PATCH] fix(export,sns): preserve sns load state across route switches --- src/pages/ExportPage.tsx | 47 +++++++++++++++++++++++++++------- src/pages/SnsPage.tsx | 54 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 539ee92..5e89b9b 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -1977,8 +1977,40 @@ function ExportPage() { return } - patchSessionLoadTraceStage(targetSessionIds, 'snsPostCounts', 'pending', { force: true }) - patchSessionLoadTraceStage(targetSessionIds, 'snsPostCounts', 'loading') + const scopeKey = exportCacheScopeReadyRef.current + ? exportCacheScopeRef.current + : await ensureExportCacheScope() + const targetSet = new Set(targetSessionIds) + let cachedCounts: Record = {} + try { + const cached = await configService.getExportSnsUserPostCountsCache(scopeKey) + cachedCounts = cached?.counts || {} + } catch (cacheError) { + console.error('读取导出页朋友圈条数缓存失败:', cacheError) + } + + const cachedTargetCounts = Object.entries(cachedCounts).reduce>((acc, [sessionId, countRaw]) => { + if (!targetSet.has(sessionId)) return acc + const nextCount = Number(countRaw) + acc[sessionId] = Number.isFinite(nextCount) ? Math.max(0, Math.floor(nextCount)) : 0 + return acc + }, {}) + const cachedReadySessionIds = Object.keys(cachedTargetCounts) + if (cachedReadySessionIds.length > 0) { + setSnsUserPostCounts(prev => ({ ...prev, ...cachedTargetCounts })) + patchSessionLoadTraceStage(cachedReadySessionIds, 'snsPostCounts', 'done') + } + + const pendingSessionIds = options?.force + ? targetSessionIds + : targetSessionIds.filter((sessionId) => !(sessionId in cachedTargetCounts)) + if (pendingSessionIds.length === 0) { + setSnsUserPostCountsStatus('ready') + return + } + + patchSessionLoadTraceStage(pendingSessionIds, 'snsPostCounts', 'pending', { force: true }) + patchSessionLoadTraceStage(pendingSessionIds, 'snsPostCounts', 'loading') setSnsUserPostCountsStatus('loading') let normalizedCounts: Record = {} @@ -1987,7 +2019,7 @@ function ExportPage() { if (runToken !== snsUserPostCountsHydrationTokenRef.current) return if (!result.success || !result.counts) { - patchSessionLoadTraceStage(targetSessionIds, 'snsPostCounts', 'failed', { + patchSessionLoadTraceStage(pendingSessionIds, 'snsPostCounts', 'failed', { error: result.error || '朋友圈条数统计失败' }) setSnsUserPostCountsStatus('error') @@ -2003,9 +2035,6 @@ function ExportPage() { void (async () => { try { - const scopeKey = exportCacheScopeReadyRef.current - ? exportCacheScopeRef.current - : await ensureExportCacheScope() await configService.setExportSnsUserPostCountsCache(scopeKey, normalizedCounts) } catch (cacheError) { console.error('写入导出页朋友圈条数缓存失败:', cacheError) @@ -2014,7 +2043,7 @@ function ExportPage() { } catch (error) { console.error('加载朋友圈用户条数失败:', error) if (runToken !== snsUserPostCountsHydrationTokenRef.current) return - patchSessionLoadTraceStage(targetSessionIds, 'snsPostCounts', 'failed', { + patchSessionLoadTraceStage(pendingSessionIds, 'snsPostCounts', 'failed', { error: String(error) }) setSnsUserPostCountsStatus('error') @@ -2025,7 +2054,7 @@ function ExportPage() { const applyBatch = () => { if (runToken !== snsUserPostCountsHydrationTokenRef.current) return - const batchSessionIds = targetSessionIds.slice(cursor, cursor + SNS_USER_POST_COUNT_BATCH_SIZE) + const batchSessionIds = pendingSessionIds.slice(cursor, cursor + SNS_USER_POST_COUNT_BATCH_SIZE) if (batchSessionIds.length === 0) { setSnsUserPostCountsStatus('ready') snsUserPostCountsBatchTimerRef.current = null @@ -2984,7 +3013,7 @@ function ExportPage() { } setIsSessionEnriching(false) setIsLoadingSessionCounts(false) - setSnsUserPostCountsStatus('idle') + setSnsUserPostCountsStatus(prev => (prev === 'loading' ? 'idle' : prev)) }, [isExportRoute]) useEffect(() => { diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index b51ce85..f088e76 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -504,7 +504,10 @@ export default function SnsPage() { } }, []) - const hydrateContactPostCounts = useCallback(async (usernames: string[], options?: { force?: boolean }) => { + const hydrateContactPostCounts = useCallback(async ( + usernames: string[], + options?: { force?: boolean; readyUsernames?: Set } + ) => { const force = options?.force === true const targets = usernames .map((username) => String(username || '').trim()) @@ -512,7 +515,7 @@ export default function SnsPage() { stopContactsCountHydration(true) if (targets.length === 0) return - const readySet = new Set( + const readySet = options?.readyUsernames || new Set( contactsRef.current .filter((contact) => contact.postCountStatus === 'ready' && typeof contact.postCount === 'number') .map((contact) => contact.username) @@ -625,8 +628,42 @@ export default function SnsPage() { setContactsLoading(true) try { const snsPostCountsScopeKey = await ensureSnsUserPostCountsCacheScopeKey() - const cachedPostCountsItem = await configService.getExportSnsUserPostCountsCache(snsPostCountsScopeKey) + const [cachedPostCountsItem, cachedContactsItem, cachedAvatarItem] = await Promise.all([ + configService.getExportSnsUserPostCountsCache(snsPostCountsScopeKey), + configService.getContactsListCache(snsPostCountsScopeKey), + configService.getContactsAvatarCache(snsPostCountsScopeKey) + ]) const cachedPostCounts = cachedPostCountsItem?.counts || {} + const cachedAvatarMap = cachedAvatarItem?.avatars || {} + const cachedContacts = (cachedContactsItem?.contacts || []) + .filter((contact) => contact.type === 'friend' || contact.type === 'former_friend') + .map((contact) => { + const cachedCount = cachedPostCounts[contact.username] + const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount) + return { + username: contact.username, + displayName: contact.displayName || contact.username, + avatarUrl: cachedAvatarMap[contact.username]?.avatarUrl, + type: (contact.type === 'former_friend' ? 'former_friend' : 'friend') as 'friend' | 'former_friend', + lastSessionTimestamp: 0, + postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined, + postCountStatus: hasCachedCount ? 'ready' as ContactPostCountStatus : 'idle' as ContactPostCountStatus + } + }) + + if (requestToken !== contactsLoadTokenRef.current) return + if (cachedContacts.length > 0) { + const cachedContactsSorted = sortContactsForRanking(cachedContacts) + setContacts(cachedContactsSorted) + setContactsLoading(false) + const cachedReadyCount = cachedContactsSorted.filter(contact => contact.postCountStatus === 'ready').length + setContactsCountProgress({ + resolved: cachedReadyCount, + total: cachedContactsSorted.length, + running: cachedReadyCount < cachedContactsSorted.length + }) + } + const [contactsResult, sessionsResult] = await Promise.all([ window.electronAPI.chat.getContacts(), window.electronAPI.chat.getSessions() @@ -670,7 +707,15 @@ export default function SnsPage() { let contactsList = sortContactsForRanking(Array.from(contactMap.values())) if (requestToken !== contactsLoadTokenRef.current) return setContacts(contactsList) - void hydrateContactPostCounts(contactsList.map(contact => contact.username)) + const readyUsernames = new Set( + contactsList + .filter((contact) => contact.postCountStatus === 'ready' && typeof contact.postCount === 'number') + .map((contact) => contact.username) + ) + void hydrateContactPostCounts( + contactsList.map(contact => contact.username), + { readyUsernames } + ) const allUsernames = contactsList.map(c => c.username) @@ -880,6 +925,7 @@ export default function SnsPage() { useEffect(() => { const handleChange = () => { cacheScopeKeyRef.current = '' + snsUserPostCountsCacheScopeKeyRef.current = '' // wxid changed, reset everything stopContactsCountHydration(true) setContacts([])