mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-06-01 23:26:55 +00:00
fix(export): stop background stats when hidden, fix loading state, optimize cache and memory usage
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ContentType, string> = {
|
||||
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<string | undefined | null>): Set<string> => {
|
||||
const set = new Set<string>()
|
||||
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<string, ContactInfo> = {}
|
||||
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<string, configService.ContactsAvatarCacheEntry>
|
||||
): {
|
||||
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
|
||||
changed: boolean
|
||||
} => {
|
||||
const nextCache: Record<string, configService.ContactsAvatarCacheEntry> = {}
|
||||
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<string, configService.ContactsAvatarCacheEntry>
|
||||
@@ -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<string>()
|
||||
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<ConversationTab>('private')
|
||||
const detailStatsPriorityRef = useRef(false)
|
||||
const sessionSnsTimelinePostsRef = useRef<SnsPost[]>([])
|
||||
@@ -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 <T,>(promise: Promise<T>, timeoutMs: number, stage: string): Promise<T> => {
|
||||
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
|
||||
? <Loader2 size={12} className="spin row-media-metric-icon" aria-label="共同好友统计加载中" />
|
||||
@@ -9771,6 +9872,9 @@ function ExportPage() {
|
||||
? new Date(sessionLoadDetailUpdatedAt).toLocaleString('zh-CN')
|
||||
: '暂无'}
|
||||
</p>
|
||||
<p>
|
||||
后台预热仅跟踪每类前 {SESSION_DETAIL_BACKGROUND_METRIC_LIMIT_PER_TAB} 个会话;其他行滚动到可见时按需加载。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="session-load-detail-close"
|
||||
|
||||
Reference in New Issue
Block a user