feat(ui): 四種嚴重度管線動畫 + WoooClaw 命名更新
- flow-pipeline.tsx: 新增 severity prop,四種管線樣式
- P0 → Style A: 脈衝光波 + 流動光效 (#cc2200)
- P1 → Style B: 進度條,龍蝦站在進度端點 (#F59E0B)
- P2 → Style C: 卡片步驟,龍蝦浮在 active 卡片上方 (#4A90D9)
- P3 → Style D: 時間軸,虛線流動動畫 (#22C55E)
- incident-card.tsx: FlowPipeline 傳入 severity={sev}
- openclaw-panel.tsx: NemoClaw→WoooClaw, OpenClaw Pipeline→WoooClaw Pipeline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -229,7 +229,7 @@ export function OpenClawPanel({
|
||||
{/* 右側文字 */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#141413', marginBottom: 2 }}>
|
||||
NemoClaw
|
||||
WoooClaw
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#87867f', marginBottom: 6 }}>
|
||||
{tPanel(status)}
|
||||
@@ -241,7 +241,7 @@ export function OpenClawPanel({
|
||||
borderRadius: 10,
|
||||
color: '#4A90D9',
|
||||
}}>
|
||||
OpenClaw Pipeline
|
||||
WoooClaw Pipeline
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
* FlowPipeline - AI中心 v6 事件流程管線
|
||||
* ========================================
|
||||
* 7節點水平管線:告警→AI偵測→AI分析→提案生成→等待授權→執行→完成
|
||||
* Q版龍蝦 SVG:進行中=橘紅 #E85530,完成=綠色 #22C55E
|
||||
* lobster-bob CSS 動畫:上下浮動 4px,1.5s loop
|
||||
* 4種嚴重度對應不同管線樣式:
|
||||
* P0 → Style A: 脈衝光波 (紅 #cc2200)
|
||||
* P1 → Style B: 進度條 (橘 #F59E0B)
|
||||
* P2 → Style C: 卡片步驟 (藍 #4A90D9)
|
||||
* P3 → Style D: 時間軸 (綠 #22C55E)
|
||||
* 2026-04-02 Claude Code: 新增 severity prop + 四種管線渲染
|
||||
*/
|
||||
|
||||
export type FlowStage =
|
||||
@@ -22,167 +26,278 @@ 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 資訊(可選)*/
|
||||
severity?: 'P0' | 'P1' | 'P2' | 'P3'
|
||||
tooltips?: Partial<Record<FlowStage, string>>
|
||||
}
|
||||
|
||||
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' },
|
||||
{ id: 'alert', label: '告警' },
|
||||
{ id: 'detection', label: 'AI偵測' },
|
||||
{ id: 'analysis', label: 'AI分析' },
|
||||
{ id: 'proposal', label: '提案生成' },
|
||||
{ id: 'approval', label: '等待授權' },
|
||||
{ id: 'execution', label: '執行' },
|
||||
{ id: 'resolved', label: '完成' },
|
||||
]
|
||||
|
||||
/** 根據 activeStage 計算每個節點的狀態 */
|
||||
const SEV_COLOR: Record<string, string> = {
|
||||
P0: '#cc2200',
|
||||
P1: '#F59E0B',
|
||||
P2: '#4A90D9',
|
||||
P3: '#22C55E',
|
||||
}
|
||||
|
||||
function getActiveIndex(activeStage: FlowStage): number {
|
||||
return FLOW_NODES.findIndex(n => n.id === 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)
|
||||
const nodeIndex = FLOW_NODES.findIndex(n => n.id === nodeId)
|
||||
const activeIndex = getActiveIndex(activeStage)
|
||||
if (nodeIndex < activeIndex) return 'done'
|
||||
if (nodeIndex === activeIndex) return 'active'
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
/** Q版龍蝦 SVG (18×20) */
|
||||
function LobsterSVG({ color }: { color: string }) {
|
||||
// ── Q版龍蝦 SVG ──────────────────────────────────────────────────────────────
|
||||
|
||||
function LobsterSVG({ color, size = 18 }: { color: string; size?: number }) {
|
||||
const h = Math.round(size * 20 / 18)
|
||||
return (
|
||||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* 身體 */}
|
||||
<svg width={size} height={h} viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="9" cy="13" rx="5.5" ry="6.5" fill={color} />
|
||||
{/* 頭部 */}
|
||||
<circle cx="9" cy="7.5" r="4.5" fill={color} />
|
||||
{/* 眼睛 */}
|
||||
<circle cx="7" cy="6.5" r="1" fill="#b03a1a" />
|
||||
<circle cx="11" cy="6.5" r="1" fill="#b03a1a" />
|
||||
{/* 左鉗 */}
|
||||
<path d="M3.5 10 Q1 9 1.5 12 Q2 14 4 13" stroke={color} strokeWidth="1.2" fill="none" strokeLinecap="round"/>
|
||||
<ellipse cx="1.5" cy="12" rx="1.2" ry="1.5" fill={color} transform="rotate(-10 1.5 12)"/>
|
||||
{/* 右鉗 */}
|
||||
<path d="M14.5 10 Q17 9 16.5 12 Q16 14 14 13" stroke={color} strokeWidth="1.2" fill="none" strokeLinecap="round"/>
|
||||
<ellipse cx="16.5" cy="12" rx="1.2" ry="1.5" fill={color} transform="rotate(10 16.5 12)"/>
|
||||
{/* 觸鬚 */}
|
||||
<path d="M7 3 Q5 1 3 2" stroke="#b03a1a" strokeWidth="0.8" fill="none" strokeLinecap="round"/>
|
||||
<path d="M11 3 Q13 1 15 2" stroke="#b03a1a" strokeWidth="0.8" fill="none" strokeLinecap="round"/>
|
||||
{/* 尾部 */}
|
||||
<path d="M6 19 Q9 21 12 19" stroke={color} strokeWidth="1.2" fill="none" strokeLinecap="round"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function FlowPipeline({ activeStage, isResolved = false, tooltips }: FlowPipelineProps) {
|
||||
const lobsterColor = isResolved ? '#22C55E' : '#E85530'
|
||||
// ── Style A: P0 脈衝光波 ──────────────────────────────────────────────────────
|
||||
|
||||
function PipelineStyleA({ activeStage, isResolved }: { activeStage: FlowStage; isResolved: boolean }) {
|
||||
const color = isResolved ? '#22C55E' : SEV_COLOR.P0
|
||||
const activeIdx = getActiveIndex(activeStage)
|
||||
|
||||
return (
|
||||
<div className="flow-pipeline-wrapper" style={{ padding: '4px 10px 8px', overflowX: 'auto' }}>
|
||||
{/* CSS keyframes via style tag */}
|
||||
<div style={{ padding: '4px 10px 8px', overflowX: 'auto' }}>
|
||||
<style>{`
|
||||
@keyframes lobster-bob {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
@keyframes pulse-wave {
|
||||
0% { box-shadow: 0 0 0 0 rgba(204,34,0,0.6); }
|
||||
70% { box-shadow: 0 0 0 8px rgba(204,34,0,0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(204,34,0,0); }
|
||||
}
|
||||
.lobster-bob { animation: lobster-bob 1.5s ease-in-out infinite; }
|
||||
.flow-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #141413;
|
||||
color: #f5f4ed;
|
||||
font-size: 11px;
|
||||
padding: 3px 7px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 10;
|
||||
@keyframes flow-light {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
.pipe-a-conn-done {
|
||||
background: linear-gradient(90deg, transparent 0%, ${color} 50%, transparent 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: flow-light 1.4s linear infinite;
|
||||
}
|
||||
.flow-node-wrap:hover .flow-tooltip { opacity: 1; }
|
||||
`}</style>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 0, minWidth: 'max-content' }}>
|
||||
{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 isDone = status === 'done' || 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 (
|
||||
<div key={node.id} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div
|
||||
className="flow-node-wrap"
|
||||
style={{ position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}
|
||||
>
|
||||
{/* Q版龍蝦(僅 active 節點顯示)*/}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
|
||||
<div style={{ height: 22, display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
|
||||
{isActive && (
|
||||
<div className={isResolved ? '' : 'lobster-bob'}>
|
||||
<LobsterSVG color={lobsterColor} />
|
||||
<div style={{ animation: 'lobster-bob 1.5s ease-in-out infinite' }}>
|
||||
<LobsterSVG color={color} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 節點圓點 */}
|
||||
<div style={{
|
||||
width: 8, height: 8,
|
||||
width: 10, height: 10,
|
||||
borderRadius: '50%',
|
||||
background: dotBg,
|
||||
border: dotBorder,
|
||||
boxShadow: dotShadow,
|
||||
background: isDone ? color : isActive ? '#fff' : '#f8f9fc',
|
||||
border: isActive ? `2px solid ${color}` : isDone ? 'none' : '1.5px solid #e0ddd4',
|
||||
animation: isActive ? 'pulse-wave 1.2s ease-out infinite' : 'none',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
|
||||
{/* 節點標籤 */}
|
||||
<span style={{
|
||||
fontSize: 11,
|
||||
color: isActive ? '#141413' : isDone || isResNode ? '#E85530' : '#b0ad9f',
|
||||
color: isActive ? '#141413' : isDone ? color : '#b0ad9f',
|
||||
fontWeight: isActive ? 700 : 400,
|
||||
marginTop: 2,
|
||||
whiteSpace: 'nowrap',
|
||||
marginTop: 2, whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{node.label}
|
||||
</span>
|
||||
|
||||
{/* Tooltip */}
|
||||
{tooltips?.[node.id] && (
|
||||
<div className="flow-tooltip">{tooltips[node.id]}</div>
|
||||
)}
|
||||
</div>
|
||||
{!isLast && (
|
||||
<div
|
||||
className={idx < activeIdx ? 'pipe-a-conn-done' : ''}
|
||||
style={{
|
||||
width: 16, height: 2,
|
||||
background: idx < activeIdx ? undefined : '#e0ddd4',
|
||||
flexShrink: 0,
|
||||
marginBottom: 18,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 連接線(最後一個節點後不需要)*/}
|
||||
// ── Style B: P1 進度條 ────────────────────────────────────────────────────────
|
||||
|
||||
function PipelineStyleB({ activeStage, isResolved }: { activeStage: FlowStage; isResolved: boolean }) {
|
||||
const color = isResolved ? '#22C55E' : SEV_COLOR.P1
|
||||
const activeIdx = getActiveIndex(activeStage)
|
||||
const totalNodes = FLOW_NODES.length
|
||||
const progressPct = isResolved ? 100 : (activeIdx / (totalNodes - 1)) * 100
|
||||
|
||||
return (
|
||||
<div style={{ padding: '4px 10px 12px', overflowX: 'auto' }}>
|
||||
<style>{`
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
`}</style>
|
||||
{/* 進度條軌道 */}
|
||||
<div style={{ position: 'relative', height: 54 }}>
|
||||
{/* 底部軌道 */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 16, left: 0, right: 0,
|
||||
height: 4, background: '#e8e5dc', borderRadius: 2,
|
||||
}} />
|
||||
{/* 已完成進度 */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 16, left: 0,
|
||||
height: 4, background: color, borderRadius: 2,
|
||||
width: `${progressPct}%`,
|
||||
transition: 'width 0.4s ease',
|
||||
}} />
|
||||
{/* 節點圓點 */}
|
||||
{FLOW_NODES.map((node, idx) => {
|
||||
const pct = (idx / (totalNodes - 1)) * 100
|
||||
const status = getNodeStatus(node.id, activeStage, isResolved)
|
||||
const isActive = status === 'active'
|
||||
const isDone = status === 'done' || status === 'resolved'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: `${pct}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
|
||||
}}
|
||||
>
|
||||
{/* 龍蝦 (active 節點,站在進度條終點上方) */}
|
||||
{isActive && (
|
||||
<div style={{ animation: 'lobster-bob 1.5s ease-in-out infinite', marginBottom: 2 }}>
|
||||
<LobsterSVG color={color} size={16} />
|
||||
</div>
|
||||
)}
|
||||
{!isActive && <div style={{ height: 20 }} />}
|
||||
{/* 圓點 */}
|
||||
<div style={{
|
||||
width: 8, height: 8, borderRadius: '50%',
|
||||
background: isDone ? color : isActive ? '#fff' : '#f8f9fc',
|
||||
border: isActive ? `2px solid ${color}` : isDone ? 'none' : '1.5px solid #e0ddd4',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
{/* 標籤 */}
|
||||
<span style={{
|
||||
fontSize: 10, color: isActive ? '#141413' : isDone ? color : '#b0ad9f',
|
||||
fontWeight: isActive ? 700 : 400,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{node.label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Style C: P2 卡片步驟 ──────────────────────────────────────────────────────
|
||||
|
||||
function PipelineStyleC({ activeStage, isResolved }: { activeStage: FlowStage; isResolved: boolean }) {
|
||||
const color = isResolved ? '#22C55E' : SEV_COLOR.P2
|
||||
const activeIdx = getActiveIndex(activeStage)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '4px 10px 8px', overflowX: 'auto' }}>
|
||||
<style>{`
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
@keyframes card-glow {
|
||||
0%,100% { box-shadow: 0 0 0 0 rgba(74,144,217,0.3); }
|
||||
50% { box-shadow: 0 0 6px 2px rgba(74,144,217,0.3); }
|
||||
}
|
||||
`}</style>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, minWidth: 'max-content' }}>
|
||||
{FLOW_NODES.map((node, idx) => {
|
||||
const status = getNodeStatus(node.id, activeStage, isResolved)
|
||||
const isActive = status === 'active'
|
||||
const isDone = status === 'done' || status === 'resolved'
|
||||
const isLast = idx === FLOW_NODES.length - 1
|
||||
|
||||
return (
|
||||
<div key={node.id} style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
|
||||
{/* 龍蝦 */}
|
||||
<div style={{ height: 22, display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
|
||||
{isActive && (
|
||||
<div style={{ animation: 'lobster-bob 1.5s ease-in-out infinite' }}>
|
||||
<LobsterSVG color={color} size={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 卡片 */}
|
||||
<div style={{
|
||||
padding: '3px 6px',
|
||||
background: isDone ? color : isActive ? '#fff' : '#f8f9fc',
|
||||
border: isActive ? `1.5px solid ${color}` : isDone ? 'none' : '1px solid #e0ddd4',
|
||||
borderRadius: 5,
|
||||
animation: isActive ? 'card-glow 1.5s ease-in-out infinite' : 'none',
|
||||
minWidth: 36,
|
||||
textAlign: 'center' as const,
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 10,
|
||||
color: isDone ? '#fff' : isActive ? color : '#b0ad9f',
|
||||
fontWeight: isActive || isDone ? 700 : 400,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{node.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{!isLast && (
|
||||
<div style={{
|
||||
width: 16, height: 1,
|
||||
background: connColor,
|
||||
width: 8, height: 1.5,
|
||||
background: idx < activeIdx ? color : '#e0ddd4',
|
||||
flexShrink: 0,
|
||||
marginBottom: 18, // 對齊圓點位置(跳過 lobster 空間 + 標籤)
|
||||
marginBottom: 11,
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
@@ -193,4 +308,96 @@ export function FlowPipeline({ activeStage, isResolved = false, tooltips }: Flow
|
||||
)
|
||||
}
|
||||
|
||||
// ── Style D: P3 時間軸 ────────────────────────────────────────────────────────
|
||||
|
||||
function PipelineStyleD({ activeStage, isResolved }: { activeStage: FlowStage; isResolved: boolean }) {
|
||||
const color = isResolved ? '#22C55E' : SEV_COLOR.P3
|
||||
const activeIdx = getActiveIndex(activeStage)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '4px 10px 8px', overflowX: 'auto' }}>
|
||||
<style>{`
|
||||
@keyframes lobster-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
|
||||
@keyframes dash-march {
|
||||
to { stroke-dashoffset: -20; }
|
||||
}
|
||||
`}</style>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 0, minWidth: 'max-content' }}>
|
||||
{FLOW_NODES.map((node, idx) => {
|
||||
const status = getNodeStatus(node.id, activeStage, isResolved)
|
||||
const isActive = status === 'active'
|
||||
const isDone = status === 'done' || status === 'resolved'
|
||||
const isLast = idx === FLOW_NODES.length - 1
|
||||
const isNextActive = idx === activeIdx - 1
|
||||
|
||||
return (
|
||||
<div key={node.id} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
|
||||
<div style={{ height: 22, display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
|
||||
{isActive && (
|
||||
<div style={{ animation: 'lobster-bob 1.5s ease-in-out infinite' }}>
|
||||
<LobsterSVG color={color} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
width: 8, height: 8, borderRadius: '50%',
|
||||
background: isDone ? color : isActive ? '#fff' : '#f8f9fc',
|
||||
border: isActive ? `2px solid ${color}` : isDone ? 'none' : '1.5px solid #e0ddd4',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
<span style={{
|
||||
fontSize: 11,
|
||||
color: isActive ? '#141413' : isDone ? color : '#b0ad9f',
|
||||
fontWeight: isActive ? 700 : 400,
|
||||
marginTop: 2, whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{node.label}
|
||||
</span>
|
||||
</div>
|
||||
{!isLast && (
|
||||
<div style={{ marginBottom: 18, flexShrink: 0 }}>
|
||||
{/* Active 前的虛線用 SVG 做流動動畫 */}
|
||||
{isNextActive ? (
|
||||
<svg width="16" height="2" style={{ display: 'block' }}>
|
||||
<line x1="0" y1="1" x2="16" y2="1"
|
||||
stroke={color} strokeWidth="1.5"
|
||||
strokeDasharray="4 3"
|
||||
style={{ animation: 'dash-march 0.6s linear infinite', strokeDashoffset: 0 }}
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<div style={{
|
||||
width: 16, height: 1.5,
|
||||
background: isDone ? color : '#e0ddd4',
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main Component ────────────────────────────────────────────────────────────
|
||||
|
||||
export function FlowPipeline({ activeStage, isResolved = false, severity = 'P3', tooltips: _tooltips }: FlowPipelineProps) {
|
||||
// 2026-04-02 Claude Code: severity → pipeline style mapping
|
||||
// P0=StyleA(脈衝光波) P1=StyleB(進度條) P2=StyleC(卡片步驟) P3=StyleD(時間軸)
|
||||
if (isResolved || severity === 'P3') {
|
||||
return <PipelineStyleD activeStage={activeStage} isResolved={isResolved} />
|
||||
}
|
||||
if (severity === 'P0') {
|
||||
return <PipelineStyleA activeStage={activeStage} isResolved={false} />
|
||||
}
|
||||
if (severity === 'P1') {
|
||||
return <PipelineStyleB activeStage={activeStage} isResolved={false} />
|
||||
}
|
||||
// P2
|
||||
return <PipelineStyleC activeStage={activeStage} isResolved={false} />
|
||||
}
|
||||
|
||||
export default FlowPipeline
|
||||
|
||||
@@ -292,7 +292,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
</div>
|
||||
|
||||
{/* 流程狀態圖 */}
|
||||
<FlowPipeline activeStage={flowStage} isResolved={isResolved} />
|
||||
<FlowPipeline activeStage={flowStage} isResolved={isResolved} severity={sev} />
|
||||
|
||||
{/* Impact 指標列 */}
|
||||
<div style={{
|
||||
|
||||
Reference in New Issue
Block a user