676 lines
23 KiB
TypeScript
676 lines
23 KiB
TypeScript
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<any> = ({ 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 = (
|
||
<div
|
||
className={cn(
|
||
'px-4 py-3 rounded-md min-w-[160px] transition-all',
|
||
isNotStarted && 'border-2 border-dashed',
|
||
!isNotStarted && 'border-2 border-solid shadow-sm',
|
||
isRunning && 'animate-pulse',
|
||
isUnreachable && 'opacity-40' // 不可达节点半透明
|
||
)}
|
||
style={{
|
||
borderColor: statusColor,
|
||
backgroundColor: isNotStarted ? '#f9fafb' : '#ffffff',
|
||
}}
|
||
>
|
||
{/* 节点名称 */}
|
||
<div className="font-medium text-sm mb-1">{nodeName}</div>
|
||
|
||
{/* 节点状态 */}
|
||
<div
|
||
className="text-xs font-medium mb-1"
|
||
style={{ color: statusColor }}
|
||
>
|
||
{getNodeStatusText(status)}
|
||
</div>
|
||
|
||
{/* 时间信息 */}
|
||
{!isNotStarted && (
|
||
<div className="text-xs text-muted-foreground space-y-0.5">
|
||
{startTime && <div>开始: {formatTime(startTime)}</div>}
|
||
{endTime && <div>结束: {formatTime(endTime)}</div>}
|
||
{displayDuration && (
|
||
<div className="font-medium">
|
||
{isRunning ? '运行中: ' : '时长: '}
|
||
{displayDuration}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 错误提示 */}
|
||
{hasFailed && errorMessage && (
|
||
<div className="mt-2 flex items-center gap-1 text-red-600">
|
||
<AlertCircle className="h-3 w-3" />
|
||
<span className="text-xs">有错误</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
{/* 输入连接点 */}
|
||
{nodeType !== 'START_EVENT' && (
|
||
<Handle type="target" position={Position.Left} />
|
||
)}
|
||
|
||
{/* 节点内容 - 如果有错误信息,包装在 Tooltip 中 */}
|
||
{hasFailed && errorMessage ? (
|
||
<TooltipProvider>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
{nodeContent}
|
||
</TooltipTrigger>
|
||
<TooltipContent side="top" className="max-w-sm">
|
||
<p className="font-medium mb-1">错误信息:</p>
|
||
<p className="text-xs">{errorMessage}</p>
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</TooltipProvider>
|
||
) : (
|
||
nodeContent
|
||
)}
|
||
|
||
{/* 输出连接点 */}
|
||
{nodeType !== 'END_EVENT' && (
|
||
<Handle type="source" position={Position.Right} />
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
const nodeTypes = {
|
||
custom: CustomFlowNode,
|
||
};
|
||
|
||
/**
|
||
* 业务信息卡片
|
||
*/
|
||
const BusinessInfoCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => {
|
||
return (
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||
<Building2 className="h-4 w-4" />
|
||
业务信息
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">应用名称:</span>
|
||
<span className="font-medium">{flowData.applicationName}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">应用编码:</span>
|
||
<span className="font-mono text-xs">{flowData.applicationCode}</span>
|
||
</div>
|
||
<Separator />
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">部署环境:</span>
|
||
<span className="font-medium">{flowData.environmentName}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">所属团队:</span>
|
||
<span className="font-medium">{flowData.teamName}</span>
|
||
</div>
|
||
<Separator />
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground flex items-center gap-1">
|
||
<User className="h-3 w-3" />
|
||
部署人:
|
||
</span>
|
||
<span className="font-medium">{flowData.deployBy}</span>
|
||
</div>
|
||
{flowData.deployRemark && (
|
||
<>
|
||
<Separator />
|
||
<div>
|
||
<span className="text-muted-foreground">备注:</span>
|
||
<p className="text-xs mt-1 text-muted-foreground">{flowData.deployRemark}</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 进度信息卡片
|
||
*/
|
||
const ProgressCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => {
|
||
const { executedNodeCount, totalNodeCount, successNodeCount, failedNodeCount, runningNodeCount } = flowData;
|
||
const progress = totalNodeCount > 0 ? (executedNodeCount / totalNodeCount) * 100 : 0;
|
||
|
||
return (
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||
<Layers className="h-4 w-4" />
|
||
执行进度
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<div>
|
||
<div className="flex justify-between text-sm mb-2">
|
||
<span className="text-muted-foreground">已执行</span>
|
||
<span className="font-medium">
|
||
{executedNodeCount} / {totalNodeCount} 节点
|
||
</span>
|
||
</div>
|
||
<Progress value={progress} className="h-2" />
|
||
<div className="text-xs text-muted-foreground mt-1 text-right">
|
||
{Math.round(progress)}%
|
||
</div>
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||
<div className="flex flex-col items-center p-2 rounded-md bg-green-50">
|
||
<CheckCircle2 className="h-4 w-4 text-green-600 mb-1" />
|
||
<span className="text-muted-foreground">成功</span>
|
||
<span className="font-medium text-green-600">{successNodeCount}</span>
|
||
</div>
|
||
<div className="flex flex-col items-center p-2 rounded-md bg-red-50">
|
||
<XCircle className="h-4 w-4 text-red-600 mb-1" />
|
||
<span className="text-muted-foreground">失败</span>
|
||
<span className="font-medium text-red-600">{failedNodeCount}</span>
|
||
</div>
|
||
<div className="flex flex-col items-center p-2 rounded-md bg-blue-50">
|
||
<Loader2 className="h-4 w-4 text-blue-600 mb-1" />
|
||
<span className="text-muted-foreground">运行中</span>
|
||
<span className="font-medium text-blue-600">{runningNodeCount}</span>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 时长统计卡片
|
||
*/
|
||
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 (
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||
<Clock className="h-4 w-4" />
|
||
时长统计
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground flex items-center gap-1">
|
||
<Calendar className="h-3 w-3" />
|
||
开始时间:
|
||
</span>
|
||
<span className="font-medium">
|
||
{deployStartTime ? formatTime(deployStartTime) : '-'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-muted-foreground">总时长:</span>
|
||
<span className="font-medium">
|
||
{formatDuration(currentDuration)}
|
||
{isRunning && <span className="text-blue-600 ml-1">(运行中...)</span>}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
<div>
|
||
<div className="text-xs text-muted-foreground mb-2">节点执行时长:</div>
|
||
<div className="space-y-1.5 text-xs max-h-32 overflow-y-auto">
|
||
{nodeInstances
|
||
.filter(ni => ni.status !== 'NOT_STARTED')
|
||
.map((ni) => {
|
||
// 后端返回的 duration 单位已经是毫秒
|
||
const nodeDuration = ni.duration
|
||
? ni.duration
|
||
: (ni.status === 'RUNNING' ? calculateRunningDuration(ni.startTime) : 0);
|
||
return (
|
||
<div key={ni.nodeId} className="flex justify-between items-center">
|
||
<span className="truncate flex-1" title={ni.nodeName}>
|
||
{ni.nodeName}
|
||
</span>
|
||
<span className="font-mono ml-2" style={{ color: getNodeStatusColor(ni.status) }}>
|
||
{formatDuration(nodeDuration)}
|
||
{ni.status === 'RUNNING' && '...'}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 实时监控提示
|
||
*/
|
||
const MonitoringAlert: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => {
|
||
const { runningNodeCount } = flowData;
|
||
|
||
if (runningNodeCount === 0) return null;
|
||
|
||
return (
|
||
<Alert className="border-blue-200 bg-blue-50">
|
||
<Loader2 className="h-4 w-4 animate-spin text-blue-600" />
|
||
<AlertDescription className="text-sm text-blue-900">
|
||
<span className="font-medium">部署正在进行中</span>
|
||
<span className="block text-xs mt-1">
|
||
当前有 {runningNodeCount} 个节点正在执行,请稍候...
|
||
</span>
|
||
</AlertDescription>
|
||
</Alert>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 信息面板(左侧边栏)
|
||
*/
|
||
const DeployInfoPanel: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => {
|
||
return (
|
||
<div className="w-80 flex-shrink-0 border-r bg-muted/30 p-4 space-y-4 overflow-y-auto">
|
||
<MonitoringAlert flowData={flowData} />
|
||
<BusinessInfoCard flowData={flowData} />
|
||
<ProgressCard flowData={flowData} />
|
||
<DurationCard flowData={flowData} />
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 部署流程图模态框(主组件)
|
||
*/
|
||
export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
||
open,
|
||
deployRecordId,
|
||
onOpenChange,
|
||
}) => {
|
||
const [loading, setLoading] = useState(false);
|
||
const [flowData, setFlowData] = useState<DeployRecordFlowGraph | null>(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<string, WorkflowNodeInstance>();
|
||
const map = new Map<string, WorkflowNodeInstance>();
|
||
flowData.nodeInstances.forEach((instance) => {
|
||
map.set(instance.nodeId, instance);
|
||
});
|
||
return map;
|
||
}, [flowData]);
|
||
|
||
// 计算可达节点(从已执行节点出发能到达的节点)
|
||
const reachableNodes = useMemo(() => {
|
||
if (!flowData?.graph?.edges || !flowData?.nodeInstances) return new Set<string>();
|
||
|
||
const executedNodeIds = flowData.nodeInstances
|
||
.filter(ni => ni.status !== 'NOT_STARTED')
|
||
.map(ni => ni.nodeId);
|
||
|
||
if (executedNodeIds.length === 0) return new Set<string>();
|
||
|
||
const reachable = new Set<string>(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<string, { x: number; y: number }>();
|
||
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 (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="!max-w-7xl w-[90vw] h-[85vh] flex flex-col p-0 overflow-hidden">
|
||
<DialogHeader className="px-6 pt-6 pb-4 border-b flex-shrink-0">
|
||
<DialogTitle className="flex items-center gap-2">
|
||
{flowData && (
|
||
<span className="text-muted-foreground">
|
||
#{flowData.deployRecordId}
|
||
</span>
|
||
)}
|
||
<span>部署流程图</span>
|
||
{deployStatusInfo && (
|
||
<Badge
|
||
variant="outline"
|
||
className={cn('flex items-center gap-1', deployStatusInfo.color)}
|
||
>
|
||
<deployStatusInfo.icon
|
||
className={cn(
|
||
'h-3 w-3',
|
||
flowData?.deployStatus === 'RUNNING' && 'animate-spin'
|
||
)}
|
||
/>
|
||
{deployStatusInfo.text}
|
||
</Badge>
|
||
)}
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
<div className="flex flex-1 overflow-hidden min-h-0">
|
||
{loading ? (
|
||
<div className="flex items-center justify-center h-full w-full">
|
||
<div className="text-center space-y-4">
|
||
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
|
||
<p className="text-sm text-muted-foreground">加载流程图数据...</p>
|
||
</div>
|
||
</div>
|
||
) : flowData ? (
|
||
<>
|
||
{/* 左侧信息面板 */}
|
||
<DeployInfoPanel flowData={flowData} />
|
||
|
||
{/* 右侧流程图可视化 */}
|
||
<div className="flex-1 relative">
|
||
<ReactFlowProvider>
|
||
<ReactFlow
|
||
nodes={flowNodes}
|
||
edges={flowEdges}
|
||
nodeTypes={nodeTypes}
|
||
fitView
|
||
className="bg-muted/10"
|
||
fitViewOptions={{ padding: 0.2, maxZoom: 1, minZoom: 0.5 }}
|
||
panOnScroll={true}
|
||
zoomOnScroll={true}
|
||
zoomOnPinch={true}
|
||
preventScrolling={false}
|
||
>
|
||
<Background
|
||
variant={BackgroundVariant.Dots}
|
||
gap={16}
|
||
size={1}
|
||
className="opacity-30"
|
||
/>
|
||
</ReactFlow>
|
||
</ReactFlowProvider>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="flex items-center justify-center h-full w-full">
|
||
<div className="text-center">
|
||
<AlertTriangle className="h-12 w-12 text-muted-foreground mx-auto mb-2" />
|
||
<p className="text-sm text-muted-foreground">暂无流程图数据</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|