mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(counts): unify contacts and export tab counters
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user