From c6e8bde0781a6ee07838fc0b5c690d2aa759eadd Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Sun, 1 Mar 2026 16:32:48 +0800 Subject: [PATCH] feat(export): prioritize tab counts via lightweight api --- electron/main.ts | 4 ++ electron/preload.ts | 1 + electron/services/chatService.ts | 113 +++++++++++++++++++++++++++++++ src/pages/ExportPage.tsx | 38 +++++++++-- src/types/electron.d.ts | 10 +++ 5 files changed, 162 insertions(+), 4 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 0a47a22..c13c9dc 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -912,6 +912,10 @@ function registerIpcHandlers() { return chatService.getSessions() }) + ipcMain.handle('chat:getExportTabCounts', async () => { + return chatService.getExportTabCounts() + }) + ipcMain.handle('chat:enrichSessionsContactInfo', async (_, usernames: string[]) => { return chatService.enrichSessionsContactInfo(usernames) }) diff --git a/electron/preload.ts b/electron/preload.ts index 49c3126..c0f76d1 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -130,6 +130,7 @@ contextBridge.exposeInMainWorld('electronAPI', { chat: { connect: () => ipcRenderer.invoke('chat:connect'), getSessions: () => ipcRenderer.invoke('chat:getSessions'), + getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'), enrichSessionsContactInfo: (usernames: string[]) => ipcRenderer.invoke('chat:enrichSessionsContactInfo', usernames), getMessages: (sessionId: string, offset?: number, limit?: number, startTime?: number, endTime?: number, ascending?: boolean) => diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 171ac0b..6cc1e4a 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -151,6 +151,13 @@ interface ExportSessionStats { groupMutualFriends?: number } +interface ExportTabCounts { + private: number + group: number + official: number + former_friend: number +} + // 表情包缓存 const emojiCache: Map = new Map() const emojiDownloading: Map> = new Map() @@ -657,6 +664,112 @@ class ChatService { } } + /** + * 获取导出页会话分类数量(轻量接口,优先用于顶部 Tab 数量展示) + */ + async getExportTabCounts(): Promise<{ success: boolean; counts?: ExportTabCounts; error?: string }> { + try { + const connectResult = await this.ensureConnected() + if (!connectResult.success) { + return { success: false, error: connectResult.error } + } + + const sessionResult = await wcdbService.getSessions() + if (!sessionResult.success || !sessionResult.sessions) { + return { success: false, error: sessionResult.error || '获取会话失败' } + } + + const counts: ExportTabCounts = { + private: 0, + group: 0, + official: 0, + former_friend: 0 + } + + const nonGroupUsernames: string[] = [] + const usernameSet = new Set() + + for (const row of sessionResult.sessions as Record[]) { + const username = + row.username || + row.user_name || + row.userName || + row.usrName || + row.UsrName || + row.talker || + row.talker_id || + row.talkerId || + '' + + if (!this.shouldKeepSession(username)) continue + if (usernameSet.has(username)) continue + usernameSet.add(username) + + if (username.endsWith('@chatroom')) { + counts.group += 1 + } else { + nonGroupUsernames.push(username) + } + } + + if (nonGroupUsernames.length === 0) { + return { success: true, counts } + } + + const contactTypeMap = new Map() + const chunkSize = 400 + + for (let i = 0; i < nonGroupUsernames.length; i += chunkSize) { + const chunk = nonGroupUsernames.slice(i, i + chunkSize) + if (chunk.length === 0) continue + + const usernamesExpr = chunk.map((name) => `'${this.escapeSqlString(name)}'`).join(',') + const contactSql = ` + SELECT username, local_type, quan_pin + FROM contact + WHERE username IN (${usernamesExpr}) + ` + + const contactResult = await wcdbService.execQuery('contact', null, contactSql) + if (!contactResult.success || !contactResult.rows) { + continue + } + + for (const row of contactResult.rows as Record[]) { + const username = String(row.username || '').trim() + if (!username) continue + + if (username.startsWith('gh_')) { + contactTypeMap.set(username, 'official') + continue + } + + const localType = this.getRowInt(row, ['local_type', 'localType', 'WCDB_CT_local_type'], 0) + const quanPin = String(this.getRowField(row, ['quan_pin', 'quanPin', 'WCDB_CT_quan_pin']) || '').trim() + if (localType === 0 && quanPin) { + contactTypeMap.set(username, 'former_friend') + } + } + } + + for (const username of nonGroupUsernames) { + const type = contactTypeMap.get(username) + if (type === 'official') { + counts.official += 1 + } else if (type === 'former_friend') { + counts.former_friend += 1 + } else { + counts.private += 1 + } + } + + return { success: true, counts } + } catch (e) { + console.error('ChatService: 获取导出页会话分类数量失败:', e) + return { success: false, error: String(e) } + } + } + /** * 获取通讯录列表 */ diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 486d31e..e8dd8ca 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -242,10 +242,12 @@ function ExportPage() { const [isLoading, setIsLoading] = useState(true) const [isSessionEnriching, setIsSessionEnriching] = useState(false) + const [isTabCountsLoading, setIsTabCountsLoading] = useState(true) const [isSnsStatsLoading, setIsSnsStatsLoading] = useState(true) const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true) const [isTaskCenterExpanded, setIsTaskCenterExpanded] = useState(false) const [sessions, setSessions] = useState([]) + const [prefetchedTabCounts, setPrefetchedTabCounts] = useState | null>(null) const [sessionMetrics, setSessionMetrics] = useState>({}) const [searchKeyword, setSearchKeyword] = useState('') const [activeTab, setActiveTab] = useState('private') @@ -373,6 +375,20 @@ function ExportPage() { } }, []) + const loadTabCounts = useCallback(async () => { + setIsTabCountsLoading(true) + try { + const result = await window.electronAPI.chat.getExportTabCounts() + if (result.success && result.counts) { + setPrefetchedTabCounts(result.counts) + } + } catch (error) { + console.error('加载导出页会话分类数量失败:', error) + } finally { + setIsTabCountsLoading(false) + } + }, []) + const loadSnsStats = useCallback(async () => { setIsSnsStatsLoading(true) try { @@ -493,7 +509,10 @@ function ExportPage() { useEffect(() => { void loadBaseConfig() - void loadSessions() + void (async () => { + await loadTabCounts() + await loadSessions() + })() // 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。 const timer = window.setTimeout(() => { @@ -501,7 +520,7 @@ function ExportPage() { }, 180) return () => window.clearTimeout(timer) - }, [loadBaseConfig, loadSessions, loadSnsStats]) + }, [loadTabCounts, loadBaseConfig, loadSessions, loadSnsStats]) useEffect(() => { preselectAppliedRef.current = false @@ -1057,7 +1076,7 @@ function ExportPage() { return set }, [tasks]) - const tabCounts = useMemo(() => { + const sessionTabCounts = useMemo(() => { const counts: Record = { private: 0, group: 0, @@ -1070,6 +1089,16 @@ function ExportPage() { return counts }, [sessions]) + const tabCounts = useMemo(() => { + if (sessions.length > 0) { + return sessionTabCounts + } + if (prefetchedTabCounts) { + return prefetchedTabCounts + } + return sessionTabCounts + }, [sessions.length, sessionTabCounts, prefetchedTabCounts]) + const contentCards = useMemo(() => { const scopeSessions = sessions.filter(session => session.kind === 'private' || session.kind === 'group') const totalSessions = scopeSessions.length @@ -1295,7 +1324,8 @@ function ExportPage() { const formatCandidateOptions = exportDialog.scope === 'sns' ? formatOptions.filter(option => option.value === 'html' || option.value === 'json') : formatOptions - const isTabCountComputing = isLoading || isSessionEnriching + const hasTabCountsSource = prefetchedTabCounts !== null || sessions.length > 0 + const isTabCountComputing = isTabCountsLoading && !hasTabCountsSource const isSessionCardStatsLoading = isLoading || isBaseConfigLoading const taskRunningCount = tasks.filter(task => task.status === 'running').length const taskQueuedCount = tasks.filter(task => task.status === 'queued').length diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index dfa82e3..4d96cb9 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -74,6 +74,16 @@ export interface ElectronAPI { chat: { connect: () => Promise<{ success: boolean; error?: string }> getSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }> + getExportTabCounts: () => Promise<{ + success: boolean + counts?: { + private: number + group: number + official: number + former_friend: number + } + error?: string + }> enrichSessionsContactInfo: (usernames: string[]) => Promise<{ success: boolean contacts?: Record