251 lines
7.9 KiB
TypeScript
251 lines
7.9 KiB
TypeScript
import React, { useCallback, useEffect } from 'react';
|
|
import {
|
|
ReactFlow,
|
|
Background,
|
|
Controls,
|
|
MiniMap,
|
|
useNodesState,
|
|
useEdgesState,
|
|
addEdge,
|
|
Connection,
|
|
Edge,
|
|
BackgroundVariant,
|
|
Panel
|
|
} from '@xyflow/react';
|
|
import '@xyflow/react/dist/style.css';
|
|
|
|
import type { FlowNode, FlowEdge } from '../types';
|
|
import { nodeTypes } from '../nodes';
|
|
import CustomEdge from './CustomEdge';
|
|
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 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: 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);
|
|
}, [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}
|
|
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;
|