feat(export): prioritize tab counts via lightweight api

This commit is contained in:
tisonhuang
2026-03-01 16:32:48 +08:00
parent adff7b9e1e
commit c6e8bde078
5 changed files with 162 additions and 4 deletions

View File

@@ -912,6 +912,10 @@ function registerIpcHandlers() {
return chatService.getSessions() return chatService.getSessions()
}) })
ipcMain.handle('chat:getExportTabCounts', async () => {
return chatService.getExportTabCounts()
})
ipcMain.handle('chat:enrichSessionsContactInfo', async (_, usernames: string[]) => { ipcMain.handle('chat:enrichSessionsContactInfo', async (_, usernames: string[]) => {
return chatService.enrichSessionsContactInfo(usernames) return chatService.enrichSessionsContactInfo(usernames)
}) })

View File

@@ -130,6 +130,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
chat: { chat: {
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'),
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) =>

View File

@@ -151,6 +151,13 @@ interface ExportSessionStats {
groupMutualFriends?: number groupMutualFriends?: number
} }
interface ExportTabCounts {
private: number
group: number
official: number
former_friend: number
}
// 表情包缓存 // 表情包缓存
const emojiCache: Map<string, string> = new Map() const emojiCache: Map<string, string> = new Map()
const emojiDownloading: Map<string, Promise<string | null>> = new Map() const emojiDownloading: Map<string, Promise<string | null>> = 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<string>()
for (const row of sessionResult.sessions as Record<string, any>[]) {
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<string, 'official' | 'former_friend'>()
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<string, any>[]) {
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) }
}
}
/** /**
* 获取通讯录列表 * 获取通讯录列表
*/ */

View File

@@ -242,10 +242,12 @@ function ExportPage() {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isSessionEnriching, setIsSessionEnriching] = useState(false) const [isSessionEnriching, setIsSessionEnriching] = useState(false)
const [isTabCountsLoading, setIsTabCountsLoading] = useState(true)
const [isSnsStatsLoading, setIsSnsStatsLoading] = useState(true) const [isSnsStatsLoading, setIsSnsStatsLoading] = useState(true)
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 [prefetchedTabCounts, setPrefetchedTabCounts] = useState<Record<ConversationTab, number> | null>(null)
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')
@@ -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 () => { const loadSnsStats = useCallback(async () => {
setIsSnsStatsLoading(true) setIsSnsStatsLoading(true)
try { try {
@@ -493,7 +509,10 @@ function ExportPage() {
useEffect(() => { useEffect(() => {
void loadBaseConfig() void loadBaseConfig()
void loadSessions() void (async () => {
await loadTabCounts()
await loadSessions()
})()
// 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。 // 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
@@ -501,7 +520,7 @@ function ExportPage() {
}, 180) }, 180)
return () => window.clearTimeout(timer) return () => window.clearTimeout(timer)
}, [loadBaseConfig, loadSessions, loadSnsStats]) }, [loadTabCounts, loadBaseConfig, loadSessions, loadSnsStats])
useEffect(() => { useEffect(() => {
preselectAppliedRef.current = false preselectAppliedRef.current = false
@@ -1057,7 +1076,7 @@ function ExportPage() {
return set return set
}, [tasks]) }, [tasks])
const tabCounts = useMemo(() => { const sessionTabCounts = useMemo(() => {
const counts: Record<ConversationTab, number> = { const counts: Record<ConversationTab, number> = {
private: 0, private: 0,
group: 0, group: 0,
@@ -1070,6 +1089,16 @@ function ExportPage() {
return counts return counts
}, [sessions]) }, [sessions])
const tabCounts = useMemo(() => {
if (sessions.length > 0) {
return sessionTabCounts
}
if (prefetchedTabCounts) {
return prefetchedTabCounts
}
return sessionTabCounts
}, [sessions.length, sessionTabCounts, prefetchedTabCounts])
const contentCards = useMemo(() => { const contentCards = useMemo(() => {
const scopeSessions = sessions.filter(session => session.kind === 'private' || session.kind === 'group') const scopeSessions = sessions.filter(session => session.kind === 'private' || session.kind === 'group')
const totalSessions = scopeSessions.length const totalSessions = scopeSessions.length
@@ -1295,7 +1324,8 @@ function ExportPage() {
const formatCandidateOptions = exportDialog.scope === 'sns' const formatCandidateOptions = exportDialog.scope === 'sns'
? formatOptions.filter(option => option.value === 'html' || option.value === 'json') ? formatOptions.filter(option => option.value === 'html' || option.value === 'json')
: formatOptions : formatOptions
const isTabCountComputing = isLoading || isSessionEnriching const hasTabCountsSource = prefetchedTabCounts !== null || sessions.length > 0
const isTabCountComputing = isTabCountsLoading && !hasTabCountsSource
const isSessionCardStatsLoading = isLoading || isBaseConfigLoading const isSessionCardStatsLoading = isLoading || isBaseConfigLoading
const taskRunningCount = tasks.filter(task => task.status === 'running').length const taskRunningCount = tasks.filter(task => task.status === 'running').length
const taskQueuedCount = tasks.filter(task => task.status === 'queued').length const taskQueuedCount = tasks.filter(task => task.status === 'queued').length

View File

@@ -74,6 +74,16 @@ export interface ElectronAPI {
chat: { chat: {
connect: () => Promise<{ success: boolean; error?: string }> connect: () => Promise<{ success: boolean; error?: string }>
getSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; 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<{ enrichSessionsContactInfo: (usernames: string[]) => Promise<{
success: boolean success: boolean
contacts?: Record<string, { displayName?: string; avatarUrl?: string }> contacts?: Record<string, { displayName?: string; avatarUrl?: string }>