From ec10f475675b49f47e62e762cf2349b3ce6449e0 Mon Sep 17 00:00:00 2001
From: Jason
Date: Sat, 30 May 2026 16:58:08 +0800
Subject: [PATCH 1/3] fix(export): stats would randomly reset to zero
---
electron/services/chatService.ts | 47 ++++++++++-
src/pages/ExportPage.tsx | 136 +++++++++++++++++++------------
src/types/electron.d.ts | 1 +
3 files changed, 129 insertions(+), 55 deletions(-)
diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts
index 6c393a0..c928d19 100644
--- a/electron/services/chatService.ts
+++ b/electron/services/chatService.ts
@@ -208,6 +208,7 @@ interface ExportSessionStatsCacheMeta {
stale: boolean
includeRelations: boolean
source: 'memory' | 'disk' | 'fresh'
+ rangeFiltered?: boolean
}
interface ExportTabCounts {
@@ -7761,7 +7762,12 @@ class ChatService {
success: true,
count: Math.max(0, Math.floor(messageCount as number))
})
- : wcdbService.getMessageCount(normalizedSessionId)
+ : this.getSessionMessageCounts([normalizedSessionId], { preferHintCache: true })
+ .then((result) => ({
+ success: result.success,
+ count: result.counts?.[normalizedSessionId],
+ error: result.error
+ }))
const [contactResult, avatarResult, messageCountResult] = await Promise.allSettled([
contactPromise,
@@ -8066,7 +8072,15 @@ class ChatService {
endTimestamp
)
resultMap[sessionId] = stats
- if (!useRangeFilter) {
+ if (useRangeFilter) {
+ cacheMeta[sessionId] = {
+ updatedAt: Date.now(),
+ stale: false,
+ includeRelations,
+ source: 'fresh',
+ rangeFiltered: true
+ }
+ } else {
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
cacheMeta[sessionId] = {
updatedAt,
@@ -8093,7 +8107,15 @@ class ChatService {
const stats = batchedStatsMap[sessionId]
if (!stats) continue
resultMap[sessionId] = stats
- if (!useRangeFilter) {
+ if (useRangeFilter) {
+ cacheMeta[sessionId] = {
+ updatedAt: Date.now(),
+ stale: false,
+ includeRelations,
+ source: 'fresh',
+ rangeFiltered: true
+ }
+ } else {
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
cacheMeta[sessionId] = {
updatedAt,
@@ -8121,7 +8143,15 @@ class ChatService {
endTimestamp
)
resultMap[sessionId] = stats
- if (!useRangeFilter) {
+ if (useRangeFilter) {
+ cacheMeta[sessionId] = {
+ updatedAt: Date.now(),
+ stale: false,
+ includeRelations,
+ source: 'fresh',
+ rangeFiltered: true
+ }
+ } else {
const updatedAt = this.setSessionStatsCacheEntry(sessionId, stats, includeRelations)
cacheMeta[sessionId] = {
updatedAt,
@@ -8132,6 +8162,15 @@ class ChatService {
}
} catch {
resultMap[sessionId] = this.buildEmptyExportSessionStats(sessionId, includeRelations)
+ if (useRangeFilter) {
+ cacheMeta[sessionId] = {
+ updatedAt: Date.now(),
+ stale: true,
+ includeRelations,
+ source: 'fresh',
+ rangeFiltered: true
+ }
+ }
}
})
}
diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx
index 42730bc..d486530 100644
--- a/src/pages/ExportPage.tsx
+++ b/src/pages/ExportPage.tsx
@@ -1384,6 +1384,7 @@ interface SessionExportCacheMeta {
stale: boolean
includeRelations: boolean
source: 'memory' | 'disk' | 'fresh'
+ rangeFiltered?: boolean
}
type SessionLoadStageStatus = 'pending' | 'loading' | 'done' | 'failed'
@@ -1602,6 +1603,12 @@ const normalizeTimestampSeconds = (value: unknown): number | undefined => {
return Math.floor(parsed)
}
+const mergeStableCount = (incoming: number | undefined, previous: number | undefined): number | undefined => {
+ if (typeof incoming !== 'number') return previous
+ if (incoming === 0 && typeof previous === 'number' && previous > 0) return previous
+ return incoming
+}
+
const clampExportSelectionToBounds = (
selection: ExportDateRangeSelection,
bounds: TimeRangeBounds | null
@@ -2369,27 +2376,6 @@ function ExportPage() {
exportConcurrency: 2
})
- const exportStatsRangeOptions = useMemo(() => {
- if (options.useAllTime || !options.dateRange) return null
- const beginTimestamp = Math.floor(options.dateRange.start.getTime() / 1000)
- const endTimestamp = Math.floor(options.dateRange.end.getTime() / 1000)
- if (!Number.isFinite(beginTimestamp) || !Number.isFinite(endTimestamp)) return null
- if (beginTimestamp <= 0 && endTimestamp <= 0) return null
- return {
- beginTimestamp: Math.max(0, beginTimestamp),
- endTimestamp: Math.max(0, endTimestamp)
- }
- }, [options.useAllTime, options.dateRange])
-
- const withExportStatsRange = useCallback((statsOptions: Record): Record => {
- if (!exportStatsRangeOptions) return statsOptions
- return {
- ...statsOptions,
- beginTimestamp: exportStatsRangeOptions.beginTimestamp,
- endTimestamp: exportStatsRangeOptions.endTimestamp
- }
- }, [exportStatsRangeOptions])
-
const [exportDialog, setExportDialog] = useState({
open: false,
intent: 'manual',
@@ -3673,9 +3659,16 @@ function ExportPage() {
return []
}, [sessionSnsCommentRankings, sessionSnsLikeRankings, sessionSnsRankMode])
- const mergeSessionContentMetrics = useCallback((input: Record) => {
+ const mergeSessionContentMetrics = useCallback((
+ input: Record,
+ options?: {
+ mergeTotalMessages?: boolean
+ preserveExistingTotalOnZero?: boolean
+ }
+ ) => {
const entries = Object.entries(input)
if (entries.length === 0) return
+ const mergeTotalMessages = options?.mergeTotalMessages !== false
const nextMessageCounts: Record = {}
const nextMetrics: Record = {}
@@ -3683,7 +3676,13 @@ function ExportPage() {
for (const [sessionIdRaw, metricRaw] of entries) {
const sessionId = String(sessionIdRaw || '').trim()
if (!sessionId || !metricRaw) continue
- const totalMessages = normalizeMessageCount(metricRaw.totalMessages)
+ const previous = sessionContentMetricsRef.current[sessionId] || {}
+ const incomingTotalMessages = normalizeMessageCount(metricRaw.totalMessages)
+ const totalMessages = mergeTotalMessages
+ ? (options?.preserveExistingTotalOnZero
+ ? mergeStableCount(incomingTotalMessages, previous.totalMessages)
+ : incomingTotalMessages)
+ : undefined
const voiceMessages = normalizeMessageCount(metricRaw.voiceMessages)
const imageMessages = normalizeMessageCount(metricRaw.imageMessages)
const videoMessages = normalizeMessageCount(metricRaw.videoMessages)
@@ -3729,8 +3728,12 @@ function ExportPage() {
let changed = false
const merged = { ...prev }
for (const [sessionId, count] of Object.entries(nextMessageCounts)) {
- if (merged[sessionId] === count) continue
- merged[sessionId] = count
+ const previousCount = normalizeMessageCount(merged[sessionId])
+ const nextCount = options?.preserveExistingTotalOnZero
+ ? mergeStableCount(count, previousCount)
+ : count
+ if (typeof nextCount !== 'number' || previousCount === nextCount) continue
+ merged[sessionId] = nextCount
changed = true
}
return changed ? merged : prev
@@ -3744,7 +3747,11 @@ function ExportPage() {
for (const [sessionId, metric] of Object.entries(nextMetrics)) {
const previous = merged[sessionId] || {}
const nextMetric: SessionContentMetric = {
- totalMessages: typeof metric.totalMessages === 'number' ? metric.totalMessages : previous.totalMessages,
+ totalMessages: mergeTotalMessages
+ ? (options?.preserveExistingTotalOnZero
+ ? mergeStableCount(metric.totalMessages, previous.totalMessages)
+ : (typeof metric.totalMessages === 'number' ? metric.totalMessages : previous.totalMessages))
+ : previous.totalMessages,
voiceMessages: typeof metric.voiceMessages === 'number' ? metric.voiceMessages : previous.voiceMessages,
imageMessages: typeof metric.imageMessages === 'number' ? metric.imageMessages : previous.imageMessages,
videoMessages: typeof metric.videoMessages === 'number' ? metric.videoMessages : previous.videoMessages,
@@ -4084,7 +4091,10 @@ function ExportPage() {
}
}, [isSessionMediaMetricReady, patchSessionLoadTraceStage])
- const applySessionMediaMetricsFromStats = useCallback((data?: Record) => {
+ const applySessionMediaMetricsFromStats = useCallback((
+ data?: Record,
+ cache?: Record
+ ) => {
if (!data) return
const nextMetrics: Record = {}
let hasPatch = false
@@ -4093,19 +4103,25 @@ function ExportPage() {
if (!sessionId) continue
const metric = pickSessionMediaMetric(metricRaw)
if (!metric) continue
- nextMetrics[sessionId] = metric
+ const metricForMerge = cache?.[sessionId]?.rangeFiltered
+ ? (() => {
+ const { totalMessages: _totalMessages, ...rest } = metric
+ return rest
+ })()
+ : metric
+ nextMetrics[sessionId] = metricForMerge
hasPatch = true
sessionMediaMetricPendingPersistRef.current[sessionId] = {
...sessionMediaMetricPendingPersistRef.current[sessionId],
- ...metric
+ ...metricForMerge
}
- if (hasCompleteSessionMediaMetric(metric)) {
+ if (hasCompleteSessionMediaMetric(metricForMerge)) {
sessionMediaMetricReadySetRef.current.add(sessionId)
}
}
if (hasPatch) {
- mergeSessionContentMetrics(nextMetrics)
+ mergeSessionContentMetrics(nextMetrics, { preserveExistingTotalOnZero: true })
scheduleFlushSessionMediaMetricCache()
}
}, [mergeSessionContentMetrics, scheduleFlushSessionMediaMetricCache])
@@ -4155,14 +4171,17 @@ function ExportPage() {
const cacheResult = await withTimeout(
window.electronAPI.chat.getExportSessionStats(
batchSessionIds,
- withExportStatsRange({ includeRelations: false, allowStaleCache: true, cacheOnly: true })
+ { includeRelations: false, allowStaleCache: true, cacheOnly: true }
),
12000,
'cacheOnly'
)
if (runId !== sessionMediaMetricRunIdRef.current) return
if (cacheResult.success && cacheResult.data) {
- applySessionMediaMetricsFromStats(cacheResult.data as Record)
+ applySessionMediaMetricsFromStats(
+ cacheResult.data as Record,
+ cacheResult.cache as Record | undefined
+ )
}
const missingSessionIds = batchSessionIds.filter(sessionId => !isSessionMediaMetricReady(sessionId))
@@ -4170,14 +4189,17 @@ function ExportPage() {
const freshResult = await withTimeout(
window.electronAPI.chat.getExportSessionStats(
missingSessionIds,
- withExportStatsRange({ includeRelations: false, allowStaleCache: true })
+ { includeRelations: false, allowStaleCache: true }
),
45000,
'fresh'
)
if (runId !== sessionMediaMetricRunIdRef.current) return
if (freshResult.success && freshResult.data) {
- applySessionMediaMetricsFromStats(freshResult.data as Record)
+ applySessionMediaMetricsFromStats(
+ freshResult.data as Record,
+ freshResult.cache as Record | undefined
+ )
}
}
@@ -4214,7 +4236,7 @@ function ExportPage() {
void runSessionMediaMetricWorker(runId)
}
}
- }, [applySessionMediaMetricsFromStats, isSessionMediaMetricReady, patchSessionLoadTraceStage, withExportStatsRange])
+ }, [applySessionMediaMetricsFromStats, isSessionMediaMetricReady, patchSessionLoadTraceStage])
const scheduleSessionMediaMetricWorker = useCallback(() => {
if (activeTaskCountRef.current > 0) return
@@ -5051,9 +5073,10 @@ function ExportPage() {
const applyStatsResult = (result?: {
success: boolean
data?: Record
+ cache?: Record
} | null) => {
if (!result?.success || !result.data) return
- applySessionMediaMetricsFromStats(result.data)
+ applySessionMediaMetricsFromStats(result.data, result.cache)
for (const sessionId of normalizedSessionIds) {
absorbMetric(sessionId, result.data[sessionId])
}
@@ -7326,13 +7349,21 @@ function ExportPage() {
cacheMeta?: SessionExportCacheMeta,
relationLoadedOverride?: boolean
) => {
- mergeSessionContentMetrics({ [sessionId]: metric })
+ const isRangeFilteredMetric = cacheMeta?.rangeFiltered === true
+ mergeSessionContentMetrics({ [sessionId]: metric }, {
+ mergeTotalMessages: !isRangeFilteredMetric,
+ preserveExistingTotalOnZero: true
+ })
setSessionDetail((prev) => {
if (!prev || prev.wxid !== sessionId) return prev
const relationLoaded = relationLoadedOverride ?? Boolean(prev.relationStatsLoaded)
+ const messageCount = mergeStableCount(
+ !isRangeFilteredMetric && Number.isFinite(metric.totalMessages) ? metric.totalMessages : undefined,
+ prev.messageCount
+ )
return {
...prev,
- messageCount: Number.isFinite(metric.totalMessages) ? metric.totalMessages : prev.messageCount,
+ messageCount: Number.isFinite(messageCount) ? messageCount as number : prev.messageCount,
voiceMessages: Number.isFinite(metric.voiceMessages) ? metric.voiceMessages : prev.voiceMessages,
imageMessages: Number.isFinite(metric.imageMessages) ? metric.imageMessages : prev.imageMessages,
videoMessages: Number.isFinite(metric.videoMessages) ? metric.videoMessages : prev.videoMessages,
@@ -7364,8 +7395,6 @@ function ExportPage() {
const preciseCacheKey = `${exportCacheScopeRef.current}::${normalizedSessionId}`
detailStatsPriorityRef.current = true
- sessionCountRequestIdRef.current += 1
- setIsLoadingSessionCounts(false)
const requestSeq = ++detailRequestSeqRef.current
const mappedSession = sessionRowByUsername.get(normalizedSessionId)
@@ -7428,16 +7457,19 @@ function ExportPage() {
const fastMessageCount = normalizeMessageCount(result.detail.messageCount)
if (typeof fastMessageCount === 'number') {
setSessionMessageCounts((prev) => {
- if (prev[normalizedSessionId] === fastMessageCount) return prev
+ const nextCount = mergeStableCount(fastMessageCount, normalizeMessageCount(prev[normalizedSessionId]))
+ if (typeof nextCount !== 'number' || prev[normalizedSessionId] === nextCount) return prev
return {
...prev,
- [normalizedSessionId]: fastMessageCount
+ [normalizedSessionId]: nextCount
}
})
mergeSessionContentMetrics({
[normalizedSessionId]: {
totalMessages: fastMessageCount
}
+ }, {
+ preserveExistingTotalOnZero: true
})
}
setSessionDetail((prev) => ({
@@ -7447,7 +7479,9 @@ function ExportPage() {
nickName: result.detail!.nickName ?? prev?.nickName,
alias: result.detail!.alias ?? prev?.alias,
avatarUrl: result.detail!.avatarUrl || prev?.avatarUrl,
- messageCount: Number.isFinite(result.detail!.messageCount) ? result.detail!.messageCount : prev?.messageCount ?? Number.NaN,
+ messageCount: Number.isFinite(result.detail!.messageCount)
+ ? mergeStableCount(result.detail!.messageCount, prev?.messageCount) ?? Number.NaN
+ : prev?.messageCount ?? Number.NaN,
voiceMessages: prev?.voiceMessages,
imageMessages: prev?.imageMessages,
videoMessages: prev?.videoMessages,
@@ -7507,7 +7541,7 @@ function ExportPage() {
try {
const quickStatsResult = await window.electronAPI.chat.getExportSessionStats(
[normalizedSessionId],
- withExportStatsRange({ includeRelations: false, allowStaleCache: true, cacheOnly: true })
+ { includeRelations: false, allowStaleCache: true, cacheOnly: true }
)
if (requestSeq !== detailRequestSeqRef.current) return
if (quickStatsResult.success) {
@@ -7534,7 +7568,7 @@ function ExportPage() {
try {
const relationCacheResult = await window.electronAPI.chat.getExportSessionStats(
[normalizedSessionId],
- withExportStatsRange({ includeRelations: true, allowStaleCache: true, cacheOnly: true })
+ { includeRelations: true, allowStaleCache: true, cacheOnly: true }
)
if (requestSeq !== detailRequestSeqRef.current) return
if (relationCacheResult.success && relationCacheResult.data) {
@@ -7559,7 +7593,7 @@ function ExportPage() {
// 后台补齐非关系统计,不走精确特型扫描,避免阻塞列表统计队列。
const freshResult = await window.electronAPI.chat.getExportSessionStats(
[normalizedSessionId],
- withExportStatsRange({ includeRelations: false, forceRefresh: true })
+ { includeRelations: false, forceRefresh: true }
)
if (requestSeq !== detailRequestSeqRef.current) return
if (freshResult.success && freshResult.data) {
@@ -7594,7 +7628,7 @@ function ExportPage() {
setIsLoadingSessionDetailExtra(false)
}
}
- }, [applySessionDetailStats, contactByUsername, mergeSessionContentMetrics, sessionContentMetrics, sessionMessageCounts, sessionRowByUsername, withExportStatsRange])
+ }, [applySessionDetailStats, contactByUsername, mergeSessionContentMetrics, sessionContentMetrics, sessionMessageCounts, sessionRowByUsername])
const loadSessionRelationStats = useCallback(async (options?: { forceRefresh?: boolean }) => {
const normalizedSessionId = String(sessionDetail?.wxid || '').trim()
@@ -7607,7 +7641,7 @@ function ExportPage() {
if (!forceRefresh) {
const relationCacheResult = await window.electronAPI.chat.getExportSessionStats(
[normalizedSessionId],
- withExportStatsRange({ includeRelations: true, allowStaleCache: true, cacheOnly: true })
+ { includeRelations: true, allowStaleCache: true, cacheOnly: true }
)
if (requestSeq !== detailRequestSeqRef.current) return
@@ -7625,7 +7659,7 @@ function ExportPage() {
const relationResult = await window.electronAPI.chat.getExportSessionStats(
[normalizedSessionId],
- withExportStatsRange({ includeRelations: true, forceRefresh, preferAccurateSpecialTypes: true })
+ { includeRelations: true, forceRefresh, preferAccurateSpecialTypes: true }
)
if (requestSeq !== detailRequestSeqRef.current) return
@@ -7645,7 +7679,7 @@ function ExportPage() {
setIsLoadingSessionRelationStats(false)
}
}
- }, [applySessionDetailStats, isLoadingSessionRelationStats, sessionDetail?.wxid, withExportStatsRange])
+ }, [applySessionDetailStats, isLoadingSessionRelationStats, sessionDetail?.wxid])
const handleRefreshTableData = useCallback(async () => {
const scopeKey = await ensureExportCacheScope()
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts
index b927e4f..b29454e 100644
--- a/src/types/electron.d.ts
+++ b/src/types/electron.d.ts
@@ -654,6 +654,7 @@ export interface ElectronAPI {
stale: boolean
includeRelations: boolean
source: 'memory' | 'disk' | 'fresh'
+ rangeFiltered?: boolean
}>
needsRefresh?: string[]
error?: string
From b063ed299bf27946d7ff1da3be0abadd13c3430f Mon Sep 17 00:00:00 2001
From: Jason
Date: Sat, 30 May 2026 18:10:59 +0800
Subject: [PATCH 2/3] 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} 个会话;其他行滚动到可见时按需加载。
+
)}
-