deploy-ease-platform/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx
dengqichen 3b67370c2e 1
2025-10-21 15:41:43 +08:00

249 lines
7.9 KiB
TypeScript

import React, { useCallback } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
addEdge,
Connection,
Edge,
BackgroundVariant,
Panel
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { v4 as uuidv4 } from 'uuid';
import type { FlowNode, FlowEdge } from '../types';
import { nodeTypes } from '../nodes';
import CustomEdge from './CustomEdge';
interface FlowCanvasProps {
initialNodes?: FlowNode[];
initialEdges?: FlowEdge[];
onNodesChange?: (nodes: FlowNode[]) => void;
onEdgesChange?: (edges: FlowEdge[]) => void;
onNodeClick?: (event: React.MouseEvent, node: any) => void;
onEdgeClick?: (event: React.MouseEvent, edge: any) => void;
onDrop?: (event: React.DragEvent) => void;
onDragOver?: (event: React.DragEvent) => void;
onViewportChange?: () => void;
className?: string;
}
const FlowCanvas: React.FC<FlowCanvasProps> = ({
initialNodes = [],
initialEdges = [],
onNodesChange,
onEdgesChange,
onNodeClick,
onEdgeClick,
onDrop,
onDragOver,
onViewportChange,
className = ''
}) => {
const [nodes, , onNodesStateChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesStateChange] = useEdgesState(initialEdges);
// 处理连接
const onConnect = useCallback(
(params: Connection | Edge) => {
const newEdge: FlowEdge = {
id: uuidv4(), // 使用 UUID 生成唯一 ID
source: params.source!,
target: params.target!,
type: 'smoothstep',
animated: true,
style: {
stroke: '#94a3b8',
strokeWidth: 2,
},
markerEnd: {
type: 'arrowclosed' as const,
color: '#94a3b8',
},
data: {
label: '',
condition: {
type: 'DEFAULT' as const,
priority: 10
}
}
};
setEdges((eds) => addEdge(newEdge, eds));
},
[setEdges]
);
// 节点变化处理
const handleNodesChange = useCallback((changes: any) => {
onNodesStateChange(changes);
if (onNodesChange) {
// 延迟获取最新状态 - 在实际项目中可以使用useEffect监听nodes变化
setTimeout(() => {
onNodesChange(nodes as FlowNode[]);
}, 0);
}
}, [onNodesStateChange, onNodesChange, nodes]);
// 边变化处理
const handleEdgesChange = useCallback((changes: any) => {
onEdgesStateChange(changes);
if (onEdgesChange) {
// 延迟获取最新状态 - 在实际项目中可以使用useEffect监听edges变化
setTimeout(() => {
onEdgesChange(edges as FlowEdge[]);
}, 0);
}
}, [onEdgesStateChange, onEdgesChange, edges]);
// 连接验证 - 利用 React Flow 的实时验证功能
const isValidConnection = useCallback((connection: Connection | Edge) => {
// 1. 防止自连接
if (connection.source === connection.target) {
return false;
}
// 2. 检查是否已存在连接(防止重复)
const isDuplicate = edges.some(
(edge) =>
edge.source === connection.source &&
edge.target === connection.target
);
if (isDuplicate) {
return false;
}
// 3. 获取源节点和目标节点
const sourceNode = nodes.find(n => n.id === connection.source);
const targetNode = nodes.find(n => n.id === connection.target);
if (!sourceNode || !targetNode) {
return false;
}
// 4. 开始节点不能作为连接目标
if (targetNode.type === 'START_EVENT') {
return false;
}
// 5. 结束节点不能作为连接源
if (sourceNode.type === 'END_EVENT') {
return false;
}
return true;
}, [edges, nodes]);
// 处理拖拽放置
const handleDrop = useCallback((event: React.DragEvent) => {
event.preventDefault();
if (onDrop) {
onDrop(event);
}
}, [onDrop]);
// 处理拖拽悬停
const handleDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
if (onDragOver) {
onDragOver(event);
}
}, [onDragOver]);
return (
<div className={`flow-canvas ${className}`} style={{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onDrop={handleDrop}
onDragOver={handleDragOver}
onMove={onViewportChange}
nodeTypes={nodeTypes}
edgeTypes={{ smoothstep: CustomEdge }}
isValidConnection={isValidConnection}
defaultEdgeOptions={{
type: 'smoothstep',
animated: false,
style: { stroke: '#94a3b8', strokeWidth: 2 },
markerEnd: { type: 'arrowclosed', color: '#94a3b8' },
}}
fitView
fitViewOptions={{
padding: 0.1,
includeHiddenNodes: false,
minZoom: 1.0,
maxZoom: 1.0,
duration: 800,
}}
minZoom={0.1}
maxZoom={4}
deleteKeyCode={['Backspace', 'Delete']}
multiSelectionKeyCode={['Meta', 'Ctrl']}
panOnScroll
selectionOnDrag
panOnDrag={[1, 2]}
selectNodesOnDrag={false}
snapToGrid
snapGrid={[15, 15]}
connectionLineStyle={{ stroke: '#3b82f6', strokeWidth: 3, strokeDasharray: '5,5' }}
connectionLineType={'smoothstep' as any}
connectOnClick={false}
elevateEdgesOnSelect
style={{ width: '100%', height: '100%' }}
>
<Background
variant={BackgroundVariant.Dots}
gap={20}
size={1}
color="#e5e7eb"
/>
<Controls
position="bottom-right"
showZoom
showFitView
showInteractive
/>
<MiniMap
position="bottom-left"
nodeColor="#3b82f6"
maskColor="rgba(0, 0, 0, 0.2)"
style={{
height: 120,
width: 200,
backgroundColor: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: '8px',
}}
/>
{/* 状态面板 */}
<Panel position="top-left">
<div style={{
background: 'white',
padding: '8px 12px',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
fontSize: '12px',
color: '#6b7280'
}}>
: {nodes.length}, : {edges.length}
</div>
</Panel>
</ReactFlow>
</div>
);
};
export default FlowCanvas;