From 16ca1339557eca4372dbfacf9f8bfa816d39d5a0 Mon Sep 17 00:00:00 2001 From: OG T Date: Wed, 1 Apr 2026 19:47:19 +0800 Subject: [PATCH] feat(incident): add FlowPipeline 7-node pipeline with lobster animation --- .../src/components/incident/flow-pipeline.tsx | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 apps/web/src/components/incident/flow-pipeline.tsx diff --git a/apps/web/src/components/incident/flow-pipeline.tsx b/apps/web/src/components/incident/flow-pipeline.tsx new file mode 100644 index 000000000..3ef4ae2b0 --- /dev/null +++ b/apps/web/src/components/incident/flow-pipeline.tsx @@ -0,0 +1,196 @@ +'use client' + +/** + * FlowPipeline - AI中心 v6 事件流程管線 + * ======================================== + * 7節點水平管線:告警→AI偵測→AI分析→提案生成→等待授權→執行→完成 + * Q版龍蝦 SVG:進行中=橘紅 #E85530,完成=綠色 #22C55E + * lobster-bob CSS 動畫:上下浮動 4px,1.5s loop + */ + +export type FlowStage = + | 'alert' + | 'detection' + | 'analysis' + | 'proposal' + | 'approval' + | 'execution' + | 'resolved' + +export type NodeStatus = 'done' | 'active' | 'pending' | 'resolved' + +interface FlowNode { + id: FlowStage + label: string + labelKey: string +} + +export interface FlowPipelineProps { + /** 目前進行到哪個 stage */ + activeStage: FlowStage + /** 是否已全部完成(resolved 狀態,綠色龍蝦靜止)*/ + isResolved?: boolean + /** 各節點 tooltip 資訊(可選)*/ + tooltips?: Partial> +} + +const FLOW_NODES: FlowNode[] = [ + { id: 'alert', label: '告警', labelKey: 'flow.alert' }, + { id: 'detection', label: 'AI偵測', labelKey: 'flow.detection' }, + { id: 'analysis', label: 'AI分析', labelKey: 'flow.analysis' }, + { id: 'proposal', label: '提案生成', labelKey: 'flow.proposal' }, + { id: 'approval', label: '等待授權', labelKey: 'flow.approval' }, + { id: 'execution', label: '執行', labelKey: 'flow.execution' }, + { id: 'resolved', label: '完成', labelKey: 'flow.resolved' }, +] + +/** 根據 activeStage 計算每個節點的狀態 */ +function getNodeStatus(nodeId: FlowStage, activeStage: FlowStage, isResolved: boolean): NodeStatus { + if (isResolved) return 'resolved' + const stageOrder = FLOW_NODES.map(n => n.id) + const nodeIndex = stageOrder.indexOf(nodeId) + const activeIndex = stageOrder.indexOf(activeStage) + if (nodeIndex < activeIndex) return 'done' + if (nodeIndex === activeIndex) return 'active' + return 'pending' +} + +/** Q版龍蝦 SVG (18×20) */ +function LobsterSVG({ color }: { color: string }) { + return ( + + {/* 身體 */} + + {/* 頭部 */} + + {/* 眼睛 */} + + + {/* 左鉗 */} + + + {/* 右鉗 */} + + + {/* 觸鬚 */} + + + {/* 尾部 */} + + + ) +} + +export function FlowPipeline({ activeStage, isResolved = false, tooltips }: FlowPipelineProps) { + const lobsterColor = isResolved ? '#22C55E' : '#E85530' + + return ( +
+ {/* CSS keyframes via style tag */} + + +
+ {FLOW_NODES.map((node, idx) => { + const status = getNodeStatus(node.id, activeStage, isResolved) + const isActive = status === 'active' + const isDone = status === 'done' + const isResNode = status === 'resolved' + const isLast = idx === FLOW_NODES.length - 1 + + // 節點顏色 + const dotBg = isDone || isResNode + ? (isResNode ? '#22C55E' : '#E85530') + : isActive ? '#141413' : '#f8f9fc' + const dotBorder = isActive + ? '1.5px solid #E85530' + : isDone || isResNode + ? 'none' + : '1.5px solid #e0ddd4' + const dotShadow = isActive ? '0 0 0 3px rgba(232,85,48,0.2)' : 'none' + + // 連接線顏色 + const connColor = isDone || isResNode ? 'rgba(232,85,48,0.5)' : '#e0ddd4' + + return ( +
+
+ {/* Q版龍蝦(僅 active 節點顯示)*/} +
+ {isActive && ( +
+ +
+ )} +
+ + {/* 節點圓點 */} +
+ + {/* 節點標籤 */} + + {node.label} + + + {/* Tooltip */} + {tooltips?.[node.id] && ( +
{tooltips[node.id]}
+ )} +
+ + {/* 連接線(最後一個節點後不需要)*/} + {!isLast && ( +
+ )} +
+ ) + })} +
+
+ ) +} + +export default FlowPipeline