mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(counts): unify contacts and export tab counters
This commit is contained in:
@@ -920,6 +920,10 @@ function registerIpcHandlers() {
|
||||
return chatService.getExportTabCounts()
|
||||
})
|
||||
|
||||
ipcMain.handle('chat:getContactTypeCounts', async () => {
|
||||
return chatService.getContactTypeCounts()
|
||||
})
|
||||
|
||||
ipcMain.handle('chat:getSessionMessageCounts', async (_, sessionIds: string[]) => {
|
||||
return chatService.getSessionMessageCounts(sessionIds)
|
||||
})
|
||||
|
||||
@@ -132,6 +132,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getSessions: () => ipcRenderer.invoke('chat:getSessions'),
|
||||
getSessionStatuses: (usernames: string[]) => ipcRenderer.invoke('chat:getSessionStatuses', usernames),
|
||||
getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'),
|
||||
getContactTypeCounts: () => ipcRenderer.invoke('chat:getContactTypeCounts'),
|
||||
getSessionMessageCounts: (sessionIds: string[]) => ipcRenderer.invoke('chat:getSessionMessageCounts', sessionIds),
|
||||
enrichSessionsContactInfo: (usernames: string[]) =>
|
||||
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 {
|
||||
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 excludeExpr = Array.from(FRIEND_EXCLUDE_USERNAMES)
|
||||
.map((username) => `'${this.escapeSqlString(username)}'`)
|
||||
.join(',')
|
||||
|
||||
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
|
||||
const countsSql = `
|
||||
SELECT
|
||||
SUM(CASE WHEN username LIKE '%@chatroom' THEN 1 ELSE 0 END) AS group_count,
|
||||
SUM(CASE WHEN username LIKE 'gh_%' THEN 1 ELSE 0 END) AS official_count,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN username NOT LIKE '%@chatroom'
|
||||
AND username NOT LIKE 'gh_%'
|
||||
AND local_type = 1
|
||||
AND username NOT IN (${excludeExpr})
|
||||
THEN 1 ELSE 0
|
||||
END
|
||||
) AS private_count,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN username NOT LIKE '%@chatroom'
|
||||
AND username NOT LIKE 'gh_%'
|
||||
AND local_type = 0
|
||||
AND COALESCE(quan_pin, '') != ''
|
||||
THEN 1 ELSE 0
|
||||
END
|
||||
) AS former_friend_count
|
||||
FROM contact
|
||||
WHERE username IN (${usernamesExpr})
|
||||
WHERE username IS NOT NULL
|
||||
AND username != ''
|
||||
`
|
||||
|
||||
const contactResult = await wcdbService.execQuery('contact', null, contactSql)
|
||||
if (!contactResult.success || !contactResult.rows) {
|
||||
continue
|
||||
const result = await wcdbService.execQuery('contact', null, countsSql)
|
||||
if (!result.success || !result.rows || result.rows.length === 0) {
|
||||
return { success: false, error: result.error || '获取联系人类型数量失败' }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
const row = result.rows[0] as Record<string, any>
|
||||
const counts: ExportTabCounts = {
|
||||
private: this.getRowInt(row, ['private_count', 'privateCount'], 0),
|
||||
group: this.getRowInt(row, ['group_count', 'groupCount'], 0),
|
||||
official: this.getRowInt(row, ['official_count', 'officialCount'], 0),
|
||||
former_friend: this.getRowInt(row, ['former_friend_count', 'formerFriendCount'], 0)
|
||||
}
|
||||
|
||||
return { success: true, counts }
|
||||
} catch (e) {
|
||||
console.error('ChatService: 获取导出页会话分类数量失败:', e)
|
||||
console.error('ChatService: 获取联系人类型数量失败:', 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 {
|
||||
opacity: 0.7;
|
||||
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 {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useMemo, useRef, type UIEvent } from
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown, MessageCircle, UserX } from 'lucide-react'
|
||||
import { useChatStore } from '../stores/chatStore'
|
||||
import { toContactTypeCardCounts, useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
|
||||
import './ContactsPage.scss'
|
||||
|
||||
interface ContactInfo {
|
||||
@@ -58,6 +59,8 @@ function ContactsPage() {
|
||||
})
|
||||
const [scrollTop, setScrollTop] = useState(0)
|
||||
const [listViewportHeight, setListViewportHeight] = useState(480)
|
||||
const sharedTabCounts = useContactTypeCountsStore(state => state.tabCounts)
|
||||
const syncContactTypeCounts = useContactTypeCountsStore(state => state.syncFromContacts)
|
||||
|
||||
const applyEnrichedContacts = useCallback((enrichedMap: Record<string, ContactEnrichInfo>) => {
|
||||
if (!enrichedMap || Object.keys(enrichedMap).length === 0) return
|
||||
@@ -151,6 +154,7 @@ function ContactsPage() {
|
||||
if (loadVersionRef.current !== loadVersion) return
|
||||
if (contactsResult.success && contactsResult.contacts) {
|
||||
setContacts(contactsResult.contacts)
|
||||
syncContactTypeCounts(contactsResult.contacts)
|
||||
setSelectedUsernames(new Set())
|
||||
setSelectedContact(prev => {
|
||||
if (!prev) return prev
|
||||
@@ -167,7 +171,7 @@ function ContactsPage() {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [enrichContactsInBackground])
|
||||
}, [enrichContactsInBackground, syncContactTypeCounts])
|
||||
|
||||
useEffect(() => {
|
||||
loadContacts()
|
||||
@@ -206,6 +210,8 @@ function ContactsPage() {
|
||||
return filtered
|
||||
}, [contacts, contactTypes, debouncedSearchKeyword])
|
||||
|
||||
const contactTypeCounts = useMemo(() => toContactTypeCardCounts(sharedTabCounts), [sharedTabCounts])
|
||||
|
||||
useEffect(() => {
|
||||
if (!listRef.current) return
|
||||
listRef.current.scrollTop = 0
|
||||
@@ -428,19 +434,27 @@ function ContactsPage() {
|
||||
<div className="type-filters">
|
||||
<label className={`filter-chip ${contactTypes.friends ? 'active' : ''}`}>
|
||||
<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 className={`filter-chip ${contactTypes.groups ? 'active' : ''}`}>
|
||||
<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 className={`filter-chip ${contactTypes.officials ? 'active' : ''}`}>
|
||||
<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 className={`filter-chip ${contactTypes.deletedFriends ? 'active' : ''}`}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import type { ChatSession as AppChatSession, ContactInfo } from '../types/models'
|
||||
import type { ExportOptions as ElectronExportOptions, ExportProgress } from '../types/electron'
|
||||
import * as configService from '../services/config'
|
||||
import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore'
|
||||
import './ExportPage.scss'
|
||||
|
||||
type ConversationTab = 'private' | 'group' | 'official' | 'former_friend'
|
||||
@@ -321,12 +322,10 @@ 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<SessionRow[]>([])
|
||||
const [prefetchedTabCounts, setPrefetchedTabCounts] = useState<Record<ConversationTab, number> | null>(null)
|
||||
const [sessionMessageCounts, setSessionMessageCounts] = useState<Record<string, number>>({})
|
||||
const [sessionMetrics, setSessionMetrics] = useState<Record<string, SessionMetrics>>({})
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
@@ -374,6 +373,11 @@ function ExportPage() {
|
||||
})
|
||||
const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false)
|
||||
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 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 }) => {
|
||||
if (!options?.silent) {
|
||||
setIsSnsStatsLoading(true)
|
||||
@@ -641,6 +631,9 @@ function ExportPage() {
|
||||
if (isStale()) return
|
||||
|
||||
const contacts: ContactInfo[] = contactsResult?.success && contactsResult.contacts ? contactsResult.contacts : []
|
||||
if (contacts.length > 0) {
|
||||
syncContactTypeCounts(contacts)
|
||||
}
|
||||
const nextContactMap = contacts.reduce<Record<string, ContactInfo>>((map, contact) => {
|
||||
map[contact.username] = contact
|
||||
return map
|
||||
@@ -694,11 +687,11 @@ function ExportPage() {
|
||||
} finally {
|
||||
if (!isStale()) setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
}, [syncContactTypeCounts])
|
||||
|
||||
useEffect(() => {
|
||||
void loadBaseConfig()
|
||||
void loadTabCounts()
|
||||
void ensureSharedTabCountsLoaded()
|
||||
void loadSessions()
|
||||
|
||||
// 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。
|
||||
@@ -707,7 +700,7 @@ function ExportPage() {
|
||||
}, 120)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [loadTabCounts, loadBaseConfig, loadSessions, loadSnsStats])
|
||||
}, [ensureSharedTabCountsLoaded, loadBaseConfig, loadSessions, loadSnsStats])
|
||||
|
||||
useEffect(() => {
|
||||
preselectAppliedRef.current = false
|
||||
@@ -1363,29 +1356,6 @@ function ExportPage() {
|
||||
return set
|
||||
}, [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 scopeSessions = sessions.filter(session => session.kind === 'private' || session.kind === 'group')
|
||||
const totalSessions = scopeSessions.length
|
||||
@@ -1617,8 +1587,7 @@ function ExportPage() {
|
||||
const formatCandidateOptions = exportDialog.scope === 'sns'
|
||||
? formatOptions.filter(option => option.value === 'html' || option.value === 'json')
|
||||
: formatOptions
|
||||
const hasTabCountsSource = prefetchedTabCounts !== null || sessions.length > 0
|
||||
const isTabCountComputing = isTabCountsLoading && !hasTabCountsSource
|
||||
const isTabCountComputing = isSharedTabCountsLoading && !isSharedTabCountsReady
|
||||
const isSessionCardStatsLoading = isLoading || isBaseConfigLoading
|
||||
const isSnsCardStatsLoading = !hasSeededSnsStats
|
||||
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
|
||||
}>
|
||||
getContactTypeCounts: () => Promise<{
|
||||
success: boolean
|
||||
counts?: {
|
||||
private: number
|
||||
group: number
|
||||
official: number
|
||||
former_friend: number
|
||||
}
|
||||
error?: string
|
||||
}>
|
||||
getSessionMessageCounts: (sessionIds: string[]) => Promise<{
|
||||
success: boolean
|
||||
counts?: Record<string, number>
|
||||
|
||||
Reference in New Issue
Block a user