diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 98ca8a2..539ee92 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -2000,6 +2000,17 @@ function ExportPage() { const value = Number(rawCount) normalizedCounts[username] = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0 } + + void (async () => { + try { + const scopeKey = exportCacheScopeReadyRef.current + ? exportCacheScopeRef.current + : await ensureExportCacheScope() + await configService.setExportSnsUserPostCountsCache(scopeKey, normalizedCounts) + } catch (cacheError) { + console.error('写入导出页朋友圈条数缓存失败:', cacheError) + } + })() } catch (error) { console.error('加载朋友圈用户条数失败:', error) if (runToken !== snsUserPostCountsHydrationTokenRef.current) return @@ -2040,7 +2051,7 @@ function ExportPage() { } applyBatch() - }, [patchSessionLoadTraceStage, snsUserPostCountsStatus]) + }, [ensureExportCacheScope, patchSessionLoadTraceStage, snsUserPostCountsStatus]) const loadSessionSnsTimelinePosts = useCallback(async (target: SessionSnsTimelineTarget, options?: { reset?: boolean }) => { const reset = Boolean(options?.reset) diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index 8f6e733..b51ce85 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -113,12 +113,14 @@ export default function SnsPage() { const [hasNewer, setHasNewer] = useState(false) const [loadingNewer, setLoadingNewer] = useState(false) const postsRef = useRef([]) + const contactsRef = useRef([]) const overviewStatsRef = useRef(overviewStats) const overviewStatsStatusRef = useRef(overviewStatsStatus) const selectedUsernamesRef = useRef(selectedUsernames) const searchKeywordRef = useRef(searchKeyword) const jumpTargetDateRef = useRef(jumpTargetDate) const cacheScopeKeyRef = useRef('') + const snsUserPostCountsCacheScopeKeyRef = useRef('') const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null) const contactsLoadTokenRef = useRef(0) const contactsCountHydrationTokenRef = useRef(0) @@ -132,6 +134,9 @@ export default function SnsPage() { useEffect(() => { postsRef.current = posts }, [posts]) + useEffect(() => { + contactsRef.current = contacts + }, [contacts]) useEffect(() => { overviewStatsRef.current = overviewStats }, [overviewStats]) @@ -222,6 +227,21 @@ export default function SnsPage() { return scopeKey }, []) + const ensureSnsUserPostCountsCacheScopeKey = useCallback(async () => { + if (snsUserPostCountsCacheScopeKeyRef.current) return snsUserPostCountsCacheScopeKeyRef.current + const [wxidRaw, dbPathRaw] = await Promise.all([ + configService.getMyWxid(), + configService.getDbPath() + ]) + const wxid = String(wxidRaw || '').trim() + const dbPath = String(dbPathRaw || '').trim() + const scopeKey = (dbPath || wxid) + ? `${dbPath}::${wxid}` + : 'default' + snsUserPostCountsCacheScopeKeyRef.current = scopeKey + return scopeKey + }, []) + const persistSnsPageCache = useCallback(async (patch?: { posts?: SnsPost[]; overviewStats?: SnsOverviewStats }) => { if (!isDefaultViewNow()) return try { @@ -484,36 +504,47 @@ export default function SnsPage() { } }, []) - const hydrateContactPostCounts = useCallback(async (usernames: string[]) => { + const hydrateContactPostCounts = useCallback(async (usernames: string[], options?: { force?: boolean }) => { + const force = options?.force === true const targets = usernames .map((username) => String(username || '').trim()) .filter(Boolean) stopContactsCountHydration(true) if (targets.length === 0) return + const readySet = new Set( + contactsRef.current + .filter((contact) => contact.postCountStatus === 'ready' && typeof contact.postCount === 'number') + .map((contact) => contact.username) + ) + const pendingTargets = force ? targets : targets.filter((username) => !readySet.has(username)) const runToken = ++contactsCountHydrationTokenRef.current const totalTargets = targets.length - const targetSet = new Set(targets) + const targetSet = new Set(pendingTargets) - setContacts((prev) => { - let changed = false - const next = prev.map((contact) => { - if (!targetSet.has(contact.username)) return contact - if (contact.postCountStatus === 'loading' && typeof contact.postCount !== 'number') return contact - changed = true - return { - ...contact, - postCount: undefined, - postCountStatus: 'loading' as ContactPostCountStatus - } + if (pendingTargets.length > 0) { + setContacts((prev) => { + let changed = false + const next = prev.map((contact) => { + if (!targetSet.has(contact.username)) return contact + if (contact.postCountStatus === 'loading' && typeof contact.postCount !== 'number') return contact + changed = true + return { + ...contact, + postCount: force ? undefined : contact.postCount, + postCountStatus: 'loading' as ContactPostCountStatus + } + }) + return changed ? sortContactsForRanking(next) : prev }) - return changed ? sortContactsForRanking(next) : prev - }) + } + const preResolved = Math.max(0, totalTargets - pendingTargets.length) setContactsCountProgress({ - resolved: 0, + resolved: preResolved, total: totalTargets, - running: true + running: pendingTargets.length > 0 }) + if (pendingTargets.length === 0) return let normalizedCounts: Record = {} try { @@ -523,17 +554,25 @@ export default function SnsPage() { normalizedCounts = Object.fromEntries( Object.entries(result.counts).map(([username, value]) => [username, normalizePostCount(value)]) ) + void (async () => { + try { + const scopeKey = await ensureSnsUserPostCountsCacheScopeKey() + await configService.setExportSnsUserPostCountsCache(scopeKey, normalizedCounts) + } catch (cacheError) { + console.error('Failed to persist SNS user post counts cache:', cacheError) + } + })() } } catch (error) { console.error('Failed to load contact post counts:', error) } - let resolved = 0 + let resolved = preResolved let cursor = 0 const applyBatch = () => { if (runToken !== contactsCountHydrationTokenRef.current) return - const batch = targets.slice(cursor, cursor + CONTACT_COUNT_BATCH_SIZE) + const batch = pendingTargets.slice(cursor, cursor + CONTACT_COUNT_BATCH_SIZE) if (batch.length === 0) { setContactsCountProgress({ resolved: totalTargets, @@ -585,6 +624,9 @@ export default function SnsPage() { stopContactsCountHydration(true) setContactsLoading(true) try { + const snsPostCountsScopeKey = await ensureSnsUserPostCountsCacheScopeKey() + const cachedPostCountsItem = await configService.getExportSnsUserPostCountsCache(snsPostCountsScopeKey) + const cachedPostCounts = cachedPostCountsItem?.counts || {} const [contactsResult, sessionsResult] = await Promise.all([ window.electronAPI.chat.getContacts(), window.electronAPI.chat.getSessions() @@ -610,14 +652,16 @@ export default function SnsPage() { if (contactsResult.success && contactsResult.contacts) { for (const c of contactsResult.contacts) { if (c.type === 'friend' || c.type === 'former_friend') { + const cachedCount = cachedPostCounts[c.username] + const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount) contactMap.set(c.username, { username: c.username, displayName: c.displayName, avatarUrl: c.avatarUrl, type: c.type === 'former_friend' ? 'former_friend' : 'friend', lastSessionTimestamp: Number(sessionTimestampMap.get(c.username) || 0), - postCount: undefined, - postCountStatus: 'idle' + postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined, + postCountStatus: hasCachedCount ? 'ready' : 'idle' }) } } @@ -668,7 +712,7 @@ export default function SnsPage() { setContactsLoading(false) } } - }, [hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration]) + }, [ensureSnsUserPostCountsCacheScopeKey, hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration]) const closeAuthorTimeline = useCallback(() => { authorTimelineRequestTokenRef.current += 1 diff --git a/src/services/config.ts b/src/services/config.ts index b5a25c8..b644ca0 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -41,6 +41,7 @@ export const CONFIG_KEYS = { EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP: 'exportSessionMessageCountCacheMap', EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP: 'exportSessionContentMetricCacheMap', EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap', + EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP: 'exportSnsUserPostCountsCacheMap', SNS_PAGE_CACHE_MAP: 'snsPageCacheMap', CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs', CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap', @@ -533,6 +534,11 @@ export interface ExportSnsStatsCacheItem { totalFriends: number } +export interface ExportSnsUserPostCountsCacheItem { + updatedAt: number + counts: Record +} + export interface SnsPageOverviewCache { totalPosts: number totalFriends: number @@ -740,6 +746,58 @@ export async function setExportSnsStatsCache( await config.set(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP, map) } +export async function getExportSnsUserPostCountsCache(scopeKey: string): Promise { + if (!scopeKey) return null + const value = await config.get(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP) + if (!value || typeof value !== 'object') return null + const rawMap = value as Record + const rawItem = rawMap[scopeKey] + if (!rawItem || typeof rawItem !== 'object') return null + + const raw = rawItem as Record + const rawCounts = raw.counts + if (!rawCounts || typeof rawCounts !== 'object') return null + + const counts: Record = {} + for (const [rawUsername, rawCount] of Object.entries(rawCounts as Record)) { + const username = String(rawUsername || '').trim() + if (!username) continue + const valueNum = Number(rawCount) + counts[username] = Number.isFinite(valueNum) ? Math.max(0, Math.floor(valueNum)) : 0 + } + + const updatedAt = typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) + ? raw.updatedAt + : 0 + return { updatedAt, counts } +} + +export async function setExportSnsUserPostCountsCache( + scopeKey: string, + counts: Record +): Promise { + if (!scopeKey) return + const current = await config.get(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP) + const map = current && typeof current === 'object' + ? { ...(current as Record) } + : {} + + const normalized: Record = {} + for (const [rawUsername, rawCount] of Object.entries(counts || {})) { + const username = String(rawUsername || '').trim() + if (!username) continue + const valueNum = Number(rawCount) + normalized[username] = Number.isFinite(valueNum) ? Math.max(0, Math.floor(valueNum)) : 0 + } + + map[scopeKey] = { + updatedAt: Date.now(), + counts: normalized + } + + await config.set(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP, map) +} + export async function getSnsPageCache(scopeKey: string): Promise { if (!scopeKey) return null const value = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP)