mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
feat(export): add session total message count column with staged loading
This commit is contained in:
@@ -1009,6 +1009,9 @@
|
|||||||
|
|
||||||
.meta-item.syncing {
|
.meta-item.syncing {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1330,6 +1333,37 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
flex-shrink: 0;
|
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 {
|
.table-virtuoso {
|
||||||
@@ -2246,6 +2280,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
.table-wrap .row-message-count {
|
||||||
|
min-width: 66px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap .row-message-count-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.diag-panel-header {
|
.diag-panel-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
@@ -780,6 +780,12 @@ const toSessionRowsWithContacts = (
|
|||||||
.sort((a, b) => (b.sortTimestamp || b.lastTimestamp || 0) - (a.sortTimestamp || a.lastTimestamp || 0))
|
.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({
|
const WriteLayoutSelector = memo(function WriteLayoutSelector({
|
||||||
writeLayout,
|
writeLayout,
|
||||||
onChange
|
onChange
|
||||||
@@ -856,6 +862,8 @@ function ExportPage() {
|
|||||||
const [contactsDataSource, setContactsDataSource] = useState<ContactsDataSource>(null)
|
const [contactsDataSource, setContactsDataSource] = useState<ContactsDataSource>(null)
|
||||||
const [contactsUpdatedAt, setContactsUpdatedAt] = useState<number | null>(null)
|
const [contactsUpdatedAt, setContactsUpdatedAt] = useState<number | null>(null)
|
||||||
const [avatarCacheUpdatedAt, setAvatarCacheUpdatedAt] = useState<number | null>(null)
|
const [avatarCacheUpdatedAt, setAvatarCacheUpdatedAt] = useState<number | null>(null)
|
||||||
|
const [sessionMessageCounts, setSessionMessageCounts] = useState<Record<string, number>>({})
|
||||||
|
const [isLoadingSessionCounts, setIsLoadingSessionCounts] = useState(false)
|
||||||
const [contactsListScrollTop, setContactsListScrollTop] = useState(0)
|
const [contactsListScrollTop, setContactsListScrollTop] = useState(0)
|
||||||
const [contactsListViewportHeight, setContactsListViewportHeight] = useState(480)
|
const [contactsListViewportHeight, setContactsListViewportHeight] = useState(480)
|
||||||
const [contactsLoadTimeoutMs, setContactsLoadTimeoutMs] = useState(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS)
|
const [contactsLoadTimeoutMs, setContactsLoadTimeoutMs] = useState(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS)
|
||||||
@@ -945,6 +953,8 @@ function ExportPage() {
|
|||||||
const inProgressSessionIdsRef = useRef<string[]>([])
|
const inProgressSessionIdsRef = useRef<string[]>([])
|
||||||
const activeTaskCountRef = useRef(0)
|
const activeTaskCountRef = useRef(0)
|
||||||
const hasBaseConfigReadyRef = useRef(false)
|
const hasBaseConfigReadyRef = useRef(false)
|
||||||
|
const sessionCountRequestIdRef = useRef(0)
|
||||||
|
const activeTabRef = useRef<ConversationTab>('private')
|
||||||
|
|
||||||
const appendFrontendDiagLog = useCallback((entry: ExportCardDiagLogEntry) => {
|
const appendFrontendDiagLog = useCallback((entry: ExportCardDiagLogEntry) => {
|
||||||
setFrontendDiagLogs(prev => {
|
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<Record<string, number>>((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<string, number> | undefined) => {
|
||||||
|
if (!input || isStale()) return
|
||||||
|
const normalized = Object.entries(input).reduce<Record<string, number>>((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 loadSessions = useCallback(async () => {
|
||||||
const loadToken = Date.now()
|
const loadToken = Date.now()
|
||||||
sessionLoadTokenRef.current = loadToken
|
sessionLoadTokenRef.current = loadToken
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setIsSessionEnriching(false)
|
setIsSessionEnriching(false)
|
||||||
|
sessionCountRequestIdRef.current += 1
|
||||||
|
setSessionMessageCounts({})
|
||||||
|
setIsLoadingSessionCounts(false)
|
||||||
|
|
||||||
const isStale = () => sessionLoadTokenRef.current !== loadToken
|
const isStale = () => sessionLoadTokenRef.current !== loadToken
|
||||||
|
|
||||||
@@ -1433,6 +1516,7 @@ function ExportPage() {
|
|||||||
|
|
||||||
if (isStale()) return
|
if (isStale()) return
|
||||||
setSessions(baseSessions)
|
setSessions(baseSessions)
|
||||||
|
void loadSessionMessageCounts(baseSessions, activeTabRef.current)
|
||||||
setSessionDataSource(cachedContacts.length > 0 ? 'cache' : 'network')
|
setSessionDataSource(cachedContacts.length > 0 ? 'cache' : 'network')
|
||||||
if (cachedContacts.length === 0) {
|
if (cachedContacts.length === 0) {
|
||||||
setSessionContactsUpdatedAt(Date.now())
|
setSessionContactsUpdatedAt(Date.now())
|
||||||
@@ -1620,7 +1704,7 @@ function ExportPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
if (!isStale()) setIsLoading(false)
|
if (!isStale()) setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [ensureExportCacheScope, loadContactsCaches, syncContactTypeCounts])
|
}, [ensureExportCacheScope, loadContactsCaches, loadSessionMessageCounts, syncContactTypeCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isExportRoute) return
|
if (!isExportRoute) return
|
||||||
@@ -1649,9 +1733,15 @@ function ExportPage() {
|
|||||||
if (isExportRoute) return
|
if (isExportRoute) return
|
||||||
// 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。
|
// 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。
|
||||||
sessionLoadTokenRef.current = Date.now()
|
sessionLoadTokenRef.current = Date.now()
|
||||||
|
sessionCountRequestIdRef.current += 1
|
||||||
setIsSessionEnriching(false)
|
setIsSessionEnriching(false)
|
||||||
|
setIsLoadingSessionCounts(false)
|
||||||
}, [isExportRoute])
|
}, [isExportRoute])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
activeTabRef.current = activeTab
|
||||||
|
}, [activeTab])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
preselectAppliedRef.current = false
|
preselectAppliedRef.current = false
|
||||||
}, [location.key, preselectSessionIds])
|
}, [location.key, preselectSessionIds])
|
||||||
@@ -3783,6 +3873,12 @@ function ExportPage() {
|
|||||||
{isContactsListLoading && contactsList.length > 0 && (
|
{isContactsListLoading && contactsList.length > 0 && (
|
||||||
<span className="meta-item syncing">后台同步中...</span>
|
<span className="meta-item syncing">后台同步中...</span>
|
||||||
)}
|
)}
|
||||||
|
{isLoadingSessionCounts && (
|
||||||
|
<span className="meta-item syncing">
|
||||||
|
<Loader2 size={12} className="spin" />
|
||||||
|
消息总数统计中…
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{contactsList.length > 0 && isContactsListLoading && (
|
{contactsList.length > 0 && isContactsListLoading && (
|
||||||
@@ -3850,6 +3946,14 @@ function ExportPage() {
|
|||||||
const isQueued = canExport && queuedSessionIds.has(contact.username)
|
const isQueued = canExport && queuedSessionIds.has(contact.username)
|
||||||
const isPaused = canExport && pausedSessionIds.has(contact.username)
|
const isPaused = canExport && pausedSessionIds.has(contact.username)
|
||||||
const recent = canExport ? formatRecentExportTime(lastExportBySession[contact.username], nowTick) : ''
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={contact.username}
|
key={contact.username}
|
||||||
@@ -3871,6 +3975,12 @@ function ExportPage() {
|
|||||||
<div className={`contact-type ${contact.type}`}>
|
<div className={`contact-type ${contact.type}`}>
|
||||||
<span>{getContactTypeName(contact.type)}</span>
|
<span>{getContactTypeName(contact.type)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="row-message-count">
|
||||||
|
<span className="row-message-count-label">总消息</span>
|
||||||
|
<strong className={`row-message-count-value ${typeof displayedMessageCount === 'number' ? '' : 'muted'}`}>
|
||||||
|
{messageCountLabel}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
<div className="row-action-cell">
|
<div className="row-action-cell">
|
||||||
<div className="row-action-main">
|
<div className="row-action-main">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user