deploy-ease-platform/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx
2025-11-14 18:08:31 +08:00

260 lines
8.2 KiB
TypeScript

import React, { useCallback, useEffect } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
addEdge,
Connection,
Edge,
BackgroundVariant,
Panel,
reconnectEdge
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import type { FlowNode, FlowEdge } from '../types';
import { nodeTypes } from '../nodes';
import SmartEdge from './SmartEdge';
import { generateEdgeId } from '../utils/idGenerator';
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 onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => {
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
}, [setEdges]);
// 处理连接
const onConnect = useCallback(
(params: Connection | Edge) => {
const newEdge: FlowEdge = {
id: generateEdgeId(), // ✅ 生成格式: eid_xxxxxxxx_xxxx_xxxx_xxxx_xxxxxxxxxxxx
source: params.source!,
target: params.target!,
type: 'smoothstep',
animated: false,
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);
}, [onNodesStateChange]);
// 边变化处理
const handleEdgesChange = useCallback((changes: any) => {
onEdgesStateChange(changes);
}, [onEdgesStateChange]);
// 使用 useEffect 监听 nodes 变化并通知父组件
useEffect(() => {
if (onNodesChange && nodes.length > 0) {
onNodesChange(nodes as FlowNode[]);
}
}, [nodes, onNodesChange]);
// 使用 useEffect 监听 edges 变化并通知父组件
useEffect(() => {
if (onEdgesChange) {
onEdgesChange(edges as FlowEdge[]);
}
}, [edges, onEdgesChange]);
// 连接验证 - 利用 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}
onReconnect={onReconnect}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onDrop={handleDrop}
onDragOver={handleDragOver}
onMove={onViewportChange}
nodeTypes={nodeTypes}
edgeTypes={{ smoothstep: SmartEdge }}
isValidConnection={isValidConnection}
defaultEdgeOptions={{
type: 'smoothstep',
animated: false,
style: { stroke: '#94a3b8', strokeWidth: 2 },
markerEnd: { type: 'arrowclosed', color: '#94a3b8' },
}}
edgesReconnectable={true}
reconnectRadius={20}
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;