diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss
index 76778f5..132b957 100644
--- a/src/pages/ExportPage.scss
+++ b/src/pages/ExportPage.scss
@@ -362,6 +362,10 @@
cursor: pointer;
font-size: 12px;
font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
&:hover {
background: var(--primary-hover);
@@ -374,6 +378,7 @@
&.running {
background: var(--primary-hover);
+ opacity: 0.65;
}
}
diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx
index 707646e..8aa55ea 100644
--- a/src/pages/ExportPage.tsx
+++ b/src/pages/ExportPage.tsx
@@ -801,6 +801,182 @@ const WriteLayoutSelector = memo(function WriteLayoutSelector({
)
})
+interface TaskCenterModalProps {
+ isOpen: boolean
+ tasks: ExportTask[]
+ taskRunningCount: number
+ taskQueuedCount: number
+ expandedPerfTaskId: string | null
+ nowTick: number
+ onClose: () => void
+ onTogglePerfTask: (taskId: string) => void
+}
+
+const TaskCenterModal = memo(function TaskCenterModal({
+ isOpen,
+ tasks,
+ taskRunningCount,
+ taskQueuedCount,
+ expandedPerfTaskId,
+ nowTick,
+ onClose,
+ onTogglePerfTask
+}: TaskCenterModalProps) {
+ if (!isOpen) return null
+
+ return (
+
+
event.stopPropagation()}
+ >
+
+
+
任务中心
+ 进行中 {taskRunningCount} · 排队 {taskQueuedCount} · 总计 {tasks.length}
+
+
+
+
+ {tasks.length === 0 ? (
+
暂无任务。点击会话导出或卡片导出后会在这里创建任务。
+ ) : (
+
+ {tasks.map(task => {
+ const canShowPerfDetail = isTextBatchTask(task) && Boolean(task.performance)
+ const isPerfExpanded = expandedPerfTaskId === task.id
+ const stageTotals = canShowPerfDetail
+ ? getTaskPerformanceStageTotals(task.performance, nowTick)
+ : null
+ const stageTotalMs = stageTotals
+ ? stageTotals.collect + stageTotals.build + stageTotals.write + stageTotals.other
+ : 0
+ const topSessions = isPerfExpanded
+ ? getTaskPerformanceTopSessions(task.performance, nowTick, 5)
+ : []
+ const normalizedProgressTotal = task.progress.total > 0 ? task.progress.total : 0
+ const normalizedProgressCurrent = normalizedProgressTotal > 0
+ ? Math.max(0, Math.min(normalizedProgressTotal, task.progress.current))
+ : 0
+ const currentSessionRatio = task.progress.phaseTotal > 0
+ ? Math.max(0, Math.min(1, task.progress.phaseProgress / task.progress.phaseTotal))
+ : null
+ return (
+
+
+
{task.title}
+
+ {getTaskStatusLabel(task)}
+ {new Date(task.createdAt).toLocaleString('zh-CN')}
+
+ {task.status === 'running' && (
+ <>
+
+
0 ? (normalizedProgressCurrent / normalizedProgressTotal) * 100 : 0}%` }}
+ />
+
+
+ {normalizedProgressTotal > 0
+ ? `${Math.floor(normalizedProgressCurrent)} / ${normalizedProgressTotal}`
+ : '处理中'}
+ {task.status === 'running' && currentSessionRatio !== null
+ ? `(当前会话 ${Math.round(currentSessionRatio * 100)}%)`
+ : ''}
+ {task.progress.phaseLabel ? ` · ${task.progress.phaseLabel}` : ''}
+
+ >
+ )}
+ {canShowPerfDetail && stageTotals && (
+
+ 累计耗时 {formatDurationMs(stageTotalMs)}
+ {task.progress.total > 0 && (
+ 平均/会话 {formatDurationMs(Math.floor(stageTotalMs / Math.max(1, task.progress.total)))}
+ )}
+
+ )}
+ {canShowPerfDetail && isPerfExpanded && stageTotals && (
+
+
阶段耗时分布
+ {[
+ { key: 'collect' as const, label: '收集消息' },
+ { key: 'build' as const, label: '构建消息' },
+ { key: 'write' as const, label: '写入文件' },
+ { key: 'other' as const, label: '其他' }
+ ].map(item => {
+ const value = stageTotals[item.key]
+ const ratio = stageTotalMs > 0 ? Math.min(100, (value / stageTotalMs) * 100) : 0
+ return (
+
+
+ {item.label}
+ {formatDurationMs(value)}
+
+
+
+ )
+ })}
+
最慢会话 Top5
+ {topSessions.length === 0 ? (
+
暂无会话耗时数据
+ ) : (
+
+ {topSessions.map((session, index) => (
+
+
+ {index + 1}. {session.sessionName || session.sessionId}
+ {!session.finishedAt ? '(进行中)' : ''}
+
+ {formatDurationMs(session.liveElapsedMs)}
+
+ ))}
+
+ )}
+
+ )}
+ {task.status === 'error' &&
{task.error || '任务失败'}
}
+
+
+ {canShowPerfDetail && (
+
+ )}
+
+
+
+ )
+ })}
+
+ )}
+
+
+
+ )
+})
+
function ExportPage() {
const location = useLocation()
const isExportRoute = location.pathname === '/export'
@@ -2480,7 +2656,7 @@ function ExportPage() {
await configService.setExportDefaultConcurrency(options.exportConcurrency)
}
- const openSingleExport = (session: SessionRow) => {
+ const openSingleExport = useCallback((session: SessionRow) => {
if (!session.hasSession) return
openExportDialog({
scope: 'single',
@@ -2488,7 +2664,7 @@ function ExportPage() {
sessionNames: [session.displayName || session.username],
title: `导出会话:${session.displayName || session.username}`
})
- }
+ }, [openExportDialog])
const resolveSessionExistingMessageCount = useCallback((session: SessionRow): number => {
const counted = normalizeMessageCount(sessionMessageCounts[session.username])
@@ -3314,6 +3490,111 @@ function ExportPage() {
const taskRunningCount = tasks.filter(task => task.status === 'running').length
const taskQueuedCount = tasks.filter(task => task.status === 'queued').length
const showInitialSkeleton = isLoading && sessions.length === 0
+ const closeTaskCenter = useCallback(() => {
+ setIsTaskCenterOpen(false)
+ setExpandedPerfTaskId(null)
+ }, [])
+ const toggleTaskPerfDetail = useCallback((taskId: string) => {
+ setExpandedPerfTaskId(prev => (prev === taskId ? null : taskId))
+ }, [])
+ const contactsListRows = useMemo(() => (
+ filteredContacts.map((contact) => {
+ const matchedSession = sessionRowByUsername.get(contact.username)
+ const canExport = Boolean(matchedSession?.hasSession)
+ const isRunning = canExport && runningSessionIds.has(contact.username)
+ const isQueued = canExport && queuedSessionIds.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 (
+
+
+
+ {contact.avatarUrl ? (
+

+ ) : (
+
{getAvatarLetter(contact.displayName)}
+ )}
+
+
+
{contact.displayName}
+
{contact.username}
+
+
+
+
+ {messageCountLabel}
+
+
+
+
+
+
+
+
+
+ {recent &&
{recent}}
+
+
+
+ )
+ })
+ ), [
+ filteredContacts,
+ isLoadingSessionCounts,
+ lastExportBySession,
+ nowTick,
+ openSessionDetail,
+ openSingleExport,
+ queuedSessionIds,
+ runningSessionIds,
+ sessionDetail?.wxid,
+ sessionMessageCounts,
+ sessionRowByUsername,
+ showSessionDetailPanel
+ ])
const chooseExportFolder = useCallback(async () => {
const result = await window.electronAPI.dialog.openFile({
title: '选择导出目录',
@@ -3381,163 +3662,16 @@ function ExportPage() {
- {isTaskCenterOpen && (
- {
- setIsTaskCenterOpen(false)
- setExpandedPerfTaskId(null)
- }}
- >
-
event.stopPropagation()}
- >
-
-
-
任务中心
- 进行中 {taskRunningCount} · 排队 {taskQueuedCount} · 总计 {tasks.length}
-
-
-
-
- {tasks.length === 0 ? (
-
暂无任务。点击会话导出或卡片导出后会在这里创建任务。
- ) : (
-
- {tasks.map(task => {
- const canShowPerfDetail = isTextBatchTask(task) && Boolean(task.performance)
- const isPerfExpanded = expandedPerfTaskId === task.id
- const stageTotals = canShowPerfDetail
- ? getTaskPerformanceStageTotals(task.performance, nowTick)
- : null
- const stageTotalMs = stageTotals
- ? stageTotals.collect + stageTotals.build + stageTotals.write + stageTotals.other
- : 0
- const topSessions = isPerfExpanded
- ? getTaskPerformanceTopSessions(task.performance, nowTick, 5)
- : []
- const normalizedProgressTotal = task.progress.total > 0 ? task.progress.total : 0
- const normalizedProgressCurrent = normalizedProgressTotal > 0
- ? Math.max(0, Math.min(normalizedProgressTotal, task.progress.current))
- : 0
- const currentSessionRatio = task.progress.phaseTotal > 0
- ? Math.max(0, Math.min(1, task.progress.phaseProgress / task.progress.phaseTotal))
- : null
- return (
-
-
-
{task.title}
-
- {getTaskStatusLabel(task)}
- {new Date(task.createdAt).toLocaleString('zh-CN')}
-
- {task.status === 'running' && (
- <>
-
-
0 ? (normalizedProgressCurrent / normalizedProgressTotal) * 100 : 0}%` }}
- />
-
-
- {normalizedProgressTotal > 0
- ? `${Math.floor(normalizedProgressCurrent)} / ${normalizedProgressTotal}`
- : '处理中'}
- {task.status === 'running' && currentSessionRatio !== null
- ? `(当前会话 ${Math.round(currentSessionRatio * 100)}%)`
- : ''}
- {task.progress.phaseLabel ? ` · ${task.progress.phaseLabel}` : ''}
-
- >
- )}
- {canShowPerfDetail && stageTotals && (
-
- 累计耗时 {formatDurationMs(stageTotalMs)}
- {task.progress.total > 0 && (
- 平均/会话 {formatDurationMs(Math.floor(stageTotalMs / Math.max(1, task.progress.total)))}
- )}
-
- )}
- {canShowPerfDetail && isPerfExpanded && stageTotals && (
-
-
阶段耗时分布
- {[
- { key: 'collect' as const, label: '收集消息' },
- { key: 'build' as const, label: '构建消息' },
- { key: 'write' as const, label: '写入文件' },
- { key: 'other' as const, label: '其他' }
- ].map(item => {
- const value = stageTotals[item.key]
- const ratio = stageTotalMs > 0 ? Math.min(100, (value / stageTotalMs) * 100) : 0
- return (
-
-
- {item.label}
- {formatDurationMs(value)}
-
-
-
- )
- })}
-
最慢会话 Top5
- {topSessions.length === 0 ? (
-
暂无会话耗时数据
- ) : (
-
- {topSessions.map((session, index) => (
-
-
- {index + 1}. {session.sessionName || session.sessionId}
- {!session.finishedAt ? '(进行中)' : ''}
-
- {formatDurationMs(session.liveElapsedMs)}
-
- ))}
-
- )}
-
- )}
- {task.status === 'error' &&
{task.error || '任务失败'}
}
-
-
- {canShowPerfDetail && (
-
- )}
-
-
-
- )
- })}
-
- )}
-
-
-
- )}
+
{contentCards.map(card => {
@@ -3585,7 +3719,12 @@ function ExportPage() {
openContentExport(card.type)
}}
>
- {isCardRunning ? '批量导出中' : '批量导出'}
+ {isCardRunning ? (
+ <>
+ 批量导出中
+
+ >
+ ) : '批量导出'}
)
@@ -3714,89 +3853,7 @@ function ExportPage() {
操作
- {filteredContacts.map((contact) => {
- const matchedSession = sessionRowByUsername.get(contact.username)
- const canExport = Boolean(matchedSession?.hasSession)
- const isRunning = canExport && runningSessionIds.has(contact.username)
- const isQueued = canExport && queuedSessionIds.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 (
-
-
-
- {contact.avatarUrl ? (
-

- ) : (
-
{getAvatarLetter(contact.displayName)}
- )}
-
-
-
{contact.displayName}
-
{contact.username}
-
-
-
-
- {messageCountLabel}
-
-
-
-
-
-
-
-
-
- {recent &&
{recent}}
-
-
-
- )
- })}
+ {contactsListRows}
>
)}