From 81741a67e769735d369b0ecd457ccdbffed02ab5 Mon Sep 17 00:00:00 2001 From: sinohqb Date: Thu, 20 Aug 2026 19:19:09 +0800 Subject: [PATCH] feat(ui): replace Steps and Column chart with custom timeline and cards in ExecutionProcess --- .../ExecutionProcess.test.tsx | 9 +- .../intelligent_eval/ExecutionProcess.tsx | 184 ++++++++++++++---- 2 files changed, 148 insertions(+), 45 deletions(-) diff --git a/frontend/web/src/components/intelligent_eval/ExecutionProcess.test.tsx b/frontend/web/src/components/intelligent_eval/ExecutionProcess.test.tsx index 9fb866a..0d46d88 100644 --- a/frontend/web/src/components/intelligent_eval/ExecutionProcess.test.tsx +++ b/frontend/web/src/components/intelligent_eval/ExecutionProcess.test.tsx @@ -3,10 +3,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { ExecutionProgress, IntelligentEval } from '../../api' import ExecutionProcess from './ExecutionProcess' -vi.mock('@ant-design/charts', () => ({ - Column: () =>
, -})) - const getExecutionProgress = vi.fn() const listDecisionLogs = vi.fn() const listTasks = vi.fn() @@ -93,11 +89,12 @@ afterEach(() => { }) describe('ExecutionProcess', () => { - it('renders lifecycle steps, next action and slot chart', async () => { + it('renders lifecycle timeline, next action and time distribution cards', async () => { mockApi() render() - await waitFor(() => expect(screen.getByTestId('slot-chart')).toBeInTheDocument()) + await waitFor(() => expect(screen.getByText('0-1h')).toBeInTheDocument()) + expect(screen.getByText('1-2h')).toBeInTheDocument() expect(screen.getByText('规划')).toBeInTheDocument() expect(screen.getByText('审批')).toBeInTheDocument() expect(screen.getByText(/下一步:等待平台触发 worker 补足当前欠账 2 个会话/)).toBeInTheDocument() diff --git a/frontend/web/src/components/intelligent_eval/ExecutionProcess.tsx b/frontend/web/src/components/intelligent_eval/ExecutionProcess.tsx index 8e5a778..586662b 100644 --- a/frontend/web/src/components/intelligent_eval/ExecutionProcess.tsx +++ b/frontend/web/src/components/intelligent_eval/ExecutionProcess.tsx @@ -1,6 +1,12 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { Alert, Card, Empty, Spin, Steps, Tag, Timeline } from 'antd' -import { Column } from '@ant-design/charts' +import { Alert, Card, Empty, Spin, Tag, Timeline } from 'antd' +import { + CheckCircleOutlined, + ClockCircleOutlined, + EditOutlined, + FileTextOutlined, + PlayCircleOutlined, +} from '@ant-design/icons' import { intelligentEvalsApi, type DecisionLog, @@ -15,11 +21,11 @@ import { usePolling } from '../../hooks/usePolling' const ACTIVE_STATUSES = new Set(['planning', 'pending_approval', 'executing']) -const STAGE_STEPS = [ - { key: 'planning', title: '规划' }, - { key: 'approval', title: '审批' }, - { key: 'executing', title: '执行' }, - { key: 'done', title: '完成' }, +const STAGE_CONFIG = [ + { key: 'planning', title: '规划', icon: EditOutlined }, + { key: 'approval', title: '审批', icon: FileTextOutlined }, + { key: 'executing', title: '执行', icon: PlayCircleOutlined }, + { key: 'done', title: '完成', icon: CheckCircleOutlined }, ] const DECISION_TYPE_LABELS: Record = { @@ -91,6 +97,136 @@ function decisionEvents(logs: DecisionLog[]): ActivityEvent[] { })) } +function LifecycleTimeline({ currentStage, abnormal }: { currentStage: string; abnormal: boolean }) { + const currentIndex = STAGE_CONFIG.findIndex((s) => s.key === currentStage) + + return ( +
+ {STAGE_CONFIG.map((stage, idx) => { + const Icon = stage.icon + const isCompleted = idx < currentIndex + const isCurrent = idx === currentIndex + const isError = isCurrent && abnormal + + const bgColor = isError + ? statusColors.failed + : isCompleted + ? statusColors.completed + : isCurrent + ? colors.primary + : colors.bgSubtle + + const textColor = isCompleted || isCurrent ? '#fff' : colors.textSecondary + + return ( +
+
+
+ {isCompleted ? : } +
+
+ {stage.title} + {isCurrent &&
当前阶段
} +
+
+ {idx < STAGE_CONFIG.length - 1 && ( +
+ )} +
+ ) + })} +
+ ) +} + +function TimeDistributionCards({ slots }: { slots: ExecutionProgress['slots'] }) { + return ( +
+ {slots.map((slot) => { + const plannedPct = slot.planned > 0 ? 100 : 0 + const createdPct = slot.planned > 0 ? (slot.created / slot.planned) * 100 : 0 + const completedPct = slot.planned > 0 ? (slot.completed / slot.planned) * 100 : 0 + + return ( + +
+
{slot.time_slot}
+ {slot.is_current && ( + + + 当前 + + )} +
+ +
+
+
+ 计划 + {slot.planned} +
+
+
+
+
+ +
+
+ 已创建 + {slot.created}/{slot.planned} +
+
+
+
+
+ +
+
+ 已完成 + {slot.completed}/{slot.planned} +
+
+
+
+
+
+ + ) + })} +
+ ) +} + export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) { const [progress, setProgress] = useState(null) const [logs, setLogs] = useState([]) @@ -132,42 +268,12 @@ export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) { return } - const stageIndex = STAGE_STEPS.findIndex((s) => s.key === progress.current_stage) const abnormal = progress.abnormal_outcome != null - const slotData = progress.slots.flatMap((s) => { - const label = s.is_current ? `${s.time_slot}(当前)` : s.time_slot - return [ - { slot: label, kind: '计划', value: s.planned }, - { slot: label, kind: '已创建', value: s.created }, - { slot: label, kind: '已完成', value: s.completed }, - ] - }) - const slotConfig = { - data: slotData, - xField: 'slot', - yField: 'value', - seriesField: 'kind', - group: true, - height: Math.max(200, progress.slots.length * 72), - color: [colors.textMuted, colors.primary, statusColors.completed], - legend: { position: 'top' }, - xAxis: { label: { autoRotate: true } }, - yAxis: { min: 0 }, - } - return (
- ({ - title: s.title, - description: s.key === progress.current_stage ? '当前阶段' : undefined, - }))} - /> + {progress.blocker != null && ( ) : ( - + )}