feat(ui): add timestamps to lifecycle timeline and replace time distribution cards with table
This commit is contained in:
parent
d61657eedb
commit
ff734c72e5
@ -97,11 +97,19 @@ function decisionEvents(logs: DecisionLog[]): ActivityEvent[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function LifecycleTimeline({ currentStage, abnormal }: { currentStage: string; abnormal: boolean }) {
|
function LifecycleTimeline({ ev, currentStage, abnormal }: { ev: IntelligentEval; currentStage: string; abnormal: boolean }) {
|
||||||
const currentIndex = STAGE_CONFIG.findIndex((s) => s.key === currentStage)
|
const currentIndex = STAGE_CONFIG.findIndex((s) => s.key === currentStage)
|
||||||
|
|
||||||
|
// Map stages to timestamps
|
||||||
|
const stageTimes: Record<string, string | null> = {
|
||||||
|
planning: ev.created_at,
|
||||||
|
approval: ev.started_at, // Approval completed when execution started
|
||||||
|
executing: ev.started_at,
|
||||||
|
done: ev.completed_at,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 0' }}>
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, padding: '12px 0' }}>
|
||||||
{STAGE_CONFIG.map((stage, idx) => {
|
{STAGE_CONFIG.map((stage, idx) => {
|
||||||
const Icon = stage.icon
|
const Icon = stage.icon
|
||||||
const isCompleted = idx < currentIndex
|
const isCompleted = idx < currentIndex
|
||||||
@ -117,10 +125,11 @@ function LifecycleTimeline({ currentStage, abnormal }: { currentStage: string; a
|
|||||||
: colors.bgSubtle
|
: colors.bgSubtle
|
||||||
|
|
||||||
const textColor = isCompleted || isCurrent ? '#fff' : colors.textSecondary
|
const textColor = isCompleted || isCurrent ? '#fff' : colors.textSecondary
|
||||||
|
const time = stageTimes[stage.key]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={stage.key} style={{ display: 'flex', alignItems: 'center', flex: idx < STAGE_CONFIG.length - 1 ? 1 : 'none' }}>
|
<div key={stage.key} style={{ display: 'flex', alignItems: 'flex-start', flex: idx < STAGE_CONFIG.length - 1 ? 1 : 'none' }}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, minWidth: 80 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 40,
|
width: 40,
|
||||||
@ -138,10 +147,15 @@ function LifecycleTimeline({ currentStage, abnormal }: { currentStage: string; a
|
|||||||
>
|
>
|
||||||
{isCompleted ? <CheckCircleOutlined /> : <Icon />}
|
{isCompleted ? <CheckCircleOutlined /> : <Icon />}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: isCurrent ? colors.primary : colors.textSecondary, fontWeight: isCurrent ? 600 : 400 }}>
|
<div style={{ fontSize: 12, color: isCurrent ? colors.primary : colors.textSecondary, fontWeight: isCurrent ? 600 : 400, textAlign: 'center' }}>
|
||||||
{stage.title}
|
{stage.title}
|
||||||
{isCurrent && <div style={{ fontSize: 11, color: colors.textMuted, marginTop: 2 }}>当前阶段</div>}
|
{isCurrent && <div style={{ fontSize: 11, color: colors.textMuted, marginTop: 2 }}>当前阶段</div>}
|
||||||
</div>
|
</div>
|
||||||
|
{time && (
|
||||||
|
<div style={{ fontSize: 11, color: colors.textMuted, textAlign: 'center' }}>
|
||||||
|
{formatDateTime(time).split(' ')[1] || formatDateTime(time)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{idx < STAGE_CONFIG.length - 1 && (
|
{idx < STAGE_CONFIG.length - 1 && (
|
||||||
<div
|
<div
|
||||||
@ -149,8 +163,7 @@ function LifecycleTimeline({ currentStage, abnormal }: { currentStage: string; a
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
height: 2,
|
height: 2,
|
||||||
background: isCompleted ? statusColors.completed : colors.border,
|
background: isCompleted ? statusColors.completed : colors.border,
|
||||||
margin: '0 8px',
|
margin: '20px 8px 0',
|
||||||
marginBottom: 28,
|
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -162,67 +175,82 @@ function LifecycleTimeline({ currentStage, abnormal }: { currentStage: string; a
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimeDistributionCards({ slots }: { slots: ExecutionProgress['slots'] }) {
|
function TimeDistributionTable({ slots }: { slots: ExecutionProgress['slots'] }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 12 }}>
|
<div style={{ border: `1px solid ${colors.border}`, borderRadius: 8, overflow: 'hidden' }}>
|
||||||
{slots.map((slot) => {
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||||
const plannedPct = slot.planned > 0 ? 100 : 0
|
<thead>
|
||||||
const createdPct = slot.planned > 0 ? (slot.created / slot.planned) * 100 : 0
|
<tr style={{ background: colors.bgSubtle, borderBottom: `1px solid ${colors.border}` }}>
|
||||||
const completedPct = slot.planned > 0 ? (slot.completed / slot.planned) * 100 : 0
|
<th style={{ padding: '10px 12px', textAlign: 'left', fontWeight: 500, fontSize: 12, color: colors.textSecondary }}>时段</th>
|
||||||
|
<th style={{ padding: '10px 12px', textAlign: 'center', fontWeight: 500, fontSize: 12, color: colors.textSecondary }}>计划</th>
|
||||||
|
<th style={{ padding: '10px 12px', textAlign: 'center', fontWeight: 500, fontSize: 12, color: colors.textSecondary }}>已创建</th>
|
||||||
|
<th style={{ padding: '10px 12px', textAlign: 'center', fontWeight: 500, fontSize: 12, color: colors.textSecondary }}>已完成</th>
|
||||||
|
<th style={{ padding: '10px 12px', textAlign: 'left', fontWeight: 500, fontSize: 12, color: colors.textSecondary, minWidth: 120 }}>进度</th>
|
||||||
|
<th style={{ padding: '10px 12px', textAlign: 'center', fontWeight: 500, fontSize: 12, color: colors.textSecondary }}>状态</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{slots.map((slot) => {
|
||||||
|
const pct = slot.planned > 0 ? Math.round((slot.completed / slot.planned) * 100) : 0
|
||||||
|
const isComplete = slot.completed >= slot.planned && slot.planned > 0
|
||||||
|
const isPast = slot.is_past
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<tr
|
||||||
key={slot.time_slot}
|
key={slot.time_slot}
|
||||||
size="small"
|
style={{
|
||||||
style={{
|
borderBottom: `1px solid ${colors.border}`,
|
||||||
border: slot.is_current ? `2px solid ${colors.primary}` : `1px solid ${colors.border}`,
|
background: slot.is_current ? `${colors.primary}08` : 'transparent',
|
||||||
background: slot.is_current ? `${colors.primary}08` : colors.bgContainer,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<td style={{ padding: '12px', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
<span style={{ fontWeight: 500 }}>{slot.time_slot}</span>
|
||||||
<div style={{ fontSize: 13, fontWeight: 600 }}>{slot.time_slot}</div>
|
{slot.is_current && (
|
||||||
{slot.is_current && (
|
<Tag color={colors.primary} style={{ fontSize: 11, margin: 0, padding: '0 6px' }}>
|
||||||
<Tag color={colors.primary} style={{ fontSize: 11, margin: 0 }}>
|
<ClockCircleOutlined style={{ marginRight: 2 }} />
|
||||||
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
当前
|
||||||
当前
|
</Tag>
|
||||||
</Tag>
|
)}
|
||||||
)}
|
</td>
|
||||||
</div>
|
<td style={{ padding: '12px', textAlign: 'center' }}>{slot.planned}</td>
|
||||||
|
<td style={{ padding: '12px', textAlign: 'center', color: slot.created > 0 ? colors.text : colors.textMuted }}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
{slot.created}
|
||||||
<div>
|
</td>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}>
|
<td style={{ padding: '12px', textAlign: 'center', color: slot.completed > 0 ? statusColors.completed : colors.textMuted }}>
|
||||||
<span>计划</span>
|
{slot.completed}
|
||||||
<span>{slot.planned}</span>
|
</td>
|
||||||
</div>
|
<td style={{ padding: '12px' }}>
|
||||||
<div style={{ height: 6, background: colors.bgSubtle, borderRadius: 3, overflow: 'hidden' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<div style={{ width: `${plannedPct}%`, height: '100%', background: colors.textMuted, borderRadius: 3 }} />
|
<div style={{ flex: 1, height: 6, background: colors.bgSubtle, borderRadius: 3, overflow: 'hidden' }}>
|
||||||
</div>
|
<div
|
||||||
</div>
|
style={{
|
||||||
|
width: `${pct}%`,
|
||||||
<div>
|
height: '100%',
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}>
|
background: isComplete ? statusColors.completed : colors.primary,
|
||||||
<span>已创建</span>
|
borderRadius: 3,
|
||||||
<span>{slot.created}/{slot.planned}</span>
|
transition: 'width 0.3s',
|
||||||
</div>
|
}}
|
||||||
<div style={{ height: 6, background: colors.bgSubtle, borderRadius: 3, overflow: 'hidden' }}>
|
/>
|
||||||
<div style={{ width: `${createdPct}%`, height: '100%', background: colors.primary, borderRadius: 3 }} />
|
</div>
|
||||||
</div>
|
<span style={{ fontSize: 12, color: colors.textSecondary, minWidth: 36, textAlign: 'right' }}>{pct}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
</td>
|
||||||
<div>
|
<td style={{ padding: '12px', textAlign: 'center' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}>
|
{isComplete ? (
|
||||||
<span>已完成</span>
|
<Tag color={statusColors.completed} style={{ fontSize: 11, margin: 0 }}>已完成</Tag>
|
||||||
<span>{slot.completed}/{slot.planned}</span>
|
) : isPast ? (
|
||||||
</div>
|
<Tag color={statusColors.failed} style={{ fontSize: 11, margin: 0 }}>未达标</Tag>
|
||||||
<div style={{ height: 6, background: colors.bgSubtle, borderRadius: 3, overflow: 'hidden' }}>
|
) : slot.is_current ? (
|
||||||
<div style={{ width: `${completedPct}%`, height: '100%', background: statusColors.completed, borderRadius: 3 }} />
|
<Tag color={colors.primary} style={{ fontSize: 11, margin: 0 }}>进行中</Tag>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<Tag style={{ fontSize: 11, margin: 0 }}>待执行</Tag>
|
||||||
</div>
|
)}
|
||||||
</Card>
|
</td>
|
||||||
)
|
</tr>
|
||||||
})}
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -273,7 +301,7 @@ export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', overflowY: 'auto', padding: '16px 16px 24px' }}>
|
<div style={{ height: '100%', overflowY: 'auto', padding: '16px 16px 24px' }}>
|
||||||
<Card size="small" title="生命周期" style={{ marginBottom: 16 }}>
|
<Card size="small" title="生命周期" style={{ marginBottom: 16 }}>
|
||||||
<LifecycleTimeline currentStage={progress.current_stage} abnormal={abnormal} />
|
<LifecycleTimeline ev={ev} currentStage={progress.current_stage} abnormal={abnormal} />
|
||||||
{progress.blocker != null && (
|
{progress.blocker != null && (
|
||||||
<Alert
|
<Alert
|
||||||
type={abnormal ? 'error' : 'warning'}
|
type={abnormal ? 'error' : 'warning'}
|
||||||
@ -296,7 +324,7 @@ export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) {
|
|||||||
{progress.slots.length === 0 ? (
|
{progress.slots.length === 0 ? (
|
||||||
<Empty description="暂无时段分布(粗计划未提交或评估未开始执行)" />
|
<Empty description="暂无时段分布(粗计划未提交或评估未开始执行)" />
|
||||||
) : (
|
) : (
|
||||||
<TimeDistributionCards slots={progress.slots} />
|
<TimeDistributionTable slots={progress.slots} />
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user