mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
perf(export): prioritize totals and keep table visible
This commit is contained in:
@@ -916,6 +916,10 @@ function registerIpcHandlers() {
|
|||||||
return chatService.getExportTabCounts()
|
return chatService.getExportTabCounts()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('chat:getSessionMessageCounts', async (_, sessionIds: string[]) => {
|
||||||
|
return chatService.getSessionMessageCounts(sessionIds)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('chat:enrichSessionsContactInfo', async (_, usernames: string[]) => {
|
ipcMain.handle('chat:enrichSessionsContactInfo', async (_, usernames: string[]) => {
|
||||||
return chatService.enrichSessionsContactInfo(usernames)
|
return chatService.enrichSessionsContactInfo(usernames)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
connect: () => ipcRenderer.invoke('chat:connect'),
|
connect: () => ipcRenderer.invoke('chat:connect'),
|
||||||
getSessions: () => ipcRenderer.invoke('chat:getSessions'),
|
getSessions: () => ipcRenderer.invoke('chat:getSessions'),
|
||||||
getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'),
|
getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'),
|
||||||
|
getSessionMessageCounts: (sessionIds: string[]) => ipcRenderer.invoke('chat:getSessionMessageCounts', sessionIds),
|
||||||
enrichSessionsContactInfo: (usernames: string[]) =>
|
enrichSessionsContactInfo: (usernames: string[]) =>
|
||||||
ipcRenderer.invoke('chat:enrichSessionsContactInfo', usernames),
|
ipcRenderer.invoke('chat:enrichSessionsContactInfo', usernames),
|
||||||
getMessages: (sessionId: string, offset?: number, limit?: number, startTime?: number, endTime?: number, ascending?: boolean) =>
|
getMessages: (sessionId: string, offset?: number, limit?: number, startTime?: number, endTime?: number, ascending?: boolean) =>
|
||||||
|
|||||||
@@ -770,6 +770,48 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取会话消息总数(轻量接口,用于列表优先排序)
|
||||||
|
*/
|
||||||
|
async getSessionMessageCounts(sessionIds: string[]): Promise<{
|
||||||
|
success: boolean
|
||||||
|
counts?: Record<string, number>
|
||||||
|
error?: string
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const connectResult = await this.ensureConnected()
|
||||||
|
if (!connectResult.success) {
|
||||||
|
return { success: false, error: connectResult.error || '数据库未连接' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSessionIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
(sessionIds || [])
|
||||||
|
.map((id) => String(id || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (normalizedSessionIds.length === 0) {
|
||||||
|
return { success: true, counts: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts: Record<string, number> = {}
|
||||||
|
await this.forEachWithConcurrency(normalizedSessionIds, 8, async (sessionId) => {
|
||||||
|
try {
|
||||||
|
const result = await wcdbService.getMessageCount(sessionId)
|
||||||
|
counts[sessionId] = result.success && typeof result.count === 'number' ? result.count : 0
|
||||||
|
} catch {
|
||||||
|
counts[sessionId] = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, counts }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ChatService: 批量获取会话消息总数失败:', e)
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取通讯录列表
|
* 获取通讯录列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -439,6 +439,7 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -552,7 +553,8 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
min-height: 0;
|
min-height: 320px;
|
||||||
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -237,9 +237,29 @@ const timestampOrDash = (timestamp?: number): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 METRICS_VIEWPORT_PREFETCH = 140
|
const MESSAGE_COUNT_VIEWPORT_PREFETCH = 220
|
||||||
const METRICS_BACKGROUND_BATCH = 60
|
const MESSAGE_COUNT_BACKGROUND_BATCH = 180
|
||||||
const METRICS_BACKGROUND_INTERVAL_MS = 180
|
const MESSAGE_COUNT_BACKGROUND_INTERVAL_MS = 100
|
||||||
|
const METRICS_VIEWPORT_PREFETCH = 90
|
||||||
|
const METRICS_BACKGROUND_BATCH = 40
|
||||||
|
const METRICS_BACKGROUND_INTERVAL_MS = 220
|
||||||
|
const CONTACT_ENRICH_TIMEOUT_MS = 7000
|
||||||
|
|
||||||
|
const withTimeout = async <T,>(promise: Promise<T>, timeoutMs: number): Promise<T | null> => {
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
try {
|
||||||
|
return await Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise<null>((resolve) => {
|
||||||
|
timer = setTimeout(() => resolve(null), timeoutMs)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const WriteLayoutSelector = memo(function WriteLayoutSelector({
|
const WriteLayoutSelector = memo(function WriteLayoutSelector({
|
||||||
writeLayout,
|
writeLayout,
|
||||||
@@ -306,6 +326,7 @@ function ExportPage() {
|
|||||||
const [isTaskCenterExpanded, setIsTaskCenterExpanded] = useState(false)
|
const [isTaskCenterExpanded, setIsTaskCenterExpanded] = useState(false)
|
||||||
const [sessions, setSessions] = useState<SessionRow[]>([])
|
const [sessions, setSessions] = useState<SessionRow[]>([])
|
||||||
const [prefetchedTabCounts, setPrefetchedTabCounts] = useState<Record<ConversationTab, number> | null>(null)
|
const [prefetchedTabCounts, setPrefetchedTabCounts] = useState<Record<ConversationTab, number> | null>(null)
|
||||||
|
const [sessionMessageCounts, setSessionMessageCounts] = useState<Record<string, number>>({})
|
||||||
const [sessionMetrics, setSessionMetrics] = useState<Record<string, SessionMetrics>>({})
|
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')
|
||||||
@@ -355,8 +376,10 @@ function ExportPage() {
|
|||||||
const progressUnsubscribeRef = useRef<(() => void) | null>(null)
|
const progressUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||||
const runningTaskIdRef = useRef<string | null>(null)
|
const runningTaskIdRef = useRef<string | null>(null)
|
||||||
const tasksRef = useRef<ExportTask[]>([])
|
const tasksRef = useRef<ExportTask[]>([])
|
||||||
|
const sessionMessageCountsRef = useRef<Record<string, number>>({})
|
||||||
const sessionMetricsRef = useRef<Record<string, SessionMetrics>>({})
|
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 loadingMetricsRef = useRef<Set<string>>(new Set())
|
||||||
const preselectAppliedRef = useRef(false)
|
const preselectAppliedRef = useRef(false)
|
||||||
const visibleSessionsRef = useRef<SessionRow[]>([])
|
const visibleSessionsRef = useRef<SessionRow[]>([])
|
||||||
@@ -365,6 +388,10 @@ function ExportPage() {
|
|||||||
tasksRef.current = tasks
|
tasksRef.current = tasks
|
||||||
}, [tasks])
|
}, [tasks])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sessionMessageCountsRef.current = sessionMessageCounts
|
||||||
|
}, [sessionMessageCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sessionMetricsRef.current = sessionMetrics
|
sessionMetricsRef.current = sessionMetrics
|
||||||
}, [sessionMetrics])
|
}, [sessionMetrics])
|
||||||
@@ -468,6 +495,12 @@ function ExportPage() {
|
|||||||
sessionLoadTokenRef.current = loadToken
|
sessionLoadTokenRef.current = loadToken
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setIsSessionEnriching(false)
|
setIsSessionEnriching(false)
|
||||||
|
loadingMessageCountsRef.current.clear()
|
||||||
|
loadingMetricsRef.current.clear()
|
||||||
|
sessionMessageCountsRef.current = {}
|
||||||
|
sessionMetricsRef.current = {}
|
||||||
|
setSessionMessageCounts({})
|
||||||
|
setSessionMetrics({})
|
||||||
|
|
||||||
const isStale = () => sessionLoadTokenRef.current !== loadToken
|
const isStale = () => sessionLoadTokenRef.current !== loadToken
|
||||||
|
|
||||||
@@ -503,10 +536,10 @@ function ExportPage() {
|
|||||||
setIsSessionEnriching(true)
|
setIsSessionEnriching(true)
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const contactsResult = await window.electronAPI.chat.getContacts()
|
const contactsResult = await withTimeout(window.electronAPI.chat.getContacts(), CONTACT_ENRICH_TIMEOUT_MS)
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
|
||||||
const contacts: ContactInfo[] = contactsResult.success && contactsResult.contacts ? contactsResult.contacts : []
|
const contacts: ContactInfo[] = contactsResult?.success && contactsResult.contacts ? contactsResult.contacts : []
|
||||||
const nextContactMap = contacts.reduce<Record<string, ContactInfo>>((map, contact) => {
|
const nextContactMap = contacts.reduce<Record<string, ContactInfo>>((map, contact) => {
|
||||||
map[contact.username] = contact
|
map[contact.username] = contact
|
||||||
return map
|
return map
|
||||||
@@ -518,8 +551,11 @@ function ExportPage() {
|
|||||||
|
|
||||||
let extraContactMap: Record<string, { displayName?: string; avatarUrl?: string }> = {}
|
let extraContactMap: Record<string, { displayName?: string; avatarUrl?: string }> = {}
|
||||||
if (needsEnrichment.length > 0) {
|
if (needsEnrichment.length > 0) {
|
||||||
const enrichResult = await window.electronAPI.chat.enrichSessionsContactInfo(needsEnrichment)
|
const enrichResult = await withTimeout(
|
||||||
if (enrichResult.success && enrichResult.contacts) {
|
window.electronAPI.chat.enrichSessionsContactInfo(needsEnrichment),
|
||||||
|
CONTACT_ENRICH_TIMEOUT_MS
|
||||||
|
)
|
||||||
|
if (enrichResult?.success && enrichResult.contacts) {
|
||||||
extraContactMap = enrichResult.contacts
|
extraContactMap = enrichResult.contacts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,12 +575,7 @@ function ExportPage() {
|
|||||||
avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0))
|
||||||
const aMetric = sessionMetricsRef.current[a.username]?.totalMessages ?? 0
|
|
||||||
const bMetric = sessionMetricsRef.current[b.username]?.totalMessages ?? 0
|
|
||||||
if (bMetric !== aMetric) return bMetric - aMetric
|
|
||||||
return (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
setSessions(nextSessions)
|
setSessions(nextSessions)
|
||||||
} catch (enrichError) {
|
} catch (enrichError) {
|
||||||
@@ -566,10 +597,8 @@ function ExportPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadBaseConfig()
|
void loadBaseConfig()
|
||||||
void (async () => {
|
void loadTabCounts()
|
||||||
await loadTabCounts()
|
void loadSessions()
|
||||||
await loadSessions()
|
|
||||||
})()
|
|
||||||
|
|
||||||
// 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。
|
// 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
@@ -608,23 +637,74 @@ function ExportPage() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const totalA = sessionMetrics[a.username]?.totalMessages ?? 0
|
const totalA = sessionMessageCounts[a.username]
|
||||||
const totalB = sessionMetrics[b.username]?.totalMessages ?? 0
|
const totalB = sessionMessageCounts[b.username]
|
||||||
if (totalB !== totalA) {
|
const hasTotalA = typeof totalA === 'number'
|
||||||
|
const hasTotalB = typeof totalB === 'number'
|
||||||
|
|
||||||
|
if (hasTotalA && hasTotalB && totalB !== totalA) {
|
||||||
return totalB - totalA
|
return totalB - totalA
|
||||||
}
|
}
|
||||||
|
if (hasTotalA !== hasTotalB) {
|
||||||
|
return hasTotalA ? -1 : 1
|
||||||
|
}
|
||||||
|
|
||||||
const latestA = sessionMetrics[a.username]?.lastTimestamp ?? a.lastTimestamp ?? 0
|
const latestA = sessionMetrics[a.username]?.lastTimestamp ?? a.lastTimestamp ?? 0
|
||||||
const latestB = sessionMetrics[b.username]?.lastTimestamp ?? b.lastTimestamp ?? 0
|
const latestB = sessionMetrics[b.username]?.lastTimestamp ?? b.lastTimestamp ?? 0
|
||||||
return latestB - latestA
|
return latestB - latestA
|
||||||
})
|
})
|
||||||
}, [sessions, activeTab, searchKeyword, sessionMetrics])
|
}, [sessions, activeTab, searchKeyword, sessionMessageCounts, sessionMetrics])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
visibleSessionsRef.current = visibleSessions
|
visibleSessionsRef.current = visibleSessions
|
||||||
}, [visibleSessions])
|
}, [visibleSessions])
|
||||||
|
|
||||||
|
const ensureSessionMessageCounts = useCallback(async (targetSessions: SessionRow[]) => {
|
||||||
|
const loadTokenAtStart = sessionLoadTokenRef.current
|
||||||
|
const currentCounts = sessionMessageCountsRef.current
|
||||||
|
const pending = targetSessions.filter(
|
||||||
|
session => currentCounts[session.username] === undefined && !loadingMessageCountsRef.current.has(session.username)
|
||||||
|
)
|
||||||
|
if (pending.length === 0) return
|
||||||
|
|
||||||
|
const updates: Record<string, number> = {}
|
||||||
|
for (const session of pending) {
|
||||||
|
loadingMessageCountsRef.current.add(session.username)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const batchSize = 220
|
||||||
|
for (let i = 0; i < pending.length; i += batchSize) {
|
||||||
|
if (loadTokenAtStart !== sessionLoadTokenRef.current) return
|
||||||
|
const chunk = pending.slice(i, i + batchSize)
|
||||||
|
const ids = chunk.map(session => session.username)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.chat.getSessionMessageCounts(ids)
|
||||||
|
for (const session of chunk) {
|
||||||
|
const value = result.success && result.counts ? result.counts[session.username] : undefined
|
||||||
|
updates[session.username] = typeof value === 'number' ? value : 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载会话总消息数失败:', error)
|
||||||
|
for (const session of chunk) {
|
||||||
|
updates[session.username] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
for (const session of pending) {
|
||||||
|
loadingMessageCountsRef.current.delete(session.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadTokenAtStart === sessionLoadTokenRef.current && Object.keys(updates).length > 0) {
|
||||||
|
setSessionMessageCounts(prev => ({ ...prev, ...updates }))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const ensureSessionMetrics = useCallback(async (targetSessions: SessionRow[]) => {
|
const ensureSessionMetrics = useCallback(async (targetSessions: SessionRow[]) => {
|
||||||
|
const loadTokenAtStart = sessionLoadTokenRef.current
|
||||||
const currentMetrics = sessionMetricsRef.current
|
const currentMetrics = sessionMetricsRef.current
|
||||||
const pending = targetSessions.filter(session => !currentMetrics[session.username] && !loadingMetricsRef.current.has(session.username))
|
const pending = targetSessions.filter(session => !currentMetrics[session.username] && !loadingMetricsRef.current.has(session.username))
|
||||||
if (pending.length === 0) return
|
if (pending.length === 0) return
|
||||||
@@ -637,6 +717,7 @@ function ExportPage() {
|
|||||||
try {
|
try {
|
||||||
const batchSize = 80
|
const batchSize = 80
|
||||||
for (let i = 0; i < pending.length; i += batchSize) {
|
for (let i = 0; i < pending.length; i += batchSize) {
|
||||||
|
if (loadTokenAtStart !== sessionLoadTokenRef.current) return
|
||||||
const chunk = pending.slice(i, i + batchSize)
|
const chunk = pending.slice(i, i + batchSize)
|
||||||
const ids = chunk.map(session => session.username)
|
const ids = chunk.map(session => session.username)
|
||||||
|
|
||||||
@@ -677,35 +758,48 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updates).length > 0) {
|
if (loadTokenAtStart === sessionLoadTokenRef.current && Object.keys(updates).length > 0) {
|
||||||
setSessionMetrics(prev => ({ ...prev, ...updates }))
|
setSessionMetrics(prev => ({ ...prev, ...updates }))
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyword = searchKeyword.trim().toLowerCase()
|
const targets = visibleSessions.slice(0, MESSAGE_COUNT_VIEWPORT_PREFETCH)
|
||||||
const targets = sessions
|
void ensureSessionMessageCounts(targets)
|
||||||
.filter((session) => {
|
}, [visibleSessions, ensureSessionMessageCounts])
|
||||||
if (session.kind !== activeTab) return false
|
|
||||||
if (!keyword) return true
|
useEffect(() => {
|
||||||
return (
|
const targets = visibleSessions.slice(0, METRICS_VIEWPORT_PREFETCH)
|
||||||
(session.displayName || '').toLowerCase().includes(keyword) ||
|
|
||||||
session.username.toLowerCase().includes(keyword)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0))
|
|
||||||
.slice(0, METRICS_VIEWPORT_PREFETCH)
|
|
||||||
void ensureSessionMetrics(targets)
|
void ensureSessionMetrics(targets)
|
||||||
}, [sessions, activeTab, searchKeyword, ensureSessionMetrics])
|
}, [visibleSessions, ensureSessionMetrics])
|
||||||
|
|
||||||
const handleTableRangeChanged = useCallback((range: { startIndex: number; endIndex: number }) => {
|
const handleTableRangeChanged = useCallback((range: { startIndex: number; endIndex: number }) => {
|
||||||
const current = visibleSessionsRef.current
|
const current = visibleSessionsRef.current
|
||||||
if (current.length === 0) return
|
if (current.length === 0) return
|
||||||
const start = Math.max(0, range.startIndex - METRICS_VIEWPORT_PREFETCH)
|
const prefetch = Math.max(MESSAGE_COUNT_VIEWPORT_PREFETCH, METRICS_VIEWPORT_PREFETCH)
|
||||||
const end = Math.min(current.length - 1, range.endIndex + 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
|
if (end < start) return
|
||||||
void ensureSessionMetrics(current.slice(start, end + 1))
|
const rangeSessions = current.slice(start, end + 1)
|
||||||
}, [ensureSessionMetrics])
|
void ensureSessionMessageCounts(rangeSessions)
|
||||||
|
void ensureSessionMetrics(rangeSessions)
|
||||||
|
}, [ensureSessionMessageCounts, ensureSessionMetrics])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessions.length === 0) return
|
||||||
|
let cursor = 0
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
if (cursor >= sessions.length) {
|
||||||
|
window.clearInterval(timer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const chunk = sessions.slice(cursor, cursor + MESSAGE_COUNT_BACKGROUND_BATCH)
|
||||||
|
cursor += MESSAGE_COUNT_BACKGROUND_BATCH
|
||||||
|
void ensureSessionMessageCounts(chunk)
|
||||||
|
}, MESSAGE_COUNT_BACKGROUND_INTERVAL_MS)
|
||||||
|
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [sessions, ensureSessionMessageCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessions.length === 0) return
|
if (sessions.length === 0) return
|
||||||
@@ -1335,7 +1429,8 @@ function ExportPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderRowCells = (session: SessionRow) => {
|
const renderRowCells = (session: SessionRow) => {
|
||||||
const metrics = sessionMetrics[session.username] || {}
|
const metrics = sessionMetrics[session.username]
|
||||||
|
const totalMessages = sessionMessageCounts[session.username]
|
||||||
const checked = selectedSessions.has(session.username)
|
const checked = selectedSessions.has(session.username)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1351,35 +1446,43 @@ function ExportPage() {
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{renderSessionName(session)}</td>
|
<td>{renderSessionName(session)}</td>
|
||||||
<td>{valueOrDash(metrics.totalMessages)}</td>
|
<td>
|
||||||
<td>{valueOrDash(metrics.voiceMessages)}</td>
|
{typeof totalMessages === 'number'
|
||||||
<td>{valueOrDash(metrics.imageMessages)}</td>
|
? totalMessages.toLocaleString()
|
||||||
<td>{valueOrDash(metrics.videoMessages)}</td>
|
: (
|
||||||
<td>{valueOrDash(metrics.emojiMessages)}</td>
|
<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') && (
|
{(activeTab === 'private' || activeTab === 'former_friend') && (
|
||||||
<>
|
<>
|
||||||
<td>{valueOrDash(metrics.privateMutualGroups)}</td>
|
<td>{valueOrDash(metrics?.privateMutualGroups)}</td>
|
||||||
<td>{timestampOrDash(metrics.firstTimestamp)}</td>
|
<td>{timestampOrDash(metrics?.firstTimestamp)}</td>
|
||||||
<td>{timestampOrDash(metrics.lastTimestamp)}</td>
|
<td>{timestampOrDash(metrics?.lastTimestamp)}</td>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'group' && (
|
{activeTab === 'group' && (
|
||||||
<>
|
<>
|
||||||
<td>{valueOrDash(metrics.groupMyMessages)}</td>
|
<td>{valueOrDash(metrics?.groupMyMessages)}</td>
|
||||||
<td>{valueOrDash(metrics.groupMemberCount)}</td>
|
<td>{valueOrDash(metrics?.groupMemberCount)}</td>
|
||||||
<td>{valueOrDash(metrics.groupActiveSpeakers)}</td>
|
<td>{valueOrDash(metrics?.groupActiveSpeakers)}</td>
|
||||||
<td>{valueOrDash(metrics.groupMutualFriends)}</td>
|
<td>{valueOrDash(metrics?.groupMutualFriends)}</td>
|
||||||
<td>{timestampOrDash(metrics.firstTimestamp)}</td>
|
<td>{timestampOrDash(metrics?.firstTimestamp)}</td>
|
||||||
<td>{timestampOrDash(metrics.lastTimestamp)}</td>
|
<td>{timestampOrDash(metrics?.lastTimestamp)}</td>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'official' && (
|
{activeTab === 'official' && (
|
||||||
<>
|
<>
|
||||||
<td>{timestampOrDash(metrics.firstTimestamp)}</td>
|
<td>{timestampOrDash(metrics?.firstTimestamp)}</td>
|
||||||
<td>{timestampOrDash(metrics.lastTimestamp)}</td>
|
<td>{timestampOrDash(metrics?.lastTimestamp)}</td>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1616,10 +1719,10 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
5
src/types/electron.d.ts
vendored
5
src/types/electron.d.ts
vendored
@@ -84,6 +84,11 @@ export interface ElectronAPI {
|
|||||||
}
|
}
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
getSessionMessageCounts: (sessionIds: string[]) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
counts?: Record<string, number>
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
enrichSessionsContactInfo: (usernames: string[]) => Promise<{
|
enrichSessionsContactInfo: (usernames: string[]) => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
contacts?: Record<string, { displayName?: string; avatarUrl?: string }>
|
contacts?: Record<string, { displayName?: string; avatarUrl?: string }>
|
||||||
|
|||||||
Reference in New Issue
Block a user