diff --git a/src/features/workbench/map-workbench-page.tsx b/src/features/workbench/map-workbench-page.tsx index bc0a876..5e61e5d 100644 --- a/src/features/workbench/map-workbench-page.tsx +++ b/src/features/workbench/map-workbench-page.tsx @@ -996,7 +996,8 @@ export function MapWorkbenchPage({ 运行时间轴 计划 {operationalSummary.planned} 执行 {operationalSummary.active} - 异常 {operationalSummary.exceptions} + 关注 {operationalSummary.attention} + 异常 {operationalSummary.exceptions} 查看全天 : null diff --git a/src/features/workbench/operational-timeline/operational-timeline-model.test.ts b/src/features/workbench/operational-timeline/operational-timeline-model.test.ts index a5176b4..5da9834 100644 --- a/src/features/workbench/operational-timeline/operational-timeline-model.test.ts +++ b/src/features/workbench/operational-timeline/operational-timeline-model.test.ts @@ -6,9 +6,11 @@ import { createOperationalEvents, DEFAULT_OPERATIONAL_EVENT_VISIBILITY, filterOperationalEvents, + getOperationalEventCategory, OPERATIONAL_TIMELINE_LAYOUT, resolveTimelineScaleLayout } from "./operational-timeline-model"; +import type { OperationalEvent } from "./operational-timeline-model"; const workOrder: ScheduledConditionItem = { id: "work-1", @@ -52,6 +54,28 @@ describe("operational timeline model", () => { expect(filtered.map((event) => event.lane)).toEqual(["plan"]); }); + it("separates attention events from failed exceptions", () => { + const [planEvent] = createOperationalEvents([workOrder]); + const attentionEvent = { + ...planEvent, + id: "attention-1", + lane: "exception", + status: "delayed" + } satisfies OperationalEvent; + const exceptionEvent = { + ...attentionEvent, + id: "exception-1", + status: "failed" + } satisfies OperationalEvent; + + expect(getOperationalEventCategory(attentionEvent)).toBe("attention"); + expect(getOperationalEventCategory(exceptionEvent)).toBe("exception"); + expect(filterOperationalEvents([attentionEvent, exceptionEvent], { + ...DEFAULT_OPERATIONAL_EVENT_VISIBILITY, + attention: false + })).toEqual([exceptionEvent]); + }); + it("only promotes abnormal condition runs into exception events", () => { const normal = { id: "condition-normal", diff --git a/src/features/workbench/operational-timeline/operational-timeline-model.ts b/src/features/workbench/operational-timeline/operational-timeline-model.ts index 93a0d0c..14ee33c 100644 --- a/src/features/workbench/operational-timeline/operational-timeline-model.ts +++ b/src/features/workbench/operational-timeline/operational-timeline-model.ts @@ -9,7 +9,8 @@ export type OperationalTimelineMode = "compact" | "expanded" | "summary"; export type OperationalEventLane = "plan" | "actual" | "exception"; export type OperationalEventStatus = "planned" | "executed" | "delayed" | "failed" | "cancelled"; export type OperationalEventImportance = "normal" | "important" | "critical"; -export type OperationalEventVisibility = Record; +export type OperationalEventCategory = "plan" | "actual" | "attention" | "exception"; +export type OperationalEventVisibility = Record; export type OperationalEvent = { id: string; @@ -50,6 +51,7 @@ export const OPERATIONAL_TIMELINE_LAYOUT = { export const DEFAULT_OPERATIONAL_EVENT_VISIBILITY: OperationalEventVisibility = { plan: true, actual: true, + attention: true, exception: true }; @@ -128,16 +130,24 @@ export function getOperationalEventSummary(events: OperationalEvent[]) { return { planned: events.filter((event) => event.lane === "plan").length, active: events.filter((event) => event.lane === "actual").length, - exceptions: events.filter((event) => event.lane === "exception").length, + attention: events.filter((event) => getOperationalEventCategory(event) === "attention").length, + exceptions: events.filter((event) => getOperationalEventCategory(event) === "exception").length, critical: events.filter((event) => event.importance === "critical").length }; } +export function getOperationalEventCategory( + event: OperationalEvent +): OperationalEventCategory { + if (event.lane === "plan" || event.lane === "actual") return event.lane; + return event.status === "failed" ? "exception" : "attention"; +} + export function filterOperationalEvents( events: OperationalEvent[], visibility: OperationalEventVisibility ) { - return events.filter((event) => visibility[event.lane]); + return events.filter((event) => visibility[getOperationalEventCategory(event)]); } function createConditionEvents(item: ScheduledConditionRecord): OperationalEvent[] { diff --git a/src/features/workbench/operational-timeline/operational-timeline.tsx b/src/features/workbench/operational-timeline/operational-timeline.tsx index 1bcabfd..0c18e69 100644 --- a/src/features/workbench/operational-timeline/operational-timeline.tsx +++ b/src/features/workbench/operational-timeline/operational-timeline.tsx @@ -21,6 +21,7 @@ import { getOperationalEventSummary, OPERATIONAL_TIMELINE_LAYOUT, type OperationalEvent, + type OperationalEventCategory, type OperationalEventCluster, type OperationalEventLane, type OperationalEventVisibility, @@ -46,6 +47,13 @@ const LANES: Array<{ id: OperationalEventLane; label: string }> = [ { id: "exception", label: "异常" } ]; +const LEGEND_ITEMS: Array<{ id: OperationalEventCategory; label: string }> = [ + { id: "plan", label: "计划" }, + { id: "actual", label: "实际" }, + { id: "attention", label: "关注" }, + { id: "exception", label: "异常" } +]; + export function OperationalTimeline({ date, cursorTime, @@ -96,6 +104,10 @@ export function OperationalTimeline({ ) : ( 分析视图保持独立时间范围 )} + ); @@ -103,13 +115,7 @@ export function OperationalTimeline({ return ( -
- -
- + {!mobile ? : null} {selectedEvent && !mobile ? ( ); @@ -519,14 +502,15 @@ function TimelineLegend({ ); } -function LegendSwatch({ lane }: { lane: OperationalEventLane }) { +function LegendSwatch({ category }: { category: OperationalEventCategory }) { return ( );