mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
feat(counts): unify contacts and export tab counters
This commit is contained in:
@@ -920,6 +920,10 @@ function registerIpcHandlers() {
|
|||||||
return chatService.getExportTabCounts()
|
return chatService.getExportTabCounts()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('chat:getContactTypeCounts', async () => {
|
||||||
|
return chatService.getContactTypeCounts()
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('chat:getSessionMessageCounts', async (_, sessionIds: string[]) => {
|
ipcMain.handle('chat:getSessionMessageCounts', async (_, sessionIds: string[]) => {
|
||||||
return chatService.getSessionMessageCounts(sessionIds)
|
return chatService.getSessionMessageCounts(sessionIds)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getSessions: () => ipcRenderer.invoke('chat:getSessions'),
|
getSessions: () => ipcRenderer.invoke('chat:getSessions'),
|
||||||
getSessionStatuses: (usernames: string[]) => ipcRenderer.invoke('chat:getSessionStatuses', usernames),
|
getSessionStatuses: (usernames: string[]) => ipcRenderer.invoke('chat:getSessionStatuses', usernames),
|
||||||
getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'),
|
getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'),
|
||||||
|
getContactTypeCounts: () => ipcRenderer.invoke('chat:getContactTypeCounts'),
|
||||||
getSessionMessageCounts: (sessionIds: string[]) => ipcRenderer.invoke('chat:getSessionMessageCounts', sessionIds),
|
getSessionMessageCounts: (sessionIds: string[]) => ipcRenderer.invoke('chat:getSessionMessageCounts', sessionIds),
|
||||||
enrichSessionsContactInfo: (usernames: string[]) =>
|
enrichSessionsContactInfo: (usernames: string[]) =>
|
||||||
ipcRenderer.invoke('chat:enrichSessionsContactInfo', usernames),
|
ipcRenderer.invoke('chat:enrichSessionsContactInfo', usernames),
|
||||||
|
|||||||
@@ -762,111 +762,73 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取导出页会话分类数量(轻量接口,优先用于顶部 Tab 数量展示)
|
* 获取联系人类型数量(好友、群聊、公众号、曾经的好友)
|
||||||
*/
|
*/
|
||||||
async getExportTabCounts(): Promise<{ success: boolean; counts?: ExportTabCounts; error?: string }> {
|
async getContactTypeCounts(): Promise<{ success: boolean; counts?: ExportTabCounts; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const connectResult = await this.ensureConnected()
|
const connectResult = await this.ensureConnected()
|
||||||
if (!connectResult.success) {
|
if (!connectResult.success) {
|
||||||
return { success: false, error: connectResult.error }
|
return { success: false, error: connectResult.error }
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionResult = await wcdbService.getSessions()
|
const excludeExpr = Array.from(FRIEND_EXCLUDE_USERNAMES)
|
||||||
if (!sessionResult.success || !sessionResult.sessions) {
|
.map((username) => `'${this.escapeSqlString(username)}'`)
|
||||||
return { success: false, error: sessionResult.error || '获取会话失败' }
|
.join(',')
|
||||||
}
|
|
||||||
|
|
||||||
const counts: ExportTabCounts = {
|
const countsSql = `
|
||||||
private: 0,
|
SELECT
|
||||||
group: 0,
|
SUM(CASE WHEN username LIKE '%@chatroom' THEN 1 ELSE 0 END) AS group_count,
|
||||||
official: 0,
|
SUM(CASE WHEN username LIKE 'gh_%' THEN 1 ELSE 0 END) AS official_count,
|
||||||
former_friend: 0
|
SUM(
|
||||||
}
|
CASE
|
||||||
|
WHEN username NOT LIKE '%@chatroom'
|
||||||
const nonGroupUsernames: string[] = []
|
AND username NOT LIKE 'gh_%'
|
||||||
const usernameSet = new Set<string>()
|
AND local_type = 1
|
||||||
|
AND username NOT IN (${excludeExpr})
|
||||||
for (const row of sessionResult.sessions as Record<string, any>[]) {
|
THEN 1 ELSE 0
|
||||||
const username =
|
END
|
||||||
row.username ||
|
) AS private_count,
|
||||||
row.user_name ||
|
SUM(
|
||||||
row.userName ||
|
CASE
|
||||||
row.usrName ||
|
WHEN username NOT LIKE '%@chatroom'
|
||||||
row.UsrName ||
|
AND username NOT LIKE 'gh_%'
|
||||||
row.talker ||
|
AND local_type = 0
|
||||||
row.talker_id ||
|
AND COALESCE(quan_pin, '') != ''
|
||||||
row.talkerId ||
|
THEN 1 ELSE 0
|
||||||
''
|
END
|
||||||
|
) AS former_friend_count
|
||||||
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
|
FROM contact
|
||||||
WHERE username IN (${usernamesExpr})
|
WHERE username IS NOT NULL
|
||||||
|
AND username != ''
|
||||||
`
|
`
|
||||||
|
|
||||||
const contactResult = await wcdbService.execQuery('contact', null, contactSql)
|
const result = await wcdbService.execQuery('contact', null, countsSql)
|
||||||
if (!contactResult.success || !contactResult.rows) {
|
if (!result.success || !result.rows || result.rows.length === 0) {
|
||||||
continue
|
return { success: false, error: result.error || '获取联系人类型数量失败' }
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const row of contactResult.rows as Record<string, any>[]) {
|
const row = result.rows[0] as Record<string, any>
|
||||||
const username = String(row.username || '').trim()
|
const counts: ExportTabCounts = {
|
||||||
if (!username) continue
|
private: this.getRowInt(row, ['private_count', 'privateCount'], 0),
|
||||||
|
group: this.getRowInt(row, ['group_count', 'groupCount'], 0),
|
||||||
if (username.startsWith('gh_')) {
|
official: this.getRowInt(row, ['official_count', 'officialCount'], 0),
|
||||||
contactTypeMap.set(username, 'official')
|
former_friend: this.getRowInt(row, ['former_friend_count', 'formerFriendCount'], 0)
|
||||||
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 }
|
return { success: true, counts }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ChatService: 获取导出页会话分类数量失败:', e)
|
console.error('ChatService: 获取联系人类型数量失败:', e)
|
||||||
return { success: false, error: String(e) }
|
return { success: false, error: String(e) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导出页会话分类数量(轻量接口,优先用于顶部 Tab 数量展示)
|
||||||
|
*/
|
||||||
|
async getExportTabCounts(): Promise<{ success: boolean; counts?: ExportTabCounts; error?: string }> {
|
||||||
|
return this.getContactTypeCounts()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量获取会话消息总数(轻量接口,用于列表优先排序)
|
* 批量获取会话消息总数(轻量接口,用于列表优先排序)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -148,6 +148,17 @@
|
|||||||
svg {
|
svg {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-label {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-count {
|
||||||
|
margin-left: auto;
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useMemo, useRef, type UIEvent } from
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown, MessageCircle, UserX } from 'lucide-react'
|
import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown, MessageCircle, UserX } from 'lucide-react'
|
||||||
import { useChatStore } from '../stores/chatStore'
|
import { useChatStore } from '../stores/chatStore'
|
||||||
|
import { toContactTypeCardCounts, useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
|
||||||
import './ContactsPage.scss'
|
import './ContactsPage.scss'
|
||||||
|
|
||||||
interface ContactInfo {
|
interface ContactInfo {
|
||||||
@@ -58,6 +59,8 @@ function ContactsPage() {
|
|||||||
})
|
})
|
||||||
const [scrollTop, setScrollTop] = useState(0)
|
const [scrollTop, setScrollTop] = useState(0)
|
||||||
const [listViewportHeight, setListViewportHeight] = useState(480)
|
const [listViewportHeight, setListViewportHeight] = useState(480)
|
||||||
|
const sharedTabCounts = useContactTypeCountsStore(state => state.tabCounts)
|
||||||
|
const syncContactTypeCounts = useContactTypeCountsStore(state => state.syncFromContacts)
|
||||||
|
|
||||||
const applyEnrichedContacts = useCallback((enrichedMap: Record<string, ContactEnrichInfo>) => {
|
const applyEnrichedContacts = useCallback((enrichedMap: Record<string, ContactEnrichInfo>) => {
|
||||||
if (!enrichedMap || Object.keys(enrichedMap).length === 0) return
|
if (!enrichedMap || Object.keys(enrichedMap).length === 0) return
|
||||||
@@ -151,6 +154,7 @@ function ContactsPage() {
|
|||||||
if (loadVersionRef.current !== loadVersion) return
|
if (loadVersionRef.current !== loadVersion) return
|
||||||
if (contactsResult.success && contactsResult.contacts) {
|
if (contactsResult.success && contactsResult.contacts) {
|
||||||
setContacts(contactsResult.contacts)
|
setContacts(contactsResult.contacts)
|
||||||
|
syncContactTypeCounts(contactsResult.contacts)
|
||||||
setSelectedUsernames(new Set())
|
setSelectedUsernames(new Set())
|
||||||
setSelectedContact(prev => {
|
setSelectedContact(prev => {
|
||||||
if (!prev) return prev
|
if (!prev) return prev
|
||||||
@@ -167,7 +171,7 @@ function ContactsPage() {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [enrichContactsInBackground])
|
}, [enrichContactsInBackground, syncContactTypeCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadContacts()
|
loadContacts()
|
||||||
@@ -206,6 +210,8 @@ function ContactsPage() {
|
|||||||
return filtered
|
return filtered
|
||||||
}, [contacts, contactTypes, debouncedSearchKeyword])
|
}, [contacts, contactTypes, debouncedSearchKeyword])
|
||||||
|
|
||||||
|
const contactTypeCounts = useMemo(() => toContactTypeCardCounts(sharedTabCounts), [sharedTabCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!listRef.current) return
|
if (!listRef.current) return
|
||||||
listRef.current.scrollTop = 0
|
listRef.current.scrollTop = 0
|
||||||
@@ -428,19 +434,27 @@ function ContactsPage() {
|
|||||||
<div className="type-filters">
|
<div className="type-filters">
|
||||||
<label className={`filter-chip ${contactTypes.friends ? 'active' : ''}`}>
|
<label className={`filter-chip ${contactTypes.friends ? 'active' : ''}`}>
|
||||||
<input type="checkbox" checked={contactTypes.friends} onChange={e => setContactTypes({ ...contactTypes, friends: e.target.checked })} />
|
<input type="checkbox" checked={contactTypes.friends} onChange={e => setContactTypes({ ...contactTypes, friends: e.target.checked })} />
|
||||||
<User size={16} /><span>好友</span>
|
<User size={16} />
|
||||||
|
<span className="chip-label">好友</span>
|
||||||
|
<span className="chip-count">{contactTypeCounts.friends}</span>
|
||||||
</label>
|
</label>
|
||||||
<label className={`filter-chip ${contactTypes.groups ? 'active' : ''}`}>
|
<label className={`filter-chip ${contactTypes.groups ? 'active' : ''}`}>
|
||||||
<input type="checkbox" checked={contactTypes.groups} onChange={e => setContactTypes({ ...contactTypes, groups: e.target.checked })} />
|
<input type="checkbox" checked={contactTypes.groups} onChange={e => setContactTypes({ ...contactTypes, groups: e.target.checked })} />
|
||||||
<Users size={16} /><span>群聊</span>
|
<Users size={16} />
|
||||||
|
<span className="chip-label">群聊</span>
|
||||||
|
<span className="chip-count">{contactTypeCounts.groups}</span>
|
||||||
</label>
|
</label>
|
||||||
<label className={`filter-chip ${contactTypes.officials ? 'active' : ''}`}>
|
<label className={`filter-chip ${contactTypes.officials ? 'active' : ''}`}>
|
||||||
<input type="checkbox" checked={contactTypes.officials} onChange={e => setContactTypes({ ...contactTypes, officials: e.target.checked })} />
|
<input type="checkbox" checked={contactTypes.officials} onChange={e => setContactTypes({ ...contactTypes, officials: e.target.checked })} />
|
||||||
<MessageSquare size={16} /><span>公众号</span>
|
<MessageSquare size={16} />
|
||||||
|
<span className="chip-label">公众号</span>
|
||||||
|
<span className="chip-count">{contactTypeCounts.officials}</span>
|
||||||
</label>
|
</label>
|
||||||
<label className={`filter-chip ${contactTypes.deletedFriends ? 'active' : ''}`}>
|
<label className={`filter-chip ${contactTypes.deletedFriends ? 'active' : ''}`}>
|
||||||
<input type="checkbox" checked={contactTypes.deletedFriends} onChange={e => setContactTypes({ ...contactTypes, deletedFriends: e.target.checked })} />
|
<input type="checkbox" checked={contactTypes.deletedFriends} onChange={e => setContactTypes({ ...contactTypes, deletedFriends: e.target.checked })} />
|
||||||
<UserX size={16} /><span>曾经的好友</span>
|
<UserX size={16} />
|
||||||
|
<span className="chip-label">曾经的好友</span>
|
||||||
|
<span className="chip-count">{contactTypeCounts.deletedFriends}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import type { ChatSession as AppChatSession, ContactInfo } from '../types/models'
|
import type { ChatSession as AppChatSession, ContactInfo } from '../types/models'
|
||||||
import type { ExportOptions as ElectronExportOptions, ExportProgress } from '../types/electron'
|
import type { ExportOptions as ElectronExportOptions, ExportProgress } from '../types/electron'
|
||||||
import * as configService from '../services/config'
|
import * as configService from '../services/config'
|
||||||
|
import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
|
||||||
import './ExportPage.scss'
|
import './ExportPage.scss'
|
||||||
|
|
||||||
type ConversationTab = 'private' | 'group' | 'official' | 'former_friend'
|
type ConversationTab = 'private' | 'group' | 'official' | 'former_friend'
|
||||||
@@ -321,12 +322,10 @@ 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 [sessionMessageCounts, setSessionMessageCounts] = useState<Record<string, number>>({})
|
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('')
|
||||||
@@ -374,6 +373,11 @@ function ExportPage() {
|
|||||||
})
|
})
|
||||||
const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false)
|
const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false)
|
||||||
const [nowTick, setNowTick] = useState(Date.now())
|
const [nowTick, setNowTick] = useState(Date.now())
|
||||||
|
const tabCounts = useContactTypeCountsStore(state => state.tabCounts)
|
||||||
|
const isSharedTabCountsLoading = useContactTypeCountsStore(state => state.isLoading)
|
||||||
|
const isSharedTabCountsReady = useContactTypeCountsStore(state => state.isReady)
|
||||||
|
const ensureSharedTabCountsLoaded = useContactTypeCountsStore(state => state.ensureLoaded)
|
||||||
|
const syncContactTypeCounts = useContactTypeCountsStore(state => state.syncFromContacts)
|
||||||
|
|
||||||
const progressUnsubscribeRef = useRef<(() => void) | null>(null)
|
const progressUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||||
const runningTaskIdRef = useRef<string | null>(null)
|
const runningTaskIdRef = useRef<string | null>(null)
|
||||||
@@ -516,20 +520,6 @@ 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 (options?: { full?: boolean; silent?: boolean }) => {
|
const loadSnsStats = useCallback(async (options?: { full?: boolean; silent?: boolean }) => {
|
||||||
if (!options?.silent) {
|
if (!options?.silent) {
|
||||||
setIsSnsStatsLoading(true)
|
setIsSnsStatsLoading(true)
|
||||||
@@ -641,6 +631,9 @@ function ExportPage() {
|
|||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
|
|
||||||
const contacts: ContactInfo[] = contactsResult?.success && contactsResult.contacts ? contactsResult.contacts : []
|
const contacts: ContactInfo[] = contactsResult?.success && contactsResult.contacts ? contactsResult.contacts : []
|
||||||
|
if (contacts.length > 0) {
|
||||||
|
syncContactTypeCounts(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
|
||||||
@@ -694,11 +687,11 @@ function ExportPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
if (!isStale()) setIsLoading(false)
|
if (!isStale()) setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [syncContactTypeCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadBaseConfig()
|
void loadBaseConfig()
|
||||||
void loadTabCounts()
|
void ensureSharedTabCountsLoaded()
|
||||||
void loadSessions()
|
void loadSessions()
|
||||||
|
|
||||||
// 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。
|
// 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。
|
||||||
@@ -707,7 +700,7 @@ function ExportPage() {
|
|||||||
}, 120)
|
}, 120)
|
||||||
|
|
||||||
return () => window.clearTimeout(timer)
|
return () => window.clearTimeout(timer)
|
||||||
}, [loadTabCounts, loadBaseConfig, loadSessions, loadSnsStats])
|
}, [ensureSharedTabCountsLoaded, loadBaseConfig, loadSessions, loadSnsStats])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
preselectAppliedRef.current = false
|
preselectAppliedRef.current = false
|
||||||
@@ -1363,29 +1356,6 @@ function ExportPage() {
|
|||||||
return set
|
return set
|
||||||
}, [tasks])
|
}, [tasks])
|
||||||
|
|
||||||
const sessionTabCounts = useMemo(() => {
|
|
||||||
const counts: Record<ConversationTab, number> = {
|
|
||||||
private: 0,
|
|
||||||
group: 0,
|
|
||||||
official: 0,
|
|
||||||
former_friend: 0
|
|
||||||
}
|
|
||||||
for (const session of sessions) {
|
|
||||||
counts[session.kind] += 1
|
|
||||||
}
|
|
||||||
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 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
|
||||||
@@ -1617,8 +1587,7 @@ 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 hasTabCountsSource = prefetchedTabCounts !== null || sessions.length > 0
|
const isTabCountComputing = isSharedTabCountsLoading && !isSharedTabCountsReady
|
||||||
const isTabCountComputing = isTabCountsLoading && !hasTabCountsSource
|
|
||||||
const isSessionCardStatsLoading = isLoading || isBaseConfigLoading
|
const isSessionCardStatsLoading = isLoading || isBaseConfigLoading
|
||||||
const isSnsCardStatsLoading = !hasSeededSnsStats
|
const isSnsCardStatsLoading = !hasSeededSnsStats
|
||||||
const taskRunningCount = tasks.filter(task => task.status === 'running').length
|
const taskRunningCount = tasks.filter(task => task.status === 'running').length
|
||||||
|
|||||||
115
src/stores/contactTypeCountsStore.ts
Normal file
115
src/stores/contactTypeCountsStore.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import type { ContactInfo } from '../types/models'
|
||||||
|
|
||||||
|
export interface ContactTypeTabCounts {
|
||||||
|
private: number
|
||||||
|
group: number
|
||||||
|
official: number
|
||||||
|
former_friend: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContactTypeCardCounts {
|
||||||
|
friends: number
|
||||||
|
groups: number
|
||||||
|
officials: number
|
||||||
|
deletedFriends: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyTabCounts: ContactTypeTabCounts = {
|
||||||
|
private: 0,
|
||||||
|
group: 0,
|
||||||
|
official: 0,
|
||||||
|
former_friend: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let inflightPromise: Promise<ContactTypeTabCounts> | null = null
|
||||||
|
|
||||||
|
const normalizeCounts = (counts?: Partial<ContactTypeTabCounts> | null): ContactTypeTabCounts => {
|
||||||
|
return {
|
||||||
|
private: Number.isFinite(counts?.private) ? Math.max(0, Math.floor(Number(counts?.private))) : 0,
|
||||||
|
group: Number.isFinite(counts?.group) ? Math.max(0, Math.floor(Number(counts?.group))) : 0,
|
||||||
|
official: Number.isFinite(counts?.official) ? Math.max(0, Math.floor(Number(counts?.official))) : 0,
|
||||||
|
former_friend: Number.isFinite(counts?.former_friend) ? Math.max(0, Math.floor(Number(counts?.former_friend))) : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toContactTypeTabCountsFromContacts = (contacts: ContactInfo[]): ContactTypeTabCounts => {
|
||||||
|
const next = { ...emptyTabCounts }
|
||||||
|
for (const contact of contacts || []) {
|
||||||
|
if (contact.type === 'friend') next.private += 1
|
||||||
|
if (contact.type === 'group') next.group += 1
|
||||||
|
if (contact.type === 'official') next.official += 1
|
||||||
|
if (contact.type === 'former_friend') next.former_friend += 1
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toContactTypeCardCounts = (counts: ContactTypeTabCounts): ContactTypeCardCounts => {
|
||||||
|
return {
|
||||||
|
friends: counts.private,
|
||||||
|
groups: counts.group,
|
||||||
|
officials: counts.official,
|
||||||
|
deletedFriends: counts.former_friend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContactTypeCountsState {
|
||||||
|
tabCounts: ContactTypeTabCounts
|
||||||
|
isLoading: boolean
|
||||||
|
isReady: boolean
|
||||||
|
updatedAt: number
|
||||||
|
setTabCounts: (counts: ContactTypeTabCounts) => void
|
||||||
|
syncFromContacts: (contacts: ContactInfo[]) => void
|
||||||
|
ensureLoaded: (options?: { force?: boolean }) => Promise<ContactTypeTabCounts>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useContactTypeCountsStore = create<ContactTypeCountsState>((set, get) => ({
|
||||||
|
tabCounts: { ...emptyTabCounts },
|
||||||
|
isLoading: false,
|
||||||
|
isReady: false,
|
||||||
|
updatedAt: 0,
|
||||||
|
setTabCounts: (counts) => {
|
||||||
|
const normalized = normalizeCounts(counts)
|
||||||
|
set({
|
||||||
|
tabCounts: normalized,
|
||||||
|
isReady: true,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
syncFromContacts: (contacts) => {
|
||||||
|
const fromContacts = toContactTypeTabCountsFromContacts(contacts || [])
|
||||||
|
get().setTabCounts(fromContacts)
|
||||||
|
},
|
||||||
|
ensureLoaded: async (options) => {
|
||||||
|
if (!options?.force && get().isReady) {
|
||||||
|
return get().tabCounts
|
||||||
|
}
|
||||||
|
if (inflightPromise) {
|
||||||
|
return inflightPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ isLoading: true })
|
||||||
|
inflightPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.chat.getContactTypeCounts()
|
||||||
|
if (result?.success && result.counts) {
|
||||||
|
const normalized = normalizeCounts(result.counts)
|
||||||
|
set({
|
||||||
|
tabCounts: normalized,
|
||||||
|
isReady: true,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
})
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载联系人类型计数失败:', error)
|
||||||
|
}
|
||||||
|
return get().tabCounts
|
||||||
|
})().finally(() => {
|
||||||
|
inflightPromise = null
|
||||||
|
set({ isLoading: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
return inflightPromise
|
||||||
|
}
|
||||||
|
}))
|
||||||
10
src/types/electron.d.ts
vendored
10
src/types/electron.d.ts
vendored
@@ -89,6 +89,16 @@ export interface ElectronAPI {
|
|||||||
}
|
}
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
getContactTypeCounts: () => Promise<{
|
||||||
|
success: boolean
|
||||||
|
counts?: {
|
||||||
|
private: number
|
||||||
|
group: number
|
||||||
|
official: number
|
||||||
|
former_friend: number
|
||||||
|
}
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
getSessionMessageCounts: (sessionIds: string[]) => Promise<{
|
getSessionMessageCounts: (sessionIds: string[]) => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
counts?: Record<string, number>
|
counts?: Record<string, number>
|
||||||
|
|||||||
Reference in New Issue
Block a user