mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
refactor(export): remove session stats columns and background counting
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user