diff --git a/frontend/web/src/components/WindowTimeline.tsx b/frontend/web/src/components/WindowTimeline.tsx new file mode 100644 index 0000000..91a20ca --- /dev/null +++ b/frontend/web/src/components/WindowTimeline.tsx @@ -0,0 +1,122 @@ +import { Tooltip } from 'antd' +import type { ReactNode } from 'react' +import { colors, fontSizes } from '../tokens' + +// A read-only horizontal timeline: one axis representing a service-cycle window, +// with markers positioned by their window offset. It is domain-agnostic — the +// ②计划预览 and ④过程时间轴 callers each adapt their data into markers. Colours +// are assigned stably per `colorKey` (same key → same colour, order-independent). + +export interface TimelineMarker { + key: string + offsetSeconds: number + colorKey: string + label?: string + badge?: number | string + tooltip: ReactNode + onClick?: () => void +} + +interface WindowTimelineProps { + windowSeconds: number + markers: TimelineMarker[] + /** colorKey → legend display name; falls back to the raw key. */ + colorLabels?: Record + showLegend?: boolean + height?: number +} + +const PALETTE = [ + '#1677ff', '#52c41a', '#fa8c16', '#722ed1', + '#13c2c2', '#eb2f96', '#faad14', '#2f54eb', +] + +const TICK_COUNT = 6 + +function fmtTick(seconds: number): string { + if (seconds < 3600) return `${Math.round(seconds / 60)}m` + const h = seconds / 3600 + return `${Number.isInteger(h) ? h : h.toFixed(1)}h` +} + +export default function WindowTimeline({ + windowSeconds, markers, colorLabels, showLegend = false, height = 64, +}: WindowTimelineProps) { + const window = windowSeconds > 0 ? windowSeconds : 1 + const uniqueKeys = Array.from(new Set(markers.map((m) => m.colorKey))).sort() + const colorOf = (key: string) => PALETTE[Math.max(0, uniqueKeys.indexOf(key)) % PALETTE.length] + const leftPct = (offset: number) => `${Math.min(1, Math.max(0, offset / window)) * 100}%` + + const ticks = Array.from({ length: TICK_COUNT + 1 }, (_, i) => i / TICK_COUNT) + + return ( +
+
+
+ + {ticks.map((t) => ( +
+
+ + {fmtTick(t * window)} + +
+ ))} + + {markers.map((m) => { + const c = colorOf(m.colorKey) + return ( + +
+ {m.label != null ? ( + + {m.label} + {m.badge != null && {m.badge}} + + ) : ( + + )} +
+
+ ) + })} +
+ + {showLegend && uniqueKeys.length > 0 && ( +
+ {uniqueKeys.map((k) => ( + + + {colorLabels?.[k] ?? k} + + ))} +
+ )} +
+ ) +}