From 756a83191d979f6ea32187abf572632754528b3e Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Wed, 4 Mar 2026 14:19:01 +0800 Subject: [PATCH] feat(export): add session total message count column with staged loading --- src/pages/ExportPage.scss | 42 ++++++++++++++ src/pages/ExportPage.tsx | 112 +++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 1 deletion(-) diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index a9a814b..39eecb5 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -1009,6 +1009,9 @@ .meta-item.syncing { color: var(--primary); + display: inline-flex; + align-items: center; + gap: 4px; } } @@ -1330,6 +1333,37 @@ color: var(--text-secondary); flex-shrink: 0; } + + .row-message-count { + min-width: 82px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + flex-shrink: 0; + text-align: right; + } + + .row-message-count-label { + font-size: 11px; + color: var(--text-tertiary); + line-height: 1; + } + + .row-message-count-value { + margin: 0; + font-size: 13px; + line-height: 1.2; + color: var(--text-primary); + font-weight: 600; + font-variant-numeric: tabular-nums; + + &.muted { + font-size: 12px; + font-weight: 500; + color: var(--text-tertiary); + } + } } .table-virtuoso { @@ -2246,6 +2280,14 @@ } @media (max-width: 720px) { + .table-wrap .row-message-count { + min-width: 66px; + } + + .table-wrap .row-message-count-label { + display: none; + } + .diag-panel-header { flex-direction: column; align-items: stretch; diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 01e4b08..b781892 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -780,6 +780,12 @@ const toSessionRowsWithContacts = ( .sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0)) } +const normalizeMessageCount = (value: unknown): number | undefined => { + const parsed = Number(value) + if (!Number.isFinite(parsed) || parsed < 0) return undefined + return Math.floor(parsed) +} + const WriteLayoutSelector = memo(function WriteLayoutSelector({ writeLayout, onChange @@ -856,6 +862,8 @@ function ExportPage() { const [contactsDataSource, setContactsDataSource] = useState(null) const [contactsUpdatedAt, setContactsUpdatedAt] = useState(null) const [avatarCacheUpdatedAt, setAvatarCacheUpdatedAt] = useState(null) + const [sessionMessageCounts, setSessionMessageCounts] = useState>({}) + const [isLoadingSessionCounts, setIsLoadingSessionCounts] = useState(false) const [contactsListScrollTop, setContactsListScrollTop] = useState(0) const [contactsListViewportHeight, setContactsListViewportHeight] = useState(480) const [contactsLoadTimeoutMs, setContactsLoadTimeoutMs] = useState(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS) @@ -945,6 +953,8 @@ function ExportPage() { const inProgressSessionIdsRef = useRef([]) const activeTaskCountRef = useRef(0) const hasBaseConfigReadyRef = useRef(false) + const sessionCountRequestIdRef = useRef(0) + const activeTabRef = useRef('private') const appendFrontendDiagLog = useCallback((entry: ExportCardDiagLogEntry) => { setFrontendDiagLogs(prev => { @@ -1387,11 +1397,84 @@ function ExportPage() { } }, []) + const loadSessionMessageCounts = useCallback(async ( + sourceSessions: SessionRow[], + priorityTab: ConversationTab + ) => { + const requestId = sessionCountRequestIdRef.current + 1 + sessionCountRequestIdRef.current = requestId + const isStale = () => sessionCountRequestIdRef.current !== requestId + + const exportableSessions = sourceSessions.filter(session => session.hasSession) + const seededHintCounts = exportableSessions.reduce>((acc, session) => { + const nextCount = normalizeMessageCount(session.messageCountHint) + if (typeof nextCount === 'number') { + acc[session.username] = nextCount + } + return acc + }, {}) + setSessionMessageCounts(seededHintCounts) + + if (exportableSessions.length === 0) { + setIsLoadingSessionCounts(false) + return + } + + const prioritizedSessionIds = exportableSessions + .filter(session => session.kind === priorityTab) + .map(session => session.username) + const prioritizedSet = new Set(prioritizedSessionIds) + const remainingSessionIds = exportableSessions + .filter(session => !prioritizedSet.has(session.username)) + .map(session => session.username) + + const applyCounts = (input: Record | undefined) => { + if (!input || isStale()) return + const normalized = Object.entries(input).reduce>((acc, [sessionId, count]) => { + const nextCount = normalizeMessageCount(count) + if (typeof nextCount === 'number') { + acc[sessionId] = nextCount + } + return acc + }, {}) + if (Object.keys(normalized).length === 0) return + setSessionMessageCounts(prev => ({ ...prev, ...normalized })) + } + + setIsLoadingSessionCounts(true) + try { + if (prioritizedSessionIds.length > 0) { + const priorityResult = await window.electronAPI.chat.getSessionMessageCounts(prioritizedSessionIds) + if (isStale()) return + if (priorityResult.success) { + applyCounts(priorityResult.counts) + } + } + + if (remainingSessionIds.length > 0) { + const remainingResult = await window.electronAPI.chat.getSessionMessageCounts(remainingSessionIds) + if (isStale()) return + if (remainingResult.success) { + applyCounts(remainingResult.counts) + } + } + } catch (error) { + console.error('导出页加载会话消息总数失败:', error) + } finally { + if (!isStale()) { + setIsLoadingSessionCounts(false) + } + } + }, []) + const loadSessions = useCallback(async () => { const loadToken = Date.now() sessionLoadTokenRef.current = loadToken setIsLoading(true) setIsSessionEnriching(false) + sessionCountRequestIdRef.current += 1 + setSessionMessageCounts({}) + setIsLoadingSessionCounts(false) const isStale = () => sessionLoadTokenRef.current !== loadToken @@ -1433,6 +1516,7 @@ function ExportPage() { if (isStale()) return setSessions(baseSessions) + void loadSessionMessageCounts(baseSessions, activeTabRef.current) setSessionDataSource(cachedContacts.length > 0 ? 'cache' : 'network') if (cachedContacts.length === 0) { setSessionContactsUpdatedAt(Date.now()) @@ -1620,7 +1704,7 @@ function ExportPage() { } finally { if (!isStale()) setIsLoading(false) } - }, [ensureExportCacheScope, loadContactsCaches, syncContactTypeCounts]) + }, [ensureExportCacheScope, loadContactsCaches, loadSessionMessageCounts, syncContactTypeCounts]) useEffect(() => { if (!isExportRoute) return @@ -1649,9 +1733,15 @@ function ExportPage() { if (isExportRoute) return // 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。 sessionLoadTokenRef.current = Date.now() + sessionCountRequestIdRef.current += 1 setIsSessionEnriching(false) + setIsLoadingSessionCounts(false) }, [isExportRoute]) + useEffect(() => { + activeTabRef.current = activeTab + }, [activeTab]) + useEffect(() => { preselectAppliedRef.current = false }, [location.key, preselectSessionIds]) @@ -3783,6 +3873,12 @@ function ExportPage() { {isContactsListLoading && contactsList.length > 0 && ( 后台同步中... )} + {isLoadingSessionCounts && ( + + + 消息总数统计中… + + )} {contactsList.length > 0 && isContactsListLoading && ( @@ -3850,6 +3946,14 @@ function ExportPage() { const isQueued = canExport && queuedSessionIds.has(contact.username) const isPaused = canExport && pausedSessionIds.has(contact.username) const recent = canExport ? formatRecentExportTime(lastExportBySession[contact.username], nowTick) : '' + const countedMessages = normalizeMessageCount(sessionMessageCounts[contact.username]) + const hintedMessages = normalizeMessageCount(matchedSession?.messageCountHint) + const displayedMessageCount = countedMessages ?? hintedMessages + const messageCountLabel = !canExport + ? '--' + : typeof displayedMessageCount === 'number' + ? displayedMessageCount.toLocaleString('zh-CN') + : (isLoadingSessionCounts ? '统计中…' : '--') return (
{getContactTypeName(contact.type)}
+
+ 总消息 + + {messageCountLabel} + +