refactor(export): remove session stats columns and background counting

This commit is contained in:
tisonhuang
2026-03-02 11:12:09 +08:00
parent 01a221831f
commit b3700c3a4c

View File

@@ -59,21 +59,6 @@ interface SessionRow extends AppChatSession {
wechatId?: string wechatId?: string
} }
interface SessionMetrics {
totalMessages?: number
voiceMessages?: number
imageMessages?: number
videoMessages?: number
emojiMessages?: number
privateMutualGroups?: number
groupMemberCount?: number
groupMyMessages?: number
groupActiveSpeakers?: number
groupMutualFriends?: number
firstTimestamp?: number
lastTimestamp?: number
}
interface TaskProgress { interface TaskProgress {
current: number current: number
total: number total: number
@@ -231,26 +216,8 @@ const getAvatarLetter = (name: string): string => {
return [...name][0] || '?' return [...name][0] || '?'
} }
const valueOrDash = (value?: number): string => {
if (value === undefined || value === null) return '--'
return value.toLocaleString()
}
const timestampOrDash = (timestamp?: number): string => {
if (!timestamp) return '--'
return formatAbsoluteDate(timestamp * 1000)
}
const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const MESSAGE_COUNT_VIEWPORT_PREFETCH = 90
const MESSAGE_COUNT_ACTIVE_TAB_WARMUP_LIMIT = 240
const MESSAGE_COUNT_REQUEST_BATCH = 120
const METRICS_VIEWPORT_PREFETCH = 60
const METRICS_REQUEST_BATCH = 24
const METRICS_BACKGROUND_BATCH = 20
const METRICS_BACKGROUND_INTERVAL_MS = 500
const CONTACT_ENRICH_TIMEOUT_MS = 7000 const CONTACT_ENRICH_TIMEOUT_MS = 7000
const EXPORT_SESSION_COUNT_CACHE_STALE_MS = 48 * 60 * 60 * 1000
const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000 const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000
const withTimeout = async <T,>(promise: Promise<T>, timeoutMs: number): Promise<T | null> => { const withTimeout = async <T,>(promise: Promise<T>, timeoutMs: number): Promise<T | null> => {
@@ -333,8 +300,6 @@ function ExportPage() {
const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true) const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true)
const [isTaskCenterExpanded, setIsTaskCenterExpanded] = useState(false) const [isTaskCenterExpanded, setIsTaskCenterExpanded] = useState(false)
const [sessions, setSessions] = useState<SessionRow[]>([]) const [sessions, setSessions] = useState<SessionRow[]>([])
const [sessionMessageCounts, setSessionMessageCounts] = useState<Record<string, number>>({})
const [sessionMetrics, setSessionMetrics] = useState<Record<string, SessionMetrics>>({})
const [searchKeyword, setSearchKeyword] = useState('') const [searchKeyword, setSearchKeyword] = useState('')
const [activeTab, setActiveTab] = useState<ConversationTab>('private') const [activeTab, setActiveTab] = useState<ConversationTab>('private')
const [selectedSessions, setSelectedSessions] = useState<Set<string>>(new Set()) const [selectedSessions, setSelectedSessions] = useState<Set<string>>(new Set())
@@ -390,21 +355,10 @@ function ExportPage() {
const runningTaskIdRef = useRef<string | null>(null) const runningTaskIdRef = useRef<string | null>(null)
const tasksRef = useRef<ExportTask[]>([]) const tasksRef = useRef<ExportTask[]>([])
const hasSeededSnsStatsRef = useRef(false) const hasSeededSnsStatsRef = useRef(false)
const sessionMessageCountsRef = useRef<Record<string, number>>({})
const sessionMetricsRef = useRef<Record<string, SessionMetrics>>({})
const sessionLoadTokenRef = useRef(0) const sessionLoadTokenRef = useRef(0)
const loadingMessageCountsRef = useRef<Set<string>>(new Set())
const loadingMetricsRef = useRef<Set<string>>(new Set())
const pendingMessageCountsRef = useRef<Set<string>>(new Set())
const pendingMetricsRef = useRef<Set<string>>(new Set())
const messageCountPumpRunningRef = useRef(false)
const metricsPumpRunningRef = useRef(false)
const isExportRouteRef = useRef(isExportRoute)
const preselectAppliedRef = useRef(false) const preselectAppliedRef = useRef(false)
const visibleSessionsRef = useRef<SessionRow[]>([])
const exportCacheScopeRef = useRef('default') const exportCacheScopeRef = useRef('default')
const exportCacheScopeReadyRef = useRef(false) const exportCacheScopeReadyRef = useRef(false)
const persistSessionCountTimerRef = useRef<number | null>(null)
useEffect(() => { useEffect(() => {
tasksRef.current = tasks tasksRef.current = tasks
@@ -414,42 +368,6 @@ function ExportPage() {
hasSeededSnsStatsRef.current = hasSeededSnsStats hasSeededSnsStatsRef.current = hasSeededSnsStats
}, [hasSeededSnsStats]) }, [hasSeededSnsStats])
useEffect(() => {
sessionMessageCountsRef.current = sessionMessageCounts
}, [sessionMessageCounts])
useEffect(() => {
sessionMetricsRef.current = sessionMetrics
}, [sessionMetrics])
useEffect(() => {
isExportRouteRef.current = isExportRoute
}, [isExportRoute])
useEffect(() => {
if (persistSessionCountTimerRef.current) {
window.clearTimeout(persistSessionCountTimerRef.current)
persistSessionCountTimerRef.current = null
}
if (isBaseConfigLoading || !exportCacheScopeReadyRef.current) return
const countSize = Object.keys(sessionMessageCounts).length
if (countSize === 0) return
persistSessionCountTimerRef.current = window.setTimeout(() => {
void configService.setExportSessionMessageCountCache(exportCacheScopeRef.current, sessionMessageCounts)
persistSessionCountTimerRef.current = null
}, 900)
return () => {
if (persistSessionCountTimerRef.current) {
window.clearTimeout(persistSessionCountTimerRef.current)
persistSessionCountTimerRef.current = null
}
}
}, [sessionMessageCounts, isBaseConfigLoading])
const preselectSessionIds = useMemo(() => { const preselectSessionIds = useMemo(() => {
const state = location.state as { preselectSessionIds?: unknown; preselectSessionId?: unknown } | null const state = location.state as { preselectSessionIds?: unknown; preselectSessionId?: unknown } | null
const rawList = Array.isArray(state?.preselectSessionIds) const rawList = Array.isArray(state?.preselectSessionIds)
@@ -490,10 +408,7 @@ function ExportPage() {
exportCacheScopeRef.current = exportCacheScope exportCacheScopeRef.current = exportCacheScope
exportCacheScopeReadyRef.current = true exportCacheScopeReadyRef.current = true
const [cachedSessionCountMap, cachedSnsStats] = await Promise.all([ const cachedSnsStats = await configService.getExportSnsStatsCache(exportCacheScope)
configService.getExportSessionMessageCountCache(exportCacheScope),
configService.getExportSnsStatsCache(exportCacheScope)
])
if (savedPath) { if (savedPath) {
setExportFolder(savedPath) setExportFolder(savedPath)
@@ -507,10 +422,6 @@ function ExportPage() {
setLastExportByContent(savedContentMap) setLastExportByContent(savedContentMap)
setLastSnsExportPostCount(savedSnsPostCount) setLastSnsExportPostCount(savedSnsPostCount)
if (cachedSessionCountMap && Date.now() - cachedSessionCountMap.updatedAt <= EXPORT_SESSION_COUNT_CACHE_STALE_MS) {
setSessionMessageCounts(cachedSessionCountMap.counts || {})
}
if (cachedSnsStats && Date.now() - cachedSnsStats.updatedAt <= EXPORT_SNS_STATS_CACHE_STALE_MS) { if (cachedSnsStats && Date.now() - cachedSnsStats.updatedAt <= EXPORT_SNS_STATS_CACHE_STALE_MS) {
setSnsStats({ setSnsStats({
totalPosts: cachedSnsStats.totalPosts || 0, totalPosts: cachedSnsStats.totalPosts || 0,
@@ -591,12 +502,6 @@ function ExportPage() {
sessionLoadTokenRef.current = loadToken sessionLoadTokenRef.current = loadToken
setIsLoading(true) setIsLoading(true)
setIsSessionEnriching(false) setIsSessionEnriching(false)
loadingMessageCountsRef.current.clear()
loadingMetricsRef.current.clear()
pendingMessageCountsRef.current.clear()
pendingMetricsRef.current.clear()
sessionMetricsRef.current = {}
setSessionMetrics({})
const isStale = () => sessionLoadTokenRef.current !== loadToken const isStale = () => sessionLoadTokenRef.current !== loadToken
@@ -626,20 +531,6 @@ function ExportPage() {
if (isStale()) return if (isStale()) return
setSessions(baseSessions) setSessions(baseSessions)
setSessionMessageCounts(prev => {
const next: Record<string, number> = {}
for (const session of baseSessions) {
const count = prev[session.username]
if (typeof count === 'number') {
next[session.username] = count
continue
}
if (typeof session.messageCountHint === 'number' && Number.isFinite(session.messageCountHint) && session.messageCountHint >= 0) {
next[session.username] = Math.floor(session.messageCountHint)
}
}
return next
})
setIsLoading(false) setIsLoading(false)
// 后台补齐联系人字段(昵称、头像、类型),不阻塞首屏会话列表渲染。 // 后台补齐联系人字段(昵称、头像、类型),不阻塞首屏会话列表渲染。
@@ -726,12 +617,8 @@ function ExportPage() {
useEffect(() => { useEffect(() => {
if (isExportRoute) return if (isExportRoute) return
// 导出页隐藏时停止后台统计请求,避免与通讯录页面查询抢占。 // 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。
sessionLoadTokenRef.current = Date.now() sessionLoadTokenRef.current = Date.now()
loadingMessageCountsRef.current.clear()
loadingMetricsRef.current.clear()
pendingMessageCountsRef.current.clear()
pendingMetricsRef.current.clear()
setIsSessionEnriching(false) setIsSessionEnriching(false)
}, [isExportRoute]) }, [isExportRoute])
@@ -764,227 +651,11 @@ function ExportPage() {
) )
}) })
.sort((a, b) => { .sort((a, b) => {
const totalA = sessionMessageCounts[a.username] const latestA = a.sortTimestamp || a.lastTimestamp || 0
const totalB = sessionMessageCounts[b.username] const latestB = b.sortTimestamp || b.lastTimestamp || 0
const hasTotalA = typeof totalA === 'number'
const hasTotalB = typeof totalB === 'number'
if (hasTotalA && hasTotalB && totalB !== totalA) {
return totalB - totalA
}
if (hasTotalA !== hasTotalB) {
return hasTotalA ? -1 : 1
}
const latestA = sessionMetrics[a.username]?.lastTimestamp ?? a.lastTimestamp ?? 0
const latestB = sessionMetrics[b.username]?.lastTimestamp ?? b.lastTimestamp ?? 0
return latestB - latestA return latestB - latestA
}) })
}, [sessions, activeTab, searchKeyword, sessionMessageCounts, sessionMetrics]) }, [sessions, activeTab, searchKeyword])
useEffect(() => {
visibleSessionsRef.current = visibleSessions
}, [visibleSessions])
const ensureSessionMessageCounts = useCallback(async (targetSessions: SessionRow[]) => {
if (!isExportRouteRef.current) return
const currentCounts = sessionMessageCountsRef.current
for (const session of targetSessions) {
if (currentCounts[session.username] !== undefined) continue
if (loadingMessageCountsRef.current.has(session.username)) continue
pendingMessageCountsRef.current.add(session.username)
}
if (pendingMessageCountsRef.current.size === 0 || messageCountPumpRunningRef.current) return
messageCountPumpRunningRef.current = true
const loadTokenAtStart = sessionLoadTokenRef.current
try {
while (isExportRouteRef.current && loadTokenAtStart === sessionLoadTokenRef.current) {
const ids = Array.from(pendingMessageCountsRef.current).slice(0, MESSAGE_COUNT_REQUEST_BATCH)
if (ids.length === 0) break
for (const id of ids) {
pendingMessageCountsRef.current.delete(id)
loadingMessageCountsRef.current.add(id)
}
const chunkUpdates: Record<string, number> = {}
try {
const result = await withTimeout(window.electronAPI.chat.getSessionMessageCounts(ids), 10000)
if (!result) {
for (const id of ids) {
chunkUpdates[id] = 0
}
} else {
for (const id of ids) {
const value = result?.success && result.counts ? result.counts[id] : undefined
chunkUpdates[id] = typeof value === 'number' ? value : 0
}
}
} catch (error) {
console.error('加载会话总消息数失败:', error)
for (const id of ids) {
chunkUpdates[id] = 0
}
} finally {
for (const id of ids) {
loadingMessageCountsRef.current.delete(id)
}
}
if (loadTokenAtStart === sessionLoadTokenRef.current && Object.keys(chunkUpdates).length > 0) {
setSessionMessageCounts(prev => ({ ...prev, ...chunkUpdates }))
}
}
} finally {
messageCountPumpRunningRef.current = false
}
}, [])
const ensureSessionMetrics = useCallback(async (targetSessions: SessionRow[]) => {
if (!isExportRouteRef.current) return
const currentMetrics = sessionMetricsRef.current
for (const session of targetSessions) {
if (currentMetrics[session.username]) continue
if (loadingMetricsRef.current.has(session.username)) continue
pendingMetricsRef.current.add(session.username)
}
if (pendingMetricsRef.current.size === 0 || metricsPumpRunningRef.current) return
metricsPumpRunningRef.current = true
const loadTokenAtStart = sessionLoadTokenRef.current
try {
while (isExportRouteRef.current && loadTokenAtStart === sessionLoadTokenRef.current) {
const ids = Array.from(pendingMetricsRef.current).slice(0, METRICS_REQUEST_BATCH)
if (ids.length === 0) break
for (const id of ids) {
pendingMetricsRef.current.delete(id)
loadingMetricsRef.current.add(id)
}
const updates: Record<string, SessionMetrics> = {}
try {
const statsResult = await window.electronAPI.chat.getExportSessionStats(ids)
if (!statsResult.success || !statsResult.data) {
console.error('加载会话统计失败:', statsResult.error || '未知错误')
for (const id of ids) {
updates[id] = {
totalMessages: 0,
voiceMessages: 0,
imageMessages: 0,
videoMessages: 0,
emojiMessages: 0
}
}
} else {
for (const id of ids) {
const raw = statsResult.data[id]
// 成功响应但无明细时按 0 回填,避免该行反复重试导致滚动抖动。
updates[id] = {
totalMessages: raw?.totalMessages ?? 0,
voiceMessages: raw?.voiceMessages ?? 0,
imageMessages: raw?.imageMessages ?? 0,
videoMessages: raw?.videoMessages ?? 0,
emojiMessages: raw?.emojiMessages ?? 0,
privateMutualGroups: raw?.privateMutualGroups,
groupMemberCount: raw?.groupMemberCount,
groupMyMessages: raw?.groupMyMessages,
groupActiveSpeakers: raw?.groupActiveSpeakers,
groupMutualFriends: raw?.groupMutualFriends,
firstTimestamp: raw?.firstTimestamp,
lastTimestamp: raw?.lastTimestamp
}
}
}
} catch (error) {
console.error('加载会话统计分批失败:', error)
for (const id of ids) {
updates[id] = {
totalMessages: 0,
voiceMessages: 0,
imageMessages: 0,
videoMessages: 0,
emojiMessages: 0
}
}
} finally {
for (const id of ids) {
loadingMetricsRef.current.delete(id)
}
}
if (loadTokenAtStart === sessionLoadTokenRef.current && Object.keys(updates).length > 0) {
setSessionMetrics(prev => ({ ...prev, ...updates }))
}
}
} catch (error) {
console.error('加载会话统计失败:', error)
} finally {
metricsPumpRunningRef.current = false
}
}, [])
useEffect(() => {
if (!isExportRoute) return
const targets = visibleSessions.slice(0, MESSAGE_COUNT_VIEWPORT_PREFETCH)
void ensureSessionMessageCounts(targets)
}, [isExportRoute, visibleSessions, ensureSessionMessageCounts])
useEffect(() => {
if (!isExportRoute) return
if (sessions.length === 0) return
const activeTabTargets = sessions
.filter(session => session.kind === activeTab)
.sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0))
.slice(0, MESSAGE_COUNT_ACTIVE_TAB_WARMUP_LIMIT)
if (activeTabTargets.length === 0) return
void ensureSessionMessageCounts(activeTabTargets)
}, [isExportRoute, sessions, activeTab, ensureSessionMessageCounts])
useEffect(() => {
if (!isExportRoute) return
const targets = visibleSessions.slice(0, METRICS_VIEWPORT_PREFETCH)
void ensureSessionMetrics(targets)
}, [isExportRoute, visibleSessions, ensureSessionMetrics])
const handleTableRangeChanged = useCallback((range: { startIndex: number; endIndex: number }) => {
if (!isExportRoute) return
const current = visibleSessionsRef.current
if (current.length === 0) return
const prefetch = Math.max(MESSAGE_COUNT_VIEWPORT_PREFETCH, METRICS_VIEWPORT_PREFETCH)
const start = Math.max(0, range.startIndex - prefetch)
const end = Math.min(current.length - 1, range.endIndex + prefetch)
if (end < start) return
const rangeSessions = current.slice(start, end + 1)
void ensureSessionMessageCounts(rangeSessions)
void ensureSessionMetrics(rangeSessions)
}, [isExportRoute, ensureSessionMessageCounts, ensureSessionMetrics])
useEffect(() => {
if (!isExportRoute) return
if (sessions.length === 0) return
const prioritySessions = [
...sessions.filter(session => session.kind === activeTab),
...sessions.filter(session => session.kind !== activeTab)
]
let cursor = 0
const timer = window.setInterval(() => {
if (cursor >= prioritySessions.length) {
window.clearInterval(timer)
return
}
const chunk = prioritySessions.slice(cursor, cursor + METRICS_BACKGROUND_BATCH)
cursor += METRICS_BACKGROUND_BATCH
void ensureSessionMetrics(chunk)
}, METRICS_BACKGROUND_INTERVAL_MS)
return () => window.clearInterval(timer)
}, [isExportRoute, sessions, activeTab, ensureSessionMetrics])
const selectedCount = selectedSessions.size const selectedCount = selectedSessions.size
@@ -1519,64 +1190,16 @@ function ExportPage() {
} }
const renderTableHeader = () => { const renderTableHeader = () => {
if (activeTab === 'private' || activeTab === 'former_friend') {
return (
<tr>
<th className="sticky-col"></th>
<th>//</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th className="sticky-right"></th>
</tr>
)
}
if (activeTab === 'group') {
return (
<tr>
<th className="sticky-col"></th>
<th>//ID</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th className="sticky-right"></th>
</tr>
)
}
return ( return (
<tr> <tr>
<th className="sticky-col"></th> <th className="sticky-col"></th>
<th>//</th> <th>//</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th className="sticky-right"></th> <th className="sticky-right"></th>
</tr> </tr>
) )
} }
const renderRowCells = (session: SessionRow) => { const renderRowCells = (session: SessionRow) => {
const metrics = sessionMetrics[session.username]
const totalMessages = sessionMessageCounts[session.username]
const checked = selectedSessions.has(session.username) const checked = selectedSessions.has(session.username)
return ( return (
@@ -1592,46 +1215,6 @@ function ExportPage() {
</td> </td>
<td>{renderSessionName(session)}</td> <td>{renderSessionName(session)}</td>
<td>
{typeof totalMessages === 'number'
? totalMessages.toLocaleString()
: (
<span className="count-loading">
<span className="animated-ellipsis" aria-hidden="true">...</span>
</span>
)}
</td>
<td>{valueOrDash(metrics?.voiceMessages)}</td>
<td>{valueOrDash(metrics?.imageMessages)}</td>
<td>{valueOrDash(metrics?.videoMessages)}</td>
<td>{valueOrDash(metrics?.emojiMessages)}</td>
{(activeTab === 'private' || activeTab === 'former_friend') && (
<>
<td>{valueOrDash(metrics?.privateMutualGroups)}</td>
<td>{timestampOrDash(metrics?.firstTimestamp)}</td>
<td>{timestampOrDash(metrics?.lastTimestamp)}</td>
</>
)}
{activeTab === 'group' && (
<>
<td>{valueOrDash(metrics?.groupMyMessages)}</td>
<td>{valueOrDash(metrics?.groupMemberCount)}</td>
<td>{valueOrDash(metrics?.groupActiveSpeakers)}</td>
<td>{valueOrDash(metrics?.groupMutualFriends)}</td>
<td>{timestampOrDash(metrics?.firstTimestamp)}</td>
<td>{timestampOrDash(metrics?.lastTimestamp)}</td>
</>
)}
{activeTab === 'official' && (
<>
<td>{timestampOrDash(metrics?.firstTimestamp)}</td>
<td>{timestampOrDash(metrics?.lastTimestamp)}</td>
</>
)}
<td className="sticky-right">{renderActionCell(session)}</td> <td className="sticky-right">{renderActionCell(session)}</td>
</> </>
) )
@@ -1872,7 +1455,7 @@ function ExportPage() {
{!showInitialSkeleton && (isLoading || isSessionEnriching) && ( {!showInitialSkeleton && (isLoading || isSessionEnriching) && (
<div className="table-stage-hint"> <div className="table-stage-hint">
<Loader2 size={14} className="spin" /> <Loader2 size={14} className="spin" />
{isLoading ? '导出板块数据加载中…' : '正在补充头像和统计…'} {isLoading ? '导出板块数据加载中…' : '正在补充头像…'}
</div> </div>
)} )}
@@ -1898,7 +1481,6 @@ function ExportPage() {
data={visibleSessions} data={visibleSessions}
fixedHeaderContent={renderTableHeader} fixedHeaderContent={renderTableHeader}
computeItemKey={(_, session) => session.username} computeItemKey={(_, session) => session.username}
rangeChanged={handleTableRangeChanged}
itemContent={(_, session) => renderRowCells(session)} itemContent={(_, session) => renderRowCells(session)}
overscan={420} overscan={420}
/> />