feat(export): add text batch task performance breakdown

This commit is contained in:
tisonhuang
2026-03-02 17:14:57 +08:00
parent 7bd801cd01
commit 750d6ad7eb
4 changed files with 458 additions and 81 deletions

View File

@@ -276,7 +276,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.invoke('export:pauseTask', taskId), ipcRenderer.invoke('export:pauseTask', taskId),
stopTask: (taskId: string) => stopTask: (taskId: string) =>
ipcRenderer.invoke('export:stopTask', taskId), ipcRenderer.invoke('export:stopTask', taskId),
onProgress: (callback: (payload: { current: number; total: number; currentSession: string; phase: string }) => void) => { onProgress: (callback: (payload: { current: number; total: number; currentSession: string; currentSessionId?: string; phase: string }) => void) => {
ipcRenderer.on('export:progress', (_, payload) => callback(payload)) ipcRenderer.on('export:progress', (_, payload) => callback(payload))
return () => ipcRenderer.removeAllListeners('export:progress') return () => ipcRenderer.removeAllListeners('export:progress')
} }

View File

@@ -108,6 +108,7 @@ export interface ExportProgress {
current: number current: number
total: number total: number
currentSession: string currentSession: string
currentSessionId?: string
phase: 'preparing' | 'exporting' | 'exporting-media' | 'exporting-voice' | 'writing' | 'complete' phase: 'preparing' | 'exporting' | 'exporting-media' | 'exporting-voice' | 'writing' | 'complete'
phaseProgress?: number phaseProgress?: number
phaseTotal?: number phaseTotal?: number
@@ -5299,7 +5300,8 @@ class ExportService {
...progress, ...progress,
current: completedCount, current: completedCount,
total: sessionIds.length, total: sessionIds.length,
currentSession: sessionInfo.displayName currentSession: sessionInfo.displayName,
currentSessionId: sessionId
}) })
} }
@@ -5411,6 +5413,7 @@ class ExportService {
current: sessionIds.length, current: sessionIds.length,
total: sessionIds.length, total: sessionIds.length,
currentSession: '', currentSession: '',
currentSessionId: '',
phase: 'complete' phase: 'complete'
}) })

View File

@@ -557,6 +557,88 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
.task-perf-summary {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 11px;
color: var(--text-secondary);
}
.task-perf-panel {
margin-top: 8px;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px;
background: var(--bg-secondary);
display: grid;
gap: 8px;
}
.task-perf-title {
font-size: 12px;
color: var(--text-primary);
font-weight: 600;
}
.task-perf-row {
display: grid;
gap: 4px;
}
.task-perf-row-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--text-secondary);
}
.task-perf-row-track {
height: 6px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.task-perf-row-fill {
height: 100%;
background: var(--primary);
}
.task-perf-empty {
font-size: 11px;
color: var(--text-secondary);
}
.task-perf-session-list {
display: grid;
gap: 4px;
}
.task-perf-session-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
font-size: 11px;
color: var(--text-secondary);
}
.task-perf-session-rank {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-perf-session-time {
font-variant-numeric: tabular-nums;
}
.task-error { .task-error {
margin-top: 6px; margin-top: 6px;
font-size: 12px; font-size: 12px;

View File

@@ -79,11 +79,29 @@ interface TaskProgress {
current: number current: number
total: number total: number
currentName: string currentName: string
phase: ExportProgress['phase'] | ''
phaseLabel: string phaseLabel: string
phaseProgress: number phaseProgress: number
phaseTotal: number phaseTotal: number
} }
type TaskPerfStage = 'collect' | 'build' | 'write' | 'other'
interface TaskSessionPerformance {
sessionId: string
sessionName: string
startedAt: number
finishedAt?: number
elapsedMs: number
lastPhase?: ExportProgress['phase']
lastPhaseStartedAt?: number
}
interface TaskPerformance {
stages: Record<TaskPerfStage, number>
sessions: Record<string, TaskSessionPerformance>
}
interface ExportTaskPayload { interface ExportTaskPayload {
sessionIds: string[] sessionIds: string[]
outputDir: string outputDir: string
@@ -110,6 +128,7 @@ interface ExportTask {
error?: string error?: string
payload: ExportTaskPayload payload: ExportTaskPayload
progress: TaskProgress progress: TaskProgress
performance?: TaskPerformance
} }
interface ExportDialogState { interface ExportDialogState {
@@ -170,11 +189,165 @@ const createEmptyProgress = (): TaskProgress => ({
current: 0, current: 0,
total: 0, total: 0,
currentName: '', currentName: '',
phase: '',
phaseLabel: '', phaseLabel: '',
phaseProgress: 0, phaseProgress: 0,
phaseTotal: 0 phaseTotal: 0
}) })
const createEmptyTaskPerformance = (): TaskPerformance => ({
stages: {
collect: 0,
build: 0,
write: 0,
other: 0
},
sessions: {}
})
const isTextBatchTask = (task: ExportTask): boolean => (
task.payload.scope === 'content' && task.payload.contentType === 'text'
)
const resolvePerfStageByPhase = (phase?: ExportProgress['phase']): TaskPerfStage => {
if (phase === 'preparing') return 'collect'
if (phase === 'writing') return 'write'
if (phase === 'exporting' || phase === 'exporting-media' || phase === 'exporting-voice') return 'build'
return 'other'
}
const cloneTaskPerformance = (performance?: TaskPerformance): TaskPerformance => ({
stages: {
collect: performance?.stages.collect || 0,
build: performance?.stages.build || 0,
write: performance?.stages.write || 0,
other: performance?.stages.other || 0
},
sessions: Object.fromEntries(
Object.entries(performance?.sessions || {}).map(([sessionId, session]) => [sessionId, { ...session }])
)
})
const resolveTaskSessionName = (task: ExportTask, sessionId: string, fallback?: string): string => {
const idx = task.payload.sessionIds.indexOf(sessionId)
if (idx >= 0) {
return task.payload.sessionNames[idx] || fallback || sessionId
}
return fallback || sessionId
}
const applyProgressToTaskPerformance = (
task: ExportTask,
payload: ExportProgress,
now: number
): TaskPerformance | undefined => {
if (!isTextBatchTask(task)) return task.performance
const sessionId = String(payload.currentSessionId || '').trim()
if (!sessionId) return task.performance || createEmptyTaskPerformance()
const performance = cloneTaskPerformance(task.performance)
const sessionName = resolveTaskSessionName(task, sessionId, payload.currentSession || sessionId)
const existing = performance.sessions[sessionId]
const session: TaskSessionPerformance = existing
? { ...existing, sessionName: existing.sessionName || sessionName }
: {
sessionId,
sessionName,
startedAt: now,
elapsedMs: 0
}
if (!session.finishedAt && session.lastPhase && typeof session.lastPhaseStartedAt === 'number') {
const delta = Math.max(0, now - session.lastPhaseStartedAt)
performance.stages[resolvePerfStageByPhase(session.lastPhase)] += delta
}
session.elapsedMs = Math.max(session.elapsedMs, now - session.startedAt)
if (payload.phase === 'complete') {
session.finishedAt = now
session.lastPhase = undefined
session.lastPhaseStartedAt = undefined
} else {
session.lastPhase = payload.phase
session.lastPhaseStartedAt = now
}
performance.sessions[sessionId] = session
return performance
}
const finalizeTaskPerformance = (task: ExportTask, now: number): TaskPerformance | undefined => {
if (!isTextBatchTask(task) || !task.performance) return task.performance
const performance = cloneTaskPerformance(task.performance)
for (const session of Object.values(performance.sessions)) {
if (session.finishedAt) continue
if (session.lastPhase && typeof session.lastPhaseStartedAt === 'number') {
const delta = Math.max(0, now - session.lastPhaseStartedAt)
performance.stages[resolvePerfStageByPhase(session.lastPhase)] += delta
}
session.elapsedMs = Math.max(session.elapsedMs, now - session.startedAt)
session.finishedAt = now
session.lastPhase = undefined
session.lastPhaseStartedAt = undefined
}
return performance
}
const getTaskPerformanceStageTotals = (
performance: TaskPerformance | undefined,
now: number
): Record<TaskPerfStage, number> => {
const totals: Record<TaskPerfStage, number> = {
collect: performance?.stages.collect || 0,
build: performance?.stages.build || 0,
write: performance?.stages.write || 0,
other: performance?.stages.other || 0
}
if (!performance) return totals
for (const session of Object.values(performance.sessions)) {
if (session.finishedAt) continue
if (!session.lastPhase || typeof session.lastPhaseStartedAt !== 'number') continue
const delta = Math.max(0, now - session.lastPhaseStartedAt)
totals[resolvePerfStageByPhase(session.lastPhase)] += delta
}
return totals
}
const getTaskPerformanceTopSessions = (
performance: TaskPerformance | undefined,
now: number,
limit = 5
): Array<TaskSessionPerformance & { liveElapsedMs: number }> => {
if (!performance) return []
return Object.values(performance.sessions)
.map((session) => {
const liveElapsedMs = session.finishedAt
? session.elapsedMs
: Math.max(session.elapsedMs, now - session.startedAt)
return {
...session,
liveElapsedMs
}
})
.sort((a, b) => b.liveElapsedMs - a.liveElapsedMs)
.slice(0, limit)
}
const formatDurationMs = (ms: number): string => {
const totalSeconds = Math.max(0, Math.floor(ms / 1000))
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
if (hours > 0) {
return `${hours}小时${minutes}${seconds}`
}
if (minutes > 0) {
return `${minutes}${seconds}`
}
return `${seconds}`
}
const getTaskStatusLabel = (task: ExportTask): string => { const getTaskStatusLabel = (task: ExportTask): string => {
if (task.status === 'queued') return '排队中' if (task.status === 'queued') return '排队中'
if (task.status === 'running') { if (task.status === 'running') {
@@ -570,6 +743,7 @@ function ExportPage() {
const [isSnsStatsLoading, setIsSnsStatsLoading] = useState(true) const [isSnsStatsLoading, setIsSnsStatsLoading] = useState(true)
const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true) const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true)
const [isTaskCenterOpen, setIsTaskCenterOpen] = useState(false) const [isTaskCenterOpen, setIsTaskCenterOpen] = useState(false)
const [expandedPerfTaskId, setExpandedPerfTaskId] = useState<string | null>(null)
const [sessions, setSessions] = useState<SessionRow[]>([]) const [sessions, setSessions] = useState<SessionRow[]>([])
const [sessionDataSource, setSessionDataSource] = useState<SessionDataSource>(null) const [sessionDataSource, setSessionDataSource] = useState<SessionDataSource>(null)
const [sessionContactsUpdatedAt, setSessionContactsUpdatedAt] = useState<number | null>(null) const [sessionContactsUpdatedAt, setSessionContactsUpdatedAt] = useState<number | null>(null)
@@ -1022,6 +1196,14 @@ function ExportPage() {
tasksRef.current = tasks tasksRef.current = tasks
}, [tasks]) }, [tasks])
useEffect(() => {
if (!expandedPerfTaskId) return
const target = tasks.find(task => task.id === expandedPerfTaskId)
if (!target || !isTextBatchTask(target)) {
setExpandedPerfTaskId(null)
}
}, [tasks, expandedPerfTaskId])
useEffect(() => { useEffect(() => {
hasSeededSnsStatsRef.current = hasSeededSnsStats hasSeededSnsStatsRef.current = hasSeededSnsStats
}, [hasSeededSnsStats]) }, [hasSeededSnsStats])
@@ -1044,6 +1226,14 @@ function ExportPage() {
return () => clearInterval(timer) return () => clearInterval(timer)
}, [isExportRoute]) }, [isExportRoute])
useEffect(() => {
if (!isTaskCenterOpen || !expandedPerfTaskId) return
const target = tasks.find(task => task.id === expandedPerfTaskId)
if (!target || target.status !== 'running' || !isTextBatchTask(target)) return
const timer = window.setInterval(() => setNowTick(Date.now()), 1000)
return () => window.clearInterval(timer)
}, [isTaskCenterOpen, expandedPerfTaskId, tasks])
const loadBaseConfig = useCallback(async (): Promise<boolean> => { const loadBaseConfig = useCallback(async (): Promise<boolean> => {
setIsBaseConfigLoading(true) setIsBaseConfigLoading(true)
let isReady = true let isReady = true
@@ -1718,7 +1908,10 @@ function ExportPage() {
controlState: undefined, controlState: undefined,
startedAt: Date.now(), startedAt: Date.now(),
finishedAt: undefined, finishedAt: undefined,
error: undefined error: undefined,
performance: isTextBatchTask(task)
? (task.performance || createEmptyTaskPerformance())
: task.performance
})) }))
progressUnsubscribeRef.current?.() progressUnsubscribeRef.current?.()
@@ -1732,6 +1925,7 @@ function ExportPage() {
current: payload.current || 0, current: payload.current || 0,
total: payload.total || 0, total: payload.total || 0,
currentName: '', currentName: '',
phase: 'exporting',
phaseLabel: payload.status || '', phaseLabel: payload.status || '',
phaseProgress: payload.total > 0 ? payload.current : 0, phaseProgress: payload.total > 0 ? payload.current : 0,
phaseTotal: payload.total || 0 phaseTotal: payload.total || 0
@@ -1743,16 +1937,20 @@ function ExportPage() {
progressUnsubscribeRef.current = window.electronAPI.export.onProgress((payload: ExportProgress) => { progressUnsubscribeRef.current = window.electronAPI.export.onProgress((payload: ExportProgress) => {
updateTask(next.id, task => { updateTask(next.id, task => {
if (task.status !== 'running') return task if (task.status !== 'running') return task
const now = Date.now()
const performance = applyProgressToTaskPerformance(task, payload, now)
return { return {
...task, ...task,
progress: { progress: {
current: payload.current, current: payload.current,
total: payload.total, total: payload.total,
currentName: payload.currentSession, currentName: payload.currentSession,
phase: payload.phase,
phaseLabel: payload.phaseLabel || '', phaseLabel: payload.phaseLabel || '',
phaseProgress: payload.phaseProgress || 0, phaseProgress: payload.phaseProgress || 0,
phaseTotal: payload.phaseTotal || 0 phaseTotal: payload.phaseTotal || 0
} },
performance
} }
}) })
}) })
@@ -1776,7 +1974,8 @@ function ExportPage() {
status: 'error', status: 'error',
controlState: undefined, controlState: undefined,
finishedAt: Date.now(), finishedAt: Date.now(),
error: result.error || '朋友圈导出失败' error: result.error || '朋友圈导出失败',
performance: finalizeTaskPerformance(task, Date.now())
})) }))
} else if (result.stopped) { } else if (result.stopped) {
updateTask(next.id, task => ({ updateTask(next.id, task => ({
@@ -1787,7 +1986,8 @@ function ExportPage() {
progress: { progress: {
...task.progress, ...task.progress,
phaseLabel: '已停止' phaseLabel: '已停止'
} },
performance: finalizeTaskPerformance(task, Date.now())
})) }))
} else if (result.paused) { } else if (result.paused) {
updateTask(next.id, task => ({ updateTask(next.id, task => ({
@@ -1798,7 +1998,8 @@ function ExportPage() {
progress: { progress: {
...task.progress, ...task.progress,
phaseLabel: '已暂停' phaseLabel: '已暂停'
} },
performance: finalizeTaskPerformance(task, Date.now())
})) }))
} else { } else {
const doneAt = Date.now() const doneAt = Date.now()
@@ -1820,7 +2021,8 @@ function ExportPage() {
phaseLabel: '完成', phaseLabel: '完成',
phaseProgress: 1, phaseProgress: 1,
phaseTotal: 1 phaseTotal: 1
} },
performance: finalizeTaskPerformance(task, doneAt)
})) }))
} }
} else { } else {
@@ -1841,7 +2043,8 @@ function ExportPage() {
status: 'error', status: 'error',
controlState: undefined, controlState: undefined,
finishedAt: Date.now(), finishedAt: Date.now(),
error: result.error || '导出失败' error: result.error || '导出失败',
performance: finalizeTaskPerformance(task, Date.now())
})) }))
} else { } else {
const doneAt = Date.now() const doneAt = Date.now()
@@ -1867,7 +2070,8 @@ function ExportPage() {
current: result.successCount + result.failCount, current: result.successCount + result.failCount,
total: task.progress.total || next.payload.sessionIds.length, total: task.progress.total || next.payload.sessionIds.length,
phaseLabel: '已停止' phaseLabel: '已停止'
} },
performance: finalizeTaskPerformance(task, doneAt)
})) }))
} else if (result.paused) { } else if (result.paused) {
const pendingSessionIds = Array.isArray(result.pendingSessionIds) const pendingSessionIds = Array.isArray(result.pendingSessionIds)
@@ -1892,7 +2096,8 @@ function ExportPage() {
phaseLabel: '完成', phaseLabel: '完成',
phaseProgress: 1, phaseProgress: 1,
phaseTotal: 1 phaseTotal: 1
} },
performance: finalizeTaskPerformance(task, doneAt)
})) }))
} else { } else {
updateTask(next.id, task => ({ updateTask(next.id, task => ({
@@ -1910,7 +2115,8 @@ function ExportPage() {
current: result.successCount + result.failCount, current: result.successCount + result.failCount,
total: task.progress.total || next.payload.sessionIds.length, total: task.progress.total || next.payload.sessionIds.length,
phaseLabel: '已暂停' phaseLabel: '已暂停'
} },
performance: finalizeTaskPerformance(task, doneAt)
})) }))
} }
} else { } else {
@@ -1926,18 +2132,21 @@ function ExportPage() {
phaseLabel: '完成', phaseLabel: '完成',
phaseProgress: 1, phaseProgress: 1,
phaseTotal: 1 phaseTotal: 1
} },
performance: finalizeTaskPerformance(task, doneAt)
})) }))
} }
} }
} }
} catch (error) { } catch (error) {
const doneAt = Date.now()
updateTask(next.id, task => ({ updateTask(next.id, task => ({
...task, ...task,
status: 'error', status: 'error',
controlState: undefined, controlState: undefined,
finishedAt: Date.now(), finishedAt: doneAt,
error: String(error) error: String(error),
performance: finalizeTaskPerformance(task, doneAt)
})) }))
} finally { } finally {
progressUnsubscribeRef.current?.() progressUnsubscribeRef.current?.()
@@ -1966,7 +2175,8 @@ function ExportPage() {
updateTask(taskId, task => ({ updateTask(taskId, task => ({
...task, ...task,
status: 'paused', status: 'paused',
controlState: undefined controlState: undefined,
performance: finalizeTaskPerformance(task, Date.now())
})) }))
return return
} }
@@ -2008,15 +2218,17 @@ function ExportPage() {
if (!shouldStop) return if (!shouldStop) return
if (target.status === 'queued' || target.status === 'paused') { if (target.status === 'queued' || target.status === 'paused') {
const doneAt = Date.now()
updateTask(taskId, task => ({ updateTask(taskId, task => ({
...task, ...task,
status: 'stopped', status: 'stopped',
controlState: undefined, controlState: undefined,
finishedAt: Date.now(), finishedAt: doneAt,
progress: { progress: {
...task.progress, ...task.progress,
phaseLabel: '已停止' phaseLabel: '已停止'
} },
performance: finalizeTaskPerformance(task, doneAt)
})) }))
return return
} }
@@ -2073,7 +2285,10 @@ function ExportPage() {
contentType: exportDialog.contentType, contentType: exportDialog.contentType,
snsOptions snsOptions
}, },
progress: createEmptyProgress() progress: createEmptyProgress(),
performance: exportDialog.scope === 'content' && exportDialog.contentType === 'text'
? createEmptyTaskPerformance()
: undefined
} }
setTasks(prev => [task, ...prev]) setTasks(prev => [task, ...prev])
@@ -2798,7 +3013,10 @@ function ExportPage() {
{isTaskCenterOpen && ( {isTaskCenterOpen && (
<div <div
className="task-center-modal-overlay" className="task-center-modal-overlay"
onClick={() => setIsTaskCenterOpen(false)} onClick={() => {
setIsTaskCenterOpen(false)
setExpandedPerfTaskId(null)
}}
> >
<div <div
className="task-center-modal" className="task-center-modal"
@@ -2815,7 +3033,10 @@ function ExportPage() {
<button <button
className="close-icon-btn" className="close-icon-btn"
type="button" type="button"
onClick={() => setIsTaskCenterOpen(false)} onClick={() => {
setIsTaskCenterOpen(false)
setExpandedPerfTaskId(null)
}}
aria-label="关闭任务中心" aria-label="关闭任务中心"
> >
<X size={16} /> <X size={16} />
@@ -2826,7 +3047,19 @@ function ExportPage() {
<div className="task-empty"></div> <div className="task-empty"></div>
) : ( ) : (
<div className="task-list"> <div className="task-list">
{tasks.map(task => ( {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)
: []
return (
<div key={task.id} className={`task-card ${task.status} ${task.controlState ? `request-${task.controlState}` : ''}`}> <div key={task.id} className={`task-card ${task.status} ${task.controlState ? `request-${task.controlState}` : ''}`}>
<div className="task-main"> <div className="task-main">
<div className="task-title">{task.title}</div> <div className="task-title">{task.title}</div>
@@ -2850,9 +3083,67 @@ function ExportPage() {
</div> </div>
</> </>
)} )}
{canShowPerfDetail && stageTotals && (
<div className="task-perf-summary">
<span> {formatDurationMs(stageTotalMs)}</span>
{task.progress.total > 0 && (
<span>/ {formatDurationMs(Math.floor(stageTotalMs / Math.max(1, task.progress.total)))}</span>
)}
</div>
)}
{canShowPerfDetail && isPerfExpanded && stageTotals && (
<div className="task-perf-panel">
<div className="task-perf-title"></div>
{[
{ 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 (
<div className="task-perf-row" key={item.key}>
<div className="task-perf-row-head">
<span>{item.label}</span>
<span>{formatDurationMs(value)}</span>
</div>
<div className="task-perf-row-track">
<div className="task-perf-row-fill" style={{ width: `${ratio}%` }} />
</div>
</div>
)
})}
<div className="task-perf-title"> Top5</div>
{topSessions.length === 0 ? (
<div className="task-perf-empty"></div>
) : (
<div className="task-perf-session-list">
{topSessions.map((session, index) => (
<div className="task-perf-session-item" key={session.sessionId}>
<span className="task-perf-session-rank">
{index + 1}. {session.sessionName || session.sessionId}
{!session.finishedAt ? '(进行中)' : ''}
</span>
<span className="task-perf-session-time">{formatDurationMs(session.liveElapsedMs)}</span>
</div>
))}
</div>
)}
</div>
)}
{task.status === 'error' && <div className="task-error">{task.error || '任务失败'}</div>} {task.status === 'error' && <div className="task-error">{task.error || '任务失败'}</div>}
</div> </div>
<div className="task-actions"> <div className="task-actions">
{canShowPerfDetail && (
<button
className={`task-action-btn ${isPerfExpanded ? 'primary' : ''}`}
type="button"
onClick={() => setExpandedPerfTaskId(prev => (prev === task.id ? null : task.id))}
>
{isPerfExpanded ? '收起详情' : '性能详情'}
</button>
)}
{(task.status === 'running' || task.status === 'queued') && ( {(task.status === 'running' || task.status === 'queued') && (
<button <button
className="task-action-btn" className="task-action-btn"
@@ -2887,7 +3178,8 @@ function ExportPage() {
</button> </button>
</div> </div>
</div> </div>
))} )
})}
</div> </div>
)} )}
</div> </div>