import React, { useEffect, useState, useMemo } from 'react'; import { ReactFlowProvider, ReactFlow, Background, Node, Edge, Handle, Position, BackgroundVariant } from '@xyflow/react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Loader2, AlertCircle, User, Building2, Layers, Calendar, Clock, CheckCircle2, XCircle, AlertTriangle } from 'lucide-react'; import { cn } from '@/lib/utils'; import '@xyflow/react/dist/style.css'; import { getDeployRecordFlowGraph } from '../service'; import type { DeployRecordFlowGraph, WorkflowNodeInstance } from '../types'; import { getStatusIcon, getStatusText, getNodeStatusText, getNodeStatusColor, formatTime, formatDuration, calculateRunningDuration } from '../utils/dashboardUtils'; interface DeployFlowGraphModalProps { open: boolean; deployRecordId: number | null; onOpenChange: (open: boolean) => void; } /** * 自定义流程节点组件 */ const CustomFlowNode: React.FC = ({ data }) => { const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage, isUnreachable } = data; const statusColor = getNodeStatusColor(status); const isNotStarted = status === 'NOT_STARTED'; const isRunning = status === 'RUNNING'; const hasFailed = status === 'FAILED'; // 计算显示的时长 const displayDuration = useMemo(() => { if (duration !== null && duration !== undefined) { // 后端返回的 duration 单位已经是毫秒 return formatDuration(duration); } if (isRunning && startTime) { const runningDuration = calculateRunningDuration(startTime); return formatDuration(runningDuration); } return null; }, [duration, isRunning, startTime]); const nodeContent = (
{/* 节点名称 */}
{nodeName}
{/* 节点状态 */}
{getNodeStatusText(status)}
{/* 时间信息 */} {!isNotStarted && (
{startTime &&
开始: {formatTime(startTime)}
} {endTime &&
结束: {formatTime(endTime)}
} {displayDuration && (
{isRunning ? '运行中: ' : '时长: '} {displayDuration}
)}
)} {/* 错误提示 */} {hasFailed && errorMessage && (
有错误
)}
); return ( <> {/* 输入连接点 */} {nodeType !== 'START_EVENT' && ( )} {/* 节点内容 - 如果有错误信息,包装在 Tooltip 中 */} {hasFailed && errorMessage ? ( {nodeContent}

错误信息:

{errorMessage}

) : ( nodeContent )} {/* 输出连接点 */} {nodeType !== 'END_EVENT' && ( )} ); }; const nodeTypes = { custom: CustomFlowNode, }; /** * 业务信息卡片 */ const BusinessInfoCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { return ( 业务信息
应用名称: {flowData.applicationName}
应用编码: {flowData.applicationCode}
部署环境: {flowData.environmentName}
所属团队: {flowData.teamName}
部署人: {flowData.deployBy}
{flowData.deployRemark && ( <>
备注:

{flowData.deployRemark}

)}
); }; /** * 进度信息卡片 */ const ProgressCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { const { executedNodeCount, totalNodeCount, successNodeCount, failedNodeCount, runningNodeCount } = flowData; const progress = totalNodeCount > 0 ? (executedNodeCount / totalNodeCount) * 100 : 0; return ( 执行进度
已执行 {executedNodeCount} / {totalNodeCount} 节点
{Math.round(progress)}%
成功 {successNodeCount}
失败 {failedNodeCount}
运行中 {runningNodeCount}
); }; /** * 时长统计卡片 */ const DurationCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { const { deployStartTime, deployDuration, nodeInstances, runningNodeCount } = flowData; const isRunning = runningNodeCount > 0; // 计算当前总时长 const currentDuration = useMemo(() => { if (deployDuration) return deployDuration; // 后端返回的已经是毫秒 if (isRunning) return calculateRunningDuration(deployStartTime); return 0; }, [deployDuration, isRunning, deployStartTime]); return ( 时长统计
开始时间: {deployStartTime ? formatTime(deployStartTime) : '-'}
总时长: {formatDuration(currentDuration)} {isRunning && (运行中...)}
节点执行时长:
{nodeInstances .filter(ni => ni.status !== 'NOT_STARTED') .map((ni) => { // 后端返回的 duration 单位已经是毫秒 const nodeDuration = ni.duration ? ni.duration : (ni.status === 'RUNNING' ? calculateRunningDuration(ni.startTime) : 0); return (
{ni.nodeName} {formatDuration(nodeDuration)} {ni.status === 'RUNNING' && '...'}
); })}
); }; /** * 实时监控提示 */ const MonitoringAlert: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { const { runningNodeCount } = flowData; if (runningNodeCount === 0) return null; return ( 部署正在进行中 当前有 {runningNodeCount} 个节点正在执行,请稍候... ); }; /** * 信息面板(左侧边栏) */ const DeployInfoPanel: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { return (
); }; /** * 部署流程图模态框(主组件) */ export const DeployFlowGraphModal: React.FC = ({ open, deployRecordId, onOpenChange, }) => { const [loading, setLoading] = useState(false); const [flowData, setFlowData] = useState(null); // 加载流程图数据 useEffect(() => { if (open && deployRecordId) { setLoading(true); getDeployRecordFlowGraph(deployRecordId) .then((data) => { setFlowData(data); }) .catch((error) => { console.error('加载部署流程图失败:', error); }) .finally(() => { setLoading(false); }); } else { setFlowData(null); } }, [open, deployRecordId]); // 创建节点ID到实例的映射 const nodeInstanceMap = useMemo(() => { if (!flowData?.nodeInstances) return new Map(); const map = new Map(); flowData.nodeInstances.forEach((instance) => { map.set(instance.nodeId, instance); }); return map; }, [flowData]); // 计算可达节点(从已执行节点出发能到达的节点) const reachableNodes = useMemo(() => { if (!flowData?.graph?.edges || !flowData?.nodeInstances) return new Set(); const executedNodeIds = flowData.nodeInstances .filter(ni => ni.status !== 'NOT_STARTED') .map(ni => ni.nodeId); if (executedNodeIds.length === 0) return new Set(); const reachable = new Set(executedNodeIds); const queue = [...executedNodeIds]; // BFS 遍历所有可达节点 while (queue.length > 0) { const currentId = queue.shift()!; const outgoingEdges = flowData.graph.edges.filter(e => e.from === currentId); for (const edge of outgoingEdges) { if (!reachable.has(edge.to)) { reachable.add(edge.to); queue.push(edge.to); } } } return reachable; }, [flowData]); // 转换为 React Flow 节点(显示所有节点,但对不可达节点置灰) const flowNodes: Node[] = useMemo(() => { if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return []; const isRunning = flowData.runningNodeCount > 0; // 按执行顺序排序已执行的节点 const executedInstances = flowData.nodeInstances .filter(ni => ni.status !== 'NOT_STARTED') .sort((a, b) => { const timeA = a.startTime ? new Date(a.startTime.replace(' ', 'T')).getTime() : 0; const timeB = b.startTime ? new Date(b.startTime.replace(' ', 'T')).getTime() : 0; return timeA - timeB; }); // 线性布局:从左到右排列 const nodePositionMap = new Map(); const horizontalSpacing = 300; // 水平间距 const verticalSpacing = 150; // 垂直间距(用于分支) const startX = 100; // 起始X坐标 const startY = 150; // 起始Y坐标 // 已执行的节点:从左到右线性排列 executedInstances.forEach((instance, index) => { nodePositionMap.set(instance.nodeId, { x: startX + index * horizontalSpacing, y: startY, }); }); // 未执行的节点:根据在图中的位置和层级布局 const notStartedNodes = flowData.graph.nodes.filter( node => !nodePositionMap.has(node.id) ); // 简单布局:按节点在edges中的出现顺序,从左到右、上到下排列 let currentX = startX + executedInstances.length * horizontalSpacing; let currentRow = 0; const nodesPerRow = 3; notStartedNodes.forEach((node, index) => { const row = Math.floor(index / nodesPerRow); const col = index % nodesPerRow; nodePositionMap.set(node.id, { x: currentX + col * horizontalSpacing, y: startY + row * verticalSpacing, }); }); // 生成所有节点 return flowData.graph.nodes.map((node) => { const instance = nodeInstanceMap.get(node.id); const position = nodePositionMap.get(node.id) || { x: 0, y: 0 }; const isReachable = reachableNodes.has(node.id); const isNotStarted = !instance || instance.status === 'NOT_STARTED'; return { id: node.id, type: 'custom', position, data: { nodeName: node.nodeName, nodeType: node.nodeType, status: instance?.status || 'NOT_STARTED', startTime: instance?.startTime, endTime: instance?.endTime, duration: instance?.duration, errorMessage: instance?.errorMessage, // 新增:不可达且未执行的节点标记为置灰 isUnreachable: isRunning && isNotStarted && !isReachable, }, }; }); }, [flowData, nodeInstanceMap, reachableNodes]); // 转换为 React Flow 边 const flowEdges: Edge[] = useMemo(() => { if (!flowData?.graph?.edges) return []; const isRunning = flowData.runningNodeCount > 0; const displayedNodeIds = new Set(flowNodes.map(n => n.id)); return flowData.graph.edges .filter(edge => { // 只显示连接已显示节点的边 return displayedNodeIds.has(edge.from) && displayedNodeIds.has(edge.to); }) .map((edge, index) => { const source = edge.from; const target = edge.to; const sourceInstance = nodeInstanceMap.get(source); const targetInstance = nodeInstanceMap.get(target); const sourceStatus = sourceInstance?.status || 'NOT_STARTED'; const targetStatus = targetInstance?.status || 'NOT_STARTED'; // 判断这条边是否在可达路径上 const isReachableEdge = reachableNodes.has(source) && reachableNodes.has(target); // 根据节点状态确定边的样式 let strokeColor = '#d1d5db'; // 默认灰色 let strokeWidth = 2; let animated = false; let opacity = 1; // 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线 if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) { strokeColor = '#10b981'; // 绿色 } // 源节点失败 = 红色 else if (sourceStatus === 'FAILED') { strokeColor = '#ef4444'; // 红色 } // 源节点运行中 = 蓝色动画 else if (sourceStatus === 'RUNNING') { strokeColor = '#3b82f6'; // 蓝色 animated = true; } // 源节点完成 + 目标节点未开始 else if (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED') { if (isReachableEdge && isRunning) { strokeColor = '#9ca3af'; // 浅灰色(即将执行) } else { strokeColor = '#d1d5db'; // 默认灰色 opacity = 0.3; // 不可达路径半透明 } } // 两端都未执行 else if (sourceStatus === 'NOT_STARTED' && targetStatus === 'NOT_STARTED') { opacity = isRunning && !isReachableEdge ? 0.3 : 0.5; } return { id: edge.id || `edge-${source}-${target}-${index}`, source, target, type: 'smoothstep', animated, style: { stroke: strokeColor, strokeWidth, opacity, strokeDasharray: (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED' && isReachableEdge) ? '5,5' : undefined, }, markerEnd: { type: 'arrowclosed', color: strokeColor, width: 20, height: 20, }, }; }); }, [flowData, nodeInstanceMap, flowNodes, reachableNodes]); // 获取部署状态信息 const deployStatusInfo = flowData ? (() => { const { icon: StatusIcon, color } = getStatusIcon(flowData.deployStatus); return { icon: StatusIcon, color, text: getStatusText(flowData.deployStatus), }; })() : null; return ( {flowData && ( #{flowData.deployRecordId} )} 部署流程图 {deployStatusInfo && ( {deployStatusInfo.text} )}
{loading ? (

加载流程图数据...

) : flowData ? ( <> {/* 左侧信息面板 */} {/* 右侧流程图可视化 */}
) : (

暂无流程图数据

)}
); };