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}
)}