From b063ed299bf27946d7ff1da3be0abadd13c3430f Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 30 May 2026 18:10:59 +0800 Subject: [PATCH] fix(export): stop background stats when hidden, fix loading state, optimize cache and memory usage --- electron/services/chatService.ts | 37 ++++-- src/pages/ExportPage.tsx | 212 +++++++++++++++++++++++-------- 2 files changed, 184 insertions(+), 65 deletions(-) diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index c928d19..145f353 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -490,6 +490,13 @@ class ChatService { return true } + private shouldPersistAvatarUrl(avatarUrl?: string): avatarUrl is string { + const normalized = String(avatarUrl || '').trim() + if (!this.isValidAvatarUrl(normalized)) return false + if (!normalized.startsWith('data:')) return true + return normalized.length <= 4096 + } + private extractErrorCode(message?: string | null): number | null { const text = String(message || '').trim() if (!text) return null @@ -1115,6 +1122,7 @@ class ChatService { const summary = this.cleanString(row.summary || row.digest || row.last_msg || row.lastMsg || '') const lastMsgType = parseInt(row.last_msg_type || row.lastMsgType || '0', 10) const cached = this.avatarCache.get(username) + const cachedAvatarUrl = this.shouldPersistAvatarUrl(cached?.avatarUrl) ? cached?.avatarUrl : undefined const contact = contactMap.get(username) const session: ChatSession = { @@ -1126,7 +1134,7 @@ class ChatService { lastTimestamp: lastTs, lastMsgType, displayName: contact?.displayName || cached?.displayName || username, - avatarUrl: cached?.avatarUrl, + avatarUrl: cachedAvatarUrl, lastMsgSender: row.last_msg_sender, lastSenderDisplayName: row.last_sender_display_name, selfWxid: myWxid @@ -1484,8 +1492,7 @@ class ChatService { // 检查缓存 for (const username of normalizedUsernames) { const cached = this.avatarCache.get(username) - const isValidAvatar = this.isValidAvatarUrl(cached?.avatarUrl) - const cachedAvatarUrl = isValidAvatar ? cached?.avatarUrl : undefined + const cachedAvatarUrl = this.shouldPersistAvatarUrl(cached?.avatarUrl) ? cached?.avatarUrl : undefined if (onlyMissingAvatar && cachedAvatarUrl) { result[username] = { displayName: skipDisplayName ? undefined : cached?.displayName, @@ -1495,7 +1502,7 @@ class ChatService { } // 如果缓存有效且有头像,直接使用;如果没有头像,也需要重新尝试获取 // 额外检查:如果头像是无效的 hex 格式(以 ffd8 开头),也需要重新获取 - if (cached && now - cached.updatedAt < this.avatarCacheTtlMs && isValidAvatar) { + if (cached && now - cached.updatedAt < this.avatarCacheTtlMs && cachedAvatarUrl) { result[username] = { displayName: skipDisplayName ? undefined : cached.displayName, avatarUrl: cachedAvatarUrl @@ -1529,7 +1536,11 @@ class ChatService { const cacheEntry: ContactCacheEntry = { displayName: displayName || previous?.displayName || username, - avatarUrl, + avatarUrl: this.shouldPersistAvatarUrl(avatarUrl) + ? avatarUrl + : this.shouldPersistAvatarUrl(previous?.avatarUrl) + ? previous?.avatarUrl + : undefined, updatedAt: now } result[username] = { @@ -1549,7 +1560,7 @@ class ChatService { if (avatarUrl) { result[username].avatarUrl = avatarUrl const cached = this.avatarCache.get(username) - if (cached) { + if (cached && this.shouldPersistAvatarUrl(avatarUrl)) { cached.avatarUrl = avatarUrl updatedEntries[username] = cached } @@ -7276,9 +7287,9 @@ class ChatService { if (!connectResult.success) return null const cached = this.avatarCache.get(username) // 检查缓存是否有效,且头像不是错误的 hex 格式 - const isValidAvatar = this.isValidAvatarUrl(cached?.avatarUrl) - if (cached && isValidAvatar && Date.now() - cached.updatedAt < this.avatarCacheTtlMs) { - return { avatarUrl: cached.avatarUrl, displayName: cached.displayName } + const cachedAvatarUrl = this.shouldPersistAvatarUrl(cached?.avatarUrl) ? cached?.avatarUrl : undefined + if (cached && cachedAvatarUrl && Date.now() - cached.updatedAt < this.avatarCacheTtlMs) { + return { avatarUrl: cachedAvatarUrl, displayName: cached.displayName } } const contact = await this.getContact(username) @@ -7296,7 +7307,11 @@ class ChatService { } const displayName = contact?.remark || contact?.nickName || contact?.alias || cached?.displayName || username const cacheEntry: ContactCacheEntry = { - avatarUrl, + avatarUrl: this.shouldPersistAvatarUrl(avatarUrl) + ? avatarUrl + : this.shouldPersistAvatarUrl(cached?.avatarUrl) + ? cached?.avatarUrl + : undefined, displayName, updatedAt: Date.now() } @@ -7732,7 +7747,7 @@ class ChatService { const cachedContact = this.avatarCache.get(normalizedSessionId) if (cachedContact) { displayName = cachedContact.displayName || normalizedSessionId - if (this.isValidAvatarUrl(cachedContact.avatarUrl)) { + if (this.shouldPersistAvatarUrl(cachedContact.avatarUrl)) { avatarUrl = cachedContact.avatarUrl } } diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index d486530..edef3fb 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -232,12 +232,13 @@ const EXPORT_PROGRESS_UI_FLUSH_INTERVAL_MS = 320 const SESSION_MEDIA_METRIC_PREFETCH_ROWS = 10 const SESSION_MEDIA_METRIC_BATCH_SIZE = 8 const SESSION_MEDIA_METRIC_BACKGROUND_FEED_SIZE = 48 -const SESSION_MEDIA_METRIC_BACKGROUND_FEED_INTERVAL_MS = 120 const SESSION_MEDIA_METRIC_CACHE_FLUSH_DELAY_MS = 1200 +const SESSION_DETAIL_BACKGROUND_METRIC_LIMIT_PER_TAB = 96 const SNS_USER_POST_COUNT_BATCH_SIZE = 12 const SNS_USER_POST_COUNT_BATCH_INTERVAL_MS = 120 const SNS_RANK_PAGE_SIZE = 50 const SNS_RANK_DISPLAY_LIMIT = 15 +const INLINE_AVATAR_CACHE_MAX_LENGTH = 4096 const contentTypeLabels: Record = { text: '聊天文本', voice: '语音', @@ -796,6 +797,13 @@ const normalizeExportAvatarUrl = (value?: string | null): string | undefined => return normalized } +const shouldPersistExportAvatarUrl = (value?: string | null): value is string => { + const normalized = normalizeExportAvatarUrl(value) + if (!normalized) return false + if (!normalized.startsWith('data:')) return true + return normalized.length <= INLINE_AVATAR_CACHE_MAX_LENGTH +} + const toComparableNameSet = (values: Array): Set => { const set = new Set() for (const value of values) { @@ -1306,6 +1314,15 @@ const buildSessionMutualFriendsMetric = ( } } +const cloneCompactSessionMutualFriendsMetric = ( + metric: SessionMutualFriendsMetric, + limit = SNS_RANK_DISPLAY_LIMIT +): SessionMutualFriendsMetric => ({ + ...metric, + items: metric.items.slice(0, Math.max(0, limit)), + count: metric.count +}) + const getSessionMutualFriendDirectionLabel = (direction: SessionMutualFriendDirection): string => { if (direction === 'incoming') return '对方赞/评TA' if (direction === 'outgoing') return 'TA赞/评对方' @@ -1435,14 +1452,47 @@ const toContactMapFromCaches = ( const map: Record = {} for (const contact of contacts || []) { if (!contact?.username) continue + const cachedAvatarUrl = avatarEntries[contact.username]?.avatarUrl map[contact.username] = { ...contact, - avatarUrl: avatarEntries[contact.username]?.avatarUrl + avatarUrl: shouldPersistExportAvatarUrl(cachedAvatarUrl) ? cachedAvatarUrl : undefined } } return map } +const compactExportAvatarEntries = ( + avatarEntries: Record +): { + avatarEntries: Record + changed: boolean +} => { + const nextCache: Record = {} + let changed = false + for (const [username, entry] of Object.entries(avatarEntries || {})) { + const normalizedUsername = String(username || '').trim() + const avatarUrl = normalizeExportAvatarUrl(entry?.avatarUrl) + if (!normalizedUsername || !shouldPersistExportAvatarUrl(avatarUrl)) { + changed = true + continue + } + nextCache[normalizedUsername] = { + avatarUrl, + updatedAt: Number(entry?.updatedAt || 0) || Date.now(), + checkedAt: Number(entry?.checkedAt || 0) || Date.now() + } + if ( + normalizedUsername !== username || + avatarUrl !== entry?.avatarUrl || + nextCache[normalizedUsername].updatedAt !== entry?.updatedAt || + nextCache[normalizedUsername].checkedAt !== entry?.checkedAt + ) { + changed = true + } + } + return { avatarEntries: nextCache, changed } +} + const mergeAvatarCacheIntoContacts = ( sourceContacts: ContactInfo[], avatarEntries: Record @@ -1454,7 +1504,7 @@ const mergeAvatarCacheIntoContacts = ( let changed = false const merged = sourceContacts.map((contact) => { const cachedAvatar = avatarEntries[contact.username]?.avatarUrl - if (!cachedAvatar || contact.avatarUrl) { + if (!shouldPersistExportAvatarUrl(cachedAvatar) || contact.avatarUrl) { return contact } changed = true @@ -1476,19 +1526,20 @@ const upsertAvatarCacheFromContacts = ( changed: boolean updatedAt: number | null } => { - const nextCache = { ...avatarEntries } + const compactedCache = compactExportAvatarEntries(avatarEntries) + const nextCache = { ...compactedCache.avatarEntries } const now = options?.now || Date.now() const markCheckedSet = new Set((options?.markCheckedUsernames || []).filter(Boolean)) const usernamesInSource = new Set() - let changed = false + let changed = compactedCache.changed for (const contact of sourceContacts) { const username = String(contact.username || '').trim() if (!username) continue usernamesInSource.add(username) const prev = nextCache[username] - const avatarUrl = String(contact.avatarUrl || '').trim() - if (!avatarUrl) continue + const avatarUrl = normalizeExportAvatarUrl(contact.avatarUrl) + if (!shouldPersistExportAvatarUrl(avatarUrl)) continue const updatedAt = !prev || prev.avatarUrl !== avatarUrl ? now : prev.updatedAt const checkedAt = markCheckedSet.has(username) ? now : (prev?.checkedAt || now) if (!prev || prev.avatarUrl !== avatarUrl || prev.updatedAt !== updatedAt || prev.checkedAt !== checkedAt) { @@ -2459,6 +2510,7 @@ function ExportPage() { const hasBaseConfigReadyRef = useRef(false) const sessionCountRequestIdRef = useRef(0) const isLoadingSessionCountsRef = useRef(false) + const isExportRouteRef = useRef(isExportRoute) const activeTabRef = useRef('private') const detailStatsPriorityRef = useRef(false) const sessionSnsTimelinePostsRef = useRef([]) @@ -2499,6 +2551,8 @@ function ExportPage() { startIndex: 0, endIndex: -1 }) + const enqueueSessionMutualFriendsRequestsRef = useRef<(sessionIds: string[], options?: { front?: boolean }) => void>(() => {}) + const scheduleSessionMutualFriendsWorkerRef = useRef<() => void>(() => {}) const handleContactsListScrollParentRef = useCallback((node: HTMLDivElement | null) => { setContactsListScrollParent(prev => (prev === node ? prev : node)) @@ -2612,6 +2666,10 @@ function ExportPage() { isLoadingSessionCountsRef.current = isLoadingSessionCounts }, [isLoadingSessionCounts]) + useEffect(() => { + isExportRouteRef.current = isExportRoute + }, [isExportRoute]) + useEffect(() => { sessionContentMetricsRef.current = sessionContentMetrics }, [sessionContentMetrics]) @@ -2895,7 +2953,7 @@ function ExportPage() { let avatarCacheChanged = false for (const [username, patch] of avatarPatches.entries()) { - if (!patch.avatarUrl) continue + if (!shouldPersistExportAvatarUrl(patch.avatarUrl)) continue const previous = contactsAvatarCacheRef.current[username] if (previous?.avatarUrl === patch.avatarUrl) continue contactsAvatarCacheRef.current[username] = { @@ -3220,7 +3278,7 @@ function ExportPage() { } }, []) - const loadSnsUserPostCounts = useCallback(async (options?: { force?: boolean }) => { + const loadSnsUserPostCounts = useCallback(async (options?: { force?: boolean; cacheOnly?: boolean }) => { if (snsUserPostCountsStatus === 'loading') return if (!options?.force && snsUserPostCountsStatus === 'ready') return @@ -3271,6 +3329,10 @@ function ExportPage() { setSnsUserPostCountsStatus('ready') return } + if (options?.cacheOnly) { + setSnsUserPostCountsStatus('ready') + return + } patchSessionLoadTraceStage(pendingSessionIds, 'snsPostCounts', 'pending', { force: true }) patchSessionLoadTraceStage(pendingSessionIds, 'snsPostCounts', 'loading') @@ -3442,7 +3504,7 @@ function ExportPage() { const username = String(sessionSnsTimelineTarget?.username || '').trim() if (!username) return false if (Object.prototype.hasOwnProperty.call(snsUserPostCounts, username)) return false - return snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle' + return snsUserPostCountsStatus === 'loading' }, [sessionSnsTimelineTarget, snsUserPostCounts, snsUserPostCountsStatus]) const openSessionSnsTimelineByTarget = useCallback((target: SessionSnsTimelineTarget) => { @@ -3468,11 +3530,11 @@ function ExportPage() { setSessionSnsRankTotalPosts(normalizedCount) } else { setSessionSnsTimelineTotalPosts(null) - setSessionSnsTimelineStatsLoading(snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle') + setSessionSnsTimelineStatsLoading(snsUserPostCountsStatus === 'loading') setSessionSnsRankTotalPosts(null) } - void loadSnsUserPostCounts() + void loadSnsUserPostCounts({ cacheOnly: true }) }, [ loadSnsUserPostCounts, snsUserPostCounts, @@ -3506,7 +3568,11 @@ function ExportPage() { const normalizedSessionId = String(contact?.username || '').trim() if (!normalizedSessionId || !isSingleContactSession(normalizedSessionId)) return const metric = sessionMutualFriendsMetricsRef.current[normalizedSessionId] - if (!metric) return + if (!metric) { + enqueueSessionMutualFriendsRequestsRef.current([normalizedSessionId], { front: true }) + scheduleSessionMutualFriendsWorkerRef.current() + return + } setSessionMutualFriendsSearch('') setSessionMutualFriendsDialogTarget({ username: normalizedSessionId, @@ -3848,6 +3914,21 @@ function ExportPage() { } }, []) + const stopExportPageBackgroundLoaders = useCallback(() => { + sessionLoadTokenRef.current = Date.now() + sessionCountRequestIdRef.current += 1 + snsUserPostCountsHydrationTokenRef.current += 1 + if (snsUserPostCountsBatchTimerRef.current) { + window.clearTimeout(snsUserPostCountsBatchTimerRef.current) + snsUserPostCountsBatchTimerRef.current = null + } + resetSessionMediaMetricLoader() + resetSessionMutualFriendsLoader() + setIsSessionEnriching(false) + setIsLoadingSessionCounts(false) + setSnsUserPostCountsStatus(prev => (prev === 'loading' ? 'idle' : prev)) + }, [resetSessionMediaMetricLoader, resetSessionMutualFriendsLoader]) + const flushSessionMutualFriendsCache = useCallback(async () => { try { const scopeKey = await ensureExportCacheScope() @@ -3880,6 +3961,7 @@ function ExportPage() { }, []) const enqueueSessionMutualFriendsRequests = useCallback((sessionIds: string[], options?: { front?: boolean }) => { + if (!isExportRouteRef.current) return if (activeTaskCountRef.current > 0) return const front = options?.front === true const incoming: string[] = [] @@ -3901,13 +3983,16 @@ function ExportPage() { } }, [isSessionMutualFriendsReady, patchSessionLoadTraceStage]) + useEffect(() => { + enqueueSessionMutualFriendsRequestsRef.current = enqueueSessionMutualFriendsRequests + }, [enqueueSessionMutualFriendsRequests]) + const hasPendingMetricLoads = useCallback((): boolean => ( isLoadingSessionCountsRef.current || sessionMediaMetricQueuedSetRef.current.size > 0 || sessionMediaMetricLoadingSetRef.current.size > 0 || sessionMediaMetricWorkerRunningRef.current || - snsUserPostCountsStatus === 'loading' || - snsUserPostCountsStatus === 'idle' + snsUserPostCountsStatus === 'loading' ), [snsUserPostCountsStatus]) const getSessionMutualFriendProfile = useCallback((sessionId: string): { @@ -4070,6 +4155,7 @@ function ExportPage() { }, []) const enqueueSessionMediaMetricRequests = useCallback((sessionIds: string[], options?: { front?: boolean }) => { + if (!isExportRouteRef.current) return if (activeTaskCountRef.current > 0) return const front = options?.front === true const incoming: string[] = [] @@ -4128,6 +4214,7 @@ function ExportPage() { const runSessionMediaMetricWorker = useCallback(async (runId: number) => { if (sessionMediaMetricWorkerRunningRef.current) return + if (!isExportRouteRef.current) return sessionMediaMetricWorkerRunningRef.current = true const withTimeout = async (promise: Promise, timeoutMs: number, stage: string): Promise => { let timer: number | null = null @@ -4146,6 +4233,7 @@ function ExportPage() { } try { while (runId === sessionMediaMetricRunIdRef.current) { + if (!isExportRouteRef.current) break if (activeTaskCountRef.current > 0) { await new Promise(resolve => window.setTimeout(resolve, 150)) continue @@ -4210,6 +4298,10 @@ function ExportPage() { }) } } catch (error) { + if (!isExportRouteRef.current || runId !== sessionMediaMetricRunIdRef.current) { + patchSessionLoadTraceStage(batchSessionIds, 'mediaMetrics', 'pending', { force: true }) + break + } console.error('导出页加载会话媒体统计失败:', error) patchSessionLoadTraceStage(batchSessionIds, 'mediaMetrics', 'failed', { error: String(error) @@ -4234,11 +4326,14 @@ function ExportPage() { sessionMediaMetricWorkerRunningRef.current = false if (runId === sessionMediaMetricRunIdRef.current && sessionMediaMetricQueueRef.current.length > 0) { void runSessionMediaMetricWorker(runId) + } else if (runId === sessionMediaMetricRunIdRef.current && sessionMutualFriendsQueueRef.current.length > 0) { + scheduleSessionMutualFriendsWorkerRef.current() } } }, [applySessionMediaMetricsFromStats, isSessionMediaMetricReady, patchSessionLoadTraceStage]) const scheduleSessionMediaMetricWorker = useCallback(() => { + if (!isExportRouteRef.current) return if (activeTaskCountRef.current > 0) return if (sessionMediaMetricWorkerRunningRef.current) return const runId = sessionMediaMetricRunIdRef.current @@ -4255,6 +4350,9 @@ function ExportPage() { let hasMore = true while (hasMore) { + if (!isExportRouteRef.current) { + throw new Error('导出页已隐藏,已停止共同好友统计') + } const result = await window.electronAPI.sns.getTimeline( SNS_RANK_PAGE_SIZE, 0, @@ -4280,14 +4378,16 @@ function ExportPage() { hasMore = pagePosts.length >= SNS_RANK_PAGE_SIZE } - return buildSessionMutualFriendsMetric(allPosts, knownTotal) + return cloneCompactSessionMutualFriendsMetric(buildSessionMutualFriendsMetric(allPosts, knownTotal)) }, [snsUserPostCounts]) const runSessionMutualFriendsWorker = useCallback(async (runId: number) => { if (sessionMutualFriendsWorkerRunningRef.current) return + if (!isExportRouteRef.current) return sessionMutualFriendsWorkerRunningRef.current = true try { while (runId === sessionMutualFriendsRunIdRef.current) { + if (!isExportRouteRef.current) break if (activeTaskCountRef.current > 0) { await new Promise(resolve => window.setTimeout(resolve, 150)) continue @@ -4313,6 +4413,10 @@ function ExportPage() { sessionMutualFriendsReadySetRef.current.add(sessionId) patchSessionLoadTraceStage([sessionId], 'mutualFriends', 'done') } catch (error) { + if (!isExportRouteRef.current || runId !== sessionMutualFriendsRunIdRef.current) { + patchSessionLoadTraceStage([sessionId], 'mutualFriends', 'pending', { force: true }) + break + } console.error('导出页加载共同好友统计失败:', error) patchSessionLoadTraceStage([sessionId], 'mutualFriends', 'failed', { error: error instanceof Error ? error.message : String(error) @@ -4338,6 +4442,7 @@ function ExportPage() { ]) const scheduleSessionMutualFriendsWorker = useCallback(() => { + if (!isExportRouteRef.current) return if (activeTaskCountRef.current > 0) return if (!isSessionCountStageReady) return if (hasPendingMetricLoads()) return @@ -4346,6 +4451,16 @@ function ExportPage() { void runSessionMutualFriendsWorker(runId) }, [hasPendingMetricLoads, isSessionCountStageReady, runSessionMutualFriendsWorker]) + useEffect(() => { + scheduleSessionMutualFriendsWorkerRef.current = scheduleSessionMutualFriendsWorker + }, [scheduleSessionMutualFriendsWorker]) + + useEffect(() => { + if (snsUserPostCountsStatus === 'loading') return + if (sessionMutualFriendsQueueRef.current.length === 0) return + scheduleSessionMutualFriendsWorker() + }, [scheduleSessionMutualFriendsWorker, snsUserPostCountsStatus]) + const loadSessionMessageCounts = useCallback(async ( sourceSessions: SessionRow[], priorityTab: ConversationTab, @@ -4862,18 +4977,8 @@ function ExportPage() { if (isExportRoute) return // 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。 setIsAutomationCreateMode(false) - sessionLoadTokenRef.current = Date.now() - sessionCountRequestIdRef.current += 1 - snsUserPostCountsHydrationTokenRef.current += 1 - if (snsUserPostCountsBatchTimerRef.current) { - window.clearTimeout(snsUserPostCountsBatchTimerRef.current) - snsUserPostCountsBatchTimerRef.current = null - } - resetSessionMutualFriendsLoader() - setIsSessionEnriching(false) - setIsLoadingSessionCounts(false) - setSnsUserPostCountsStatus(prev => (prev === 'loading' ? 'idle' : prev)) - }, [isExportRoute, resetSessionMutualFriendsLoader]) + stopExportPageBackgroundLoaders() + }, [isExportRoute, stopExportPageBackgroundLoaders]) useEffect(() => { if (activeTab === 'official') { @@ -6808,6 +6913,7 @@ function ExportPage() { for (const session of sessions) { if (!session.hasSession) continue if (!keywordMatchedContactUsernameSet.has(session.username)) continue + if (targets[session.kind].length >= SESSION_DETAIL_BACKGROUND_METRIC_LIMIT_PER_TAB) continue targets[session.kind].push(session.username) } return targets @@ -7056,18 +7162,21 @@ function ExportPage() { ]) useEffect(() => { + if (!isExportRoute) return if (filteredContacts.length === 0) return const bootstrapTargets = filteredContacts.slice(0, 24).map((contact) => contact.username) void hydrateVisibleContactAvatars(bootstrapTargets) - }, [filteredContacts, hydrateVisibleContactAvatars]) + }, [filteredContacts, hydrateVisibleContactAvatars, isExportRoute]) useEffect(() => { + if (!isExportRoute) return const sessionId = String(sessionDetail?.wxid || '').trim() if (!sessionId) return void hydrateVisibleContactAvatars([sessionId]) - }, [hydrateVisibleContactAvatars, sessionDetail?.wxid]) + }, [hydrateVisibleContactAvatars, isExportRoute, sessionDetail?.wxid]) useEffect(() => { + if (!isExportRoute) return if (activeTaskCount > 0) return if (filteredContacts.length === 0) return const runId = sessionMediaMetricRunIdRef.current @@ -7102,9 +7211,7 @@ function ExportPage() { scheduleSessionMediaMetricWorker() } - if (cursor < filteredContacts.length) { - sessionMediaMetricBackgroundFeedTimerRef.current = window.setTimeout(feedNext, SESSION_MEDIA_METRIC_BACKGROUND_FEED_INTERVAL_MS) - } + // 限制后台预热规模;继续滚动时再按可见行补齐。 } feedNext() @@ -7119,11 +7226,13 @@ function ExportPage() { collectVisibleSessionMetricTargets, enqueueSessionMediaMetricRequests, filteredContacts, + isExportRoute, scheduleSessionMediaMetricWorker, sessionRowByUsername ]) useEffect(() => { + if (!isExportRoute) return if (activeTaskCount > 0) return const runId = sessionMediaMetricRunIdRef.current const allTargets = [ @@ -7133,7 +7242,6 @@ function ExportPage() { ] if (allTargets.length === 0) return - let timer: number | null = null let cursor = 0 const feedNext = () => { if (runId !== sessionMediaMetricRunIdRef.current) return @@ -7148,20 +7256,14 @@ function ExportPage() { enqueueSessionMediaMetricRequests(batchIds) scheduleSessionMediaMetricWorker() } - if (cursor < allTargets.length) { - timer = window.setTimeout(feedNext, SESSION_MEDIA_METRIC_BACKGROUND_FEED_INTERVAL_MS) - } + // 数据详情只展示受限目标;更多行由可见行加载按需补齐。 } feedNext() - return () => { - if (timer !== null) { - window.clearTimeout(timer) - } - } }, [ activeTaskCount, enqueueSessionMediaMetricRequests, + isExportRoute, loadDetailTargetsByTab.former_friend, loadDetailTargetsByTab.group, loadDetailTargetsByTab.private, @@ -7169,6 +7271,7 @@ function ExportPage() { ]) useEffect(() => { + if (!isExportRoute) return if (activeTaskCount > 0) return if (!isSessionCountStageReady || filteredContacts.length === 0) return const runId = sessionMutualFriendsRunIdRef.current @@ -7203,9 +7306,7 @@ function ExportPage() { scheduleSessionMutualFriendsWorker() } - if (cursor < filteredContacts.length) { - sessionMutualFriendsBackgroundFeedTimerRef.current = window.setTimeout(feedNext, SESSION_MEDIA_METRIC_BACKGROUND_FEED_INTERVAL_MS) - } + // 限制后台预热规模;继续滚动时再按可见行补齐。 } feedNext() @@ -7220,6 +7321,7 @@ function ExportPage() { collectVisibleSessionMutualFriendsTargets, enqueueSessionMutualFriendsRequests, filteredContacts, + isExportRoute, isSessionCountStageReady, scheduleSessionMutualFriendsWorker, sessionRowByUsername @@ -7318,7 +7420,7 @@ function ExportPage() { const sessionId = String(sessionDetail?.wxid || '').trim() if (!sessionId || !sessionDetailSupportsSnsTimeline) return '朋友圈:0条' - if (snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle') { + if (snsUserPostCountsStatus === 'loading') { return '朋友圈:统计中...' } if (snsUserPostCountsStatus === 'error') { @@ -7761,7 +7863,7 @@ function ExportPage() { useEffect(() => { if (!showSessionDetailPanel || !sessionDetailSupportsSnsTimeline) return if (snsUserPostCountsStatus === 'idle') { - void loadSnsUserPostCounts() + void loadSnsUserPostCounts({ cacheOnly: true }) } }, [ loadSnsUserPostCounts, @@ -7774,7 +7876,7 @@ function ExportPage() { if (!isExportRoute || !isSessionCountStageReady) return if (snsUserPostCountsStatus !== 'idle') return const timer = window.setTimeout(() => { - void loadSnsUserPostCounts() + void loadSnsUserPostCounts({ cacheOnly: true }) }, 260) return () => window.clearTimeout(timer) }, [isExportRoute, isSessionCountStageReady, loadSnsUserPostCounts, snsUserPostCountsStatus]) @@ -7789,7 +7891,7 @@ function ExportPage() { setSessionSnsTimelineStatsLoading(false) return } - if (snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle') { + if (snsUserPostCountsStatus === 'loading') { setSessionSnsTimelineStatsLoading(true) return } @@ -7843,7 +7945,7 @@ function ExportPage() { detailStatsPriorityRef.current = true setShowSessionDetailPanel(true) if (isSingleContactSession(sessionId)) { - void loadSnsUserPostCounts() + void loadSnsUserPostCounts({ cacheOnly: true }) } void loadSessionDetail(sessionId) }, [loadSessionDetail, loadSnsUserPostCounts]) @@ -7862,7 +7964,7 @@ function ExportPage() { useEffect(() => { if (!showSessionLoadDetailModal) return if (snsUserPostCountsStatus === 'idle') { - void loadSnsUserPostCounts() + void loadSnsUserPostCounts({ cacheOnly: true }) } const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { @@ -8543,8 +8645,7 @@ function ExportPage() { ( snsStageStatus === 'pending' || snsStageStatus === 'loading' || - snsUserPostCountsStatus === 'loading' || - snsUserPostCountsStatus === 'idle' + snsUserPostCountsStatus === 'loading' ) ) const snsRawCount = Number(snsUserPostCounts[contact.username] || 0) @@ -8697,7 +8798,7 @@ function ExportPage() { className={`row-sns-metric-btn row-mutual-friends-btn ${isMutualFriendsLoading ? 'loading' : ''} ${hasMutualFriendsMetric ? 'ready' : ''}`} title={`查看 ${contact.displayName || contact.username} 的共同好友`} onClick={() => openSessionMutualFriendsDialog(contact)} - disabled={!hasMutualFriendsMetric} + disabled={isMutualFriendsLoading} > {isMutualFriendsLoading ? @@ -9771,6 +9872,9 @@ function ExportPage() { ? new Date(sessionLoadDetailUpdatedAt).toLocaleString('zh-CN') : '暂无'}

+

+ 后台预热仅跟踪每类前 {SESSION_DETAIL_BACKGROUND_METRIC_LIMIT_PER_TAB} 个会话;其他行滚动到可见时按需加载。 +