You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1150 lines
34 KiB
TypeScript

import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
ReactFlow,
applyNodeChanges,
applyEdgeChanges,
addEdge,
reconnectEdge,
Background,
Controls,
Node,
Edge,
ReactFlowProvider,
useReactFlow,
EdgeTypes,
SelectionMode,
useStoreApi,
Panel,
ConnectionLineType
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { useSelector, useDispatch } from 'react-redux';
import { updateCanvasDataMap } from '@/store/ideContainer';
import { debounce } from 'lodash';
import { nodeTypeMap, nodeTypes, registerNodeType } from '@/components/FlowEditor/node';
import SideBar from './sideBar/sideBar';
import { convertFlowData, revertFlowData } from '@/utils/convertFlowData';
import { exampleFlowData } from '@/pages/flowEditor/test/exampleFlowData';
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
import CustomEdge from './components/customEdge';
import CustomConnectionLine from './components/customConnectionLine';
import NodeContextMenu from './components/nodeContextMenu';
import EdgeContextMenu from './components/edgeContextMenu';
import PaneContextMenu from './components/paneContextMenu';
import NodeEditModal from './components/nodeEditModal';
import AddNodeMenu from './components/addNodeMenu';
import ActionBar from './components/actionBar';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
import { setMainFlow } from '@/api/appRes';
import { getUserToken } from '@/api/user';
import { Message } from '@arco-design/web-react';
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
import { HistoryProvider, useHistory } from './components/historyContext';
import useWebSocket from '@/hooks/useWebSocket';
const edgeTypes: EdgeTypes = {
custom: CustomEdge
};
const FlowEditorWithProvider: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ initialData, useDefault }) => {
return (
<div style={{ width: '100%', height: '91vh', display: 'flex' }} onContextMenu={(e) => e.preventDefault()}>
<ReactFlowProvider>
{/*<SideBar />*/}
<FlowEditor initialData={initialData} useDefault={useDefault} />
</ReactFlowProvider>
</div>
);
};
const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ initialData, useDefault }) => {
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
const { canvasDataMap } = useSelector(state => state.ideContainer);
const reactFlowInstance = useReactFlow();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [menu, setMenu] = useState<{
id: string;
type: 'node' | 'edge' | 'pane';
top: number;
left: number;
position?: { x: number; y: number };
} | null>(null);
const store = useStoreApi();
const dispatch = useDispatch();
// 添加编辑弹窗相关状态
const [editingNode, setEditingNode] = useState<Node | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDelete, setIsDelete] = useState(false);
// 添加节点选择弹窗状态
const [edgeForNodeAdd, setEdgeForNodeAdd] = useState<Edge | null>(null);
const [positionForNodeAdd, setPositionForNodeAdd] = useState<{ x: number, y: number } | null>(null);
const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines();
const updateCanvasDataMapDebounced = useRef(
debounce((dispatch, canvasDataMap, id, nodes, edges) => {
dispatch(updateCanvasDataMap({
...canvasDataMap,
[id]: { nodes, edges }
}));
}, 500)
).current;
// 获取handle类型 (api或data)
const getHandleType = (handleId: string, nodeParams: any) => {
// 检查是否为api类型的handle
const apiOuts = nodeParams.apiOuts || [];
const apiIns = nodeParams.apiIns || [];
if (apiOuts.some((api: any) => (api.name || api.id) === handleId) ||
apiIns.some((api: any) => (api.name || api.id) === handleId)) {
return 'api';
}
// 检查是否为data类型的handle
const dataOuts = nodeParams.dataOuts || [];
const dataIns = nodeParams.dataIns || [];
if (dataOuts.some((data: any) => (data.name || data.id) === handleId) ||
dataIns.some((data: any) => (data.name || data.id) === handleId)) {
return 'data';
}
// 默认为data类型
return 'data';
};
// 验证数据类型是否匹配
const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeTypes, sourceHandleId: string, targetHandleId: string) => {
const sourceParams = sourceNode.data?.parameters || {};
const targetParams = targetNode.data?.parameters || {};
// 获取源节点的输出参数
let sourceDataType = '';
const sourceApiOuts = sourceParams.apiOuts || [];
const sourceDataOuts = sourceParams.dataOuts || [];
// 查找源handle的数据类型
const sourceApi = sourceApiOuts.find((api: any) => api.name === sourceHandleId);
const sourceData = sourceDataOuts.find((data: any) => data.name === sourceHandleId);
if (sourceApi) {
sourceDataType = sourceApi.dataType || '';
}
else if (sourceData) {
sourceDataType = sourceData.dataType || '';
}
// 获取目标节点的输入参数
let targetDataType = '';
const targetApiIns = targetParams.apiIns || [];
const targetDataIns = targetParams.dataIns || [];
// 查找目标handle的数据类型
const targetApi = targetApiIns.find((api: any) => api.name === targetHandleId);
const targetData = targetDataIns.find((data: any) => data.name === targetHandleId);
if (targetApi) {
targetDataType = targetApi.dataType || '';
}
else if (targetData) {
targetDataType = targetData.dataType || '';
}
// 如果任一数据类型为空,则允许连接
if (!sourceDataType || !targetDataType) {
return true;
}
// 比较数据类型是否匹配
return sourceDataType === targetDataType;
};
// 在组件顶部添加历史记录相关状态
const [historyInitialized, setHistoryInitialized] = useState(false);
const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// 修改 onNodesChange 函数,添加防抖机制
const onNodesChange = useCallback(
(changes: any) => {
const newNodes = applyNodeChanges(changes, nodes);
setNodes(newNodes);
// 如果需要在节点变化时执行某些操作,可以在这里添加
onPaneClick();
// 只有当变化是节点位置变化时才不立即记录历史
const isPositionChange = changes.some((change: any) =>
change.type === 'position' && change.dragging === false
);
// 如果是位置变化结束或者不是位置变化,则记录历史
if (isPositionChange || !changes.some((change: any) => change.type === 'position')) {
// 清除之前的定时器
if (historyTimeoutRef.current) {
clearTimeout(historyTimeoutRef.current);
}
// 设置新的定时器,延迟记录历史记录
historyTimeoutRef.current = setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...newNodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, 100);
}
},
[nodes, edges]
);
// 修改 onEdgesChange 函数
const onEdgesChange = useCallback(
(changes: any) => {
const newEdges = applyEdgeChanges(changes, edges);
setEdges(newEdges);
// 如果需要在边变化时执行某些操作,可以在这里添加
onPaneClick();
// 边的变化立即记录历史
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...nodes], edges: [...newEdges] }
});
document.dispatchEvent(event);
},
[edges, nodes]
);
const onNodesDelete = useCallback((deletedNodes) => {
setIsDelete(true);
closeEditModal();
}, []);
// 修改 onConnect 函数
const onConnect = useCallback(
(params: any) => {
// 获取源节点和目标节点
const sourceNode = nodes.find(node => node.id === params.source);
const targetNode = nodes.find(node => node.id === params.target);
// 如果找不到节点,不创建连接
if (!sourceNode || !targetNode) {
return;
}
// 获取源节点和目标节点的参数信息
const sourceParams = sourceNode.data?.parameters || {};
const targetParams = targetNode.data?.parameters || {};
console.log(sourceParams, targetParams, params);
// 获取源handle和目标handle的类型 (api或data)
const sourceHandleType = getHandleType(params.sourceHandle, sourceParams);
const targetHandleType = getHandleType(params.targetHandle, targetParams);
// 验证连接类型是否匹配 (api只能连api, data只能连data)
if (sourceHandleType !== targetHandleType) {
console.warn('连接类型不匹配: ', sourceHandleType, targetHandleType);
return;
}
// 验证数据类型是否匹配
if (!validateDataType(sourceNode, targetNode, params.sourceHandle, params.targetHandle)) {
console.warn('数据类型不匹配');
return;
}
// TODO 目前是临时定义,存在事件数据的时候才展示
// params.data = {
// displayData: {
// label: '测试',
// value: '数据展示'
// }
// };
// 如果验证通过,创建连接
setEdges((edgesSnapshot) => {
const newEdges = addEdge({ ...params, type: 'custom' }, edgesSnapshot);
// 连接建立后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...nodes], edges: [...newEdges] }
});
document.dispatchEvent(event);
}, 0);
return newEdges;
});
},
[nodes]
);
// 边重新连接处理
const onReconnect = useCallback(
(oldEdge: Edge, newConnection: any) => {
// 获取源节点和目标节点
const sourceNode = nodes.find(node => node.id === newConnection.source);
const targetNode = nodes.find(node => node.id === newConnection.target);
// 如果找不到节点,不创建连接
if (!sourceNode || !targetNode) {
return;
}
// 获取源节点和目标节点的参数信息
const sourceParams = sourceNode.data?.parameters || {};
const targetParams = targetNode.data?.parameters || {};
// 获取源handle和目标handle的类型 (api或data)
const sourceHandleType = getHandleType(newConnection.sourceHandle, sourceParams);
const targetHandleType = getHandleType(newConnection.targetHandle, targetParams);
// 验证连接类型是否匹配 (api只能连api, data只能连data)
if (sourceHandleType !== targetHandleType) {
console.warn('连接类型不匹配: ', sourceHandleType, targetHandleType);
return;
}
// 验证数据类型是否匹配
if (!validateDataType(sourceNode, targetNode, newConnection.sourceHandle, newConnection.targetHandle)) {
console.warn('数据类型不匹配');
return;
}
// 如果验证通过,重新连接
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
},
[nodes]
);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
// 侧边栏节点实例
// 修改 onDrop 函数
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
if (!reactFlowInstance) return;
const callBack = event.dataTransfer.getData('application/reactflow');
const nodeData = JSON.parse(callBack);
if (typeof nodeData.nodeType === 'undefined' || !nodeData.nodeType) {
return;
}
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY
});
const newNode = {
id: `${nodeData.nodeType}-${Date.now()}`,
type: nodeData.nodeType,
position,
data: { ...nodeData.data, title: nodeData.nodeName, type: nodeData.nodeType }
};
// 将未定义的节点动态追加进nodeTypes
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
// 目前默认添加的都是系统组件/本地组件
if (!nodeMap.includes(nodeData.nodeType)) registerNodeType(nodeData.nodeType, LocalNode, nodeData.nodeName);
setNodes((nds) => {
const newNodes = nds.concat(newNode);
// 添加节点后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...newNodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, 0);
return newNodes;
});
},
[reactFlowInstance, edges]
);
const onNodeDrag = useCallback(
(_: any, node: Node) => {
// 获取对齐线
getGuidelines(node, nodes);
},
[nodes, getGuidelines]
);
// 节点拖拽结束处理
const onNodeDragStop = useCallback(() => {
// 清除对齐线
clearGuidelines();
}, [clearGuidelines]);
useEffect(() => {
if (canvasDataMap[initialData?.id]) {
const { edges, nodes } = canvasDataMap[initialData?.id];
setNodes(nodes);
setEdges(edges);
}
else {
// 首次进入
const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(initialData, useDefault);
// 为所有边添加类型-
const initialEdges: Edge[] = convertedEdges.map(edge => ({
...edge,
type: 'custom'
}));
setNodes(convertedNodes);
setEdges(initialEdges);
if (initialData?.id) {
dispatch(updateCanvasDataMap({
...canvasDataMap,
[initialData.id]: { convertedNodes, initialEdges }
}));
}
}
// 标记历史记录已初始化
setHistoryInitialized(true);
}, [initialData]);
// 实时更新 canvasDataMap
useEffect(() => {
if (initialData?.id) {
updateCanvasDataMapDebounced(dispatch, canvasDataMap, initialData.id, nodes, edges);
}
// 清理函数,在组件卸载时取消防抖
return () => {
updateCanvasDataMapDebounced.cancel();
};
}, [nodes, edges, initialData?.id, dispatch, canvasDataMap]);
// 监听边的变化,处理添加节点的触发
useEffect(() => {
const edgeToAddNode = edges.find(edge => edge.data?.addNodeTrigger);
const pane = reactFlowWrapper.current?.getBoundingClientRect();
if (edgeToAddNode) {
edgeToAddNode.data.y = (edgeToAddNode.data.clientY as number) - pane.top;
edgeToAddNode.data.x = (edgeToAddNode.data.clientX as number) - pane.left;
setEdgeForNodeAdd(edgeToAddNode);
// 清除触发标志
setEdges(eds => eds.map(edge => {
if (edge.id === edgeToAddNode.id) {
const { addNodeTrigger, ...restData } = edge.data || {};
return {
...edge,
data: restData
};
}
return edge;
}));
}
}, [edges]);
// 节点右键菜单处理
const onNodeContextMenu = useCallback(
(event: React.MouseEvent, node: Node) => {
event.preventDefault();
const pane = reactFlowWrapper.current?.getBoundingClientRect();
if (!pane) return;
setMenu({
id: node.id,
type: 'node',
top: event.clientY - pane.top,
left: event.clientX - pane.left
});
},
[setMenu]
);
// 节点双击处理
const onNodeDoubleClick = useCallback(
(event: React.MouseEvent, node: Node) => {
// 不可编辑的类型
if (['AND', 'OR', 'JSON2STR', 'STR2JSON'].includes(node.type)) return;
setEditingNode(node);
setIsEditModalOpen(true);
},
[]
);
// 边右键菜单处理
const onEdgeContextMenu = useCallback(
(event: React.MouseEvent, edge: Edge) => {
event.preventDefault();
const pane = reactFlowWrapper.current?.getBoundingClientRect();
if (!pane) return;
setMenu({
id: edge.id,
type: 'edge',
top: event.clientY - pane.top,
left: event.clientX - pane.left
});
},
[setMenu]
);
// 画布右键菜单处理
const onPaneContextMenu = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
const pane = reactFlowWrapper.current?.getBoundingClientRect();
if (!pane || !reactFlowInstance) return;
// 计算在画布中的位置
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY
});
setMenu({
id: 'pane-context-menu',
type: 'pane',
top: event.clientY - pane.top,
left: event.clientX - pane.left,
position
});
},
[reactFlowInstance]
);
// 点击画布其他区域关闭菜单
const onPaneClick = useCallback(() => {
setMenu(null);
// 关闭添加节点菜单
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
}, [setMenu]);
// 关闭编辑弹窗
const closeEditModal = useCallback(() => {
setIsEditModalOpen(false);
setEditingNode(null);
}, []);
// 保存节点编辑
const saveNodeEdit = useCallback((updatedData: any) => {
console.log('updatedData:', updatedData);
const updatedNodes = nodes.map((node) => {
if (node.id === editingNode?.id) {
return {
...node,
data: { ...node.data, ...updatedData }
};
}
return node;
});
setNodes(updatedNodes);
closeEditModal();
// TODO 如果需要在节点编辑后立即保存到服务器,可以调用保存函数
// saveFlowDataToServer();
}, [nodes, editingNode, closeEditModal]);
// 修改删除节点函数
const deleteNode = useCallback((node: Node) => {
setNodes((nds) => nds.filter((n) => n.id !== node.id));
setEdges((eds) => eds.filter((e) => e.source !== node.id && e.target !== node.id));
setMenu(null);
// 删除节点后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: {
nodes: [...nodes.filter((n) => n.id !== node.id)],
edges: [...edges.filter((e) => e.source !== node.id && e.target !== node.id)]
}
});
document.dispatchEvent(event);
}, 0);
}, [nodes, edges]);
// 修改删除边函数
const deleteEdge = useCallback((edge: Edge) => {
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
setMenu(null);
// 删除边后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: {
nodes: [...nodes],
edges: [...edges.filter((e) => e.id !== edge.id)]
}
});
document.dispatchEvent(event);
}, 0);
}, [nodes, edges]);
// 编辑节点
const editNode = useCallback((node: Node) => {
setMenu(null);
setEditingNode(node);
setIsEditModalOpen(true);
}, []);
// 编辑边
const editEdge = useCallback((edge: Edge) => {
// 这里可以实现边编辑逻辑
console.log('编辑边:', edge);
setMenu(null);
}, []);
// 复制节点
const copyNode = useCallback((node: Node) => {
// 这里可以实现节点复制逻辑
console.log('复制节点:', node);
setMenu(null);
}, []);
// 在边上添加节点的具体实现
// 修改 addNodeOnEdge 函数
const addNodeOnEdge = useCallback((nodeType: string, node: any) => {
if (!edgeForNodeAdd || !reactFlowInstance) return;
// 查找节点定义
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType) || node;
if (!nodeDefinition) return;
// 获取源节点和目标节点
const sourceNode = nodes.find(n => n.id === edgeForNodeAdd.source);
const targetNode = nodes.find(n => n.id === edgeForNodeAdd.target);
if (!sourceNode || !targetNode) return;
// 计算中点位置
const position = {
x: (sourceNode.position.x + targetNode.position.x) / 2,
y: (sourceNode.position.y + targetNode.position.y) / 2
};
// 创建新节点
const newNode = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
...nodeDefinition.data,
title: nodeDefinition.nodeName,
type: nodeType
}
};
// 将未定义的节点动态追加进nodeTypes
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, nodeType === 'BASIC' ? BasicNode : LocalNode, nodeDefinition.nodeName);
// 添加新节点
setNodes((nds) => [...nds, newNode]);
// 删除旧边
setEdges((eds) => eds.filter(e => e.id !== edgeForNodeAdd.id));
// 创建新边: source -> new node, new node -> target
const newEdges = [
...edges.filter(e => e.id !== edgeForNodeAdd.id),
{
id: `e${edgeForNodeAdd.source}-${newNode.id}`,
source: edgeForNodeAdd.source,
target: newNode.id,
type: 'custom'
},
{
id: `e${newNode.id}-${edgeForNodeAdd.target}`,
source: newNode.id,
target: edgeForNodeAdd.target,
type: 'custom'
}
];
setEdges(newEdges);
// 关闭菜单
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
// 添加节点后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: {
nodes: [...nodes, newNode],
edges: [...newEdges]
}
});
document.dispatchEvent(event);
}, 0);
}, [edgeForNodeAdd, nodes, reactFlowInstance, edges]);
// 在画布上添加节点
// 修改 addNodeOnPane 函数
const addNodeOnPane = useCallback((nodeType: string, position: { x: number; y: number }, node?: any) => {
setMenu(null);
if (!reactFlowInstance) return;
// 查找节点定义
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType) || node;
if (!nodeDefinition) return;
// 创建新节点
const newNode = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
...nodeDefinition.data,
title: nodeDefinition.nodeName,
type: nodeType
}
};
// 将未定义的节点动态追加进nodeTypes
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
// 目前默认添加的都是系统组件/本地组件
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, nodeType === 'BASIC' ? BasicNode : LocalNode, nodeDefinition.nodeName);
setNodes((nds) => {
const newNodes = [...nds, newNode];
// 添加节点后记录历史
setTimeout(() => {
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...newNodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, 0);
return newNodes;
});
}, [reactFlowInstance, edges]);
// 处理添加节点的统一方法
const handleAddNode = useCallback((nodeType: string, node) => {
// 如果是通过边添加节点
if (edgeForNodeAdd) {
addNodeOnEdge(nodeType, node);
}
// 如果是通过画布添加节点
else if (positionForNodeAdd) {
addNodeOnPane(nodeType, positionForNodeAdd, node);
}
// 清除状态
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
}, [edgeForNodeAdd, positionForNodeAdd, addNodeOnEdge, addNodeOnPane]);
// 保存所有节点和边数据到服务器
const saveFlowDataToServer = useCallback(async () => {
try {
// 转换会原始数据类型
const revertedData = revertFlowData(nodes, edges);
console.log('initialData:', initialData);
const res: any = await setMainFlow(revertedData, initialData.id);
if (res.code === 200) {
Message.success('保存成功');
}
else {
Message.error(res.message);
}
} catch (error) {
console.error('Error saving flow data:', error);
Message.error(error);
}
}, [nodes, edges]);
// 添加运行状态
const [isRunning, setIsRunning] = useState(false);
// 初始化WebSocket hook
const ws = useWebSocket({
onOpen: () => {
console.log('WebSocket连接已建立');
Message.success('运行已启动');
},
onClose: () => {
console.log('WebSocket连接已关闭');
setIsRunning(false);
Message.info('运行已停止');
},
onError: (event) => {
console.error('WebSocket错误:', event);
setIsRunning(false);
Message.error('运行连接出错');
},
onMessage: (event) => {
console.log('收到WebSocket消息:', event.data);
// 这里可以处理从后端收到的消息,例如日志更新等
}
});
// 修改运行处理函数
const handleRun = useCallback(async (running: boolean) => {
if (running) {
// 启动运行
const res = await getUserToken();
const token = res.data;
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
let wsApi = `${protocol}://${window.location.host}/ws/v1/bpms-runtime`;
if (window.location.host.includes('localhost')) {
// WS_API = `wss://${host}/ws/v1/bpms-runtime`;
wsApi = `ws://api.myserver.com:4121/ws/v1/bpms-runtime`;
}
const uri = `${wsApi}?x-auth0-token=${token}`;
ws.connect(uri);
setIsRunning(true);
}
else {
// 停止运行
ws.disconnect();
setIsRunning(false);
}
}, [initialData?.id, ws]);
if (!historyInitialized) {
return <div>Loading...</div>;
}
return (
<HistoryProvider
initialNodes={nodes}
initialEdges={edges}
onHistoryChange={(newNodes, newEdges) => {
setNodes(newNodes);
setEdges(newEdges);
}}
>
<FlowEditorContent
nodes={nodes}
edges={edges}
setNodes={setNodes}
setEdges={setEdges}
reactFlowInstance={reactFlowInstance}
reactFlowWrapper={reactFlowWrapper}
menu={menu}
setMenu={setMenu}
store={store}
dispatch={dispatch}
editingNode={editingNode}
setEditingNode={setEditingNode}
isEditModalOpen={isEditModalOpen}
setIsEditModalOpen={setIsEditModalOpen}
isDelete={isDelete}
setIsDelete={setIsDelete}
edgeForNodeAdd={edgeForNodeAdd}
setEdgeForNodeAdd={setEdgeForNodeAdd}
positionForNodeAdd={positionForNodeAdd}
setPositionForNodeAdd={setPositionForNodeAdd}
getGuidelines={getGuidelines}
clearGuidelines={clearGuidelines}
AlignmentGuides={AlignmentGuides}
updateCanvasDataMapDebounced={updateCanvasDataMapDebounced}
canvasDataMap={canvasDataMap}
initialData={initialData}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onReconnect={onReconnect}
onDragOver={onDragOver}
onDrop={onDrop}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
onNodeContextMenu={onNodeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
onEdgeContextMenu={onEdgeContextMenu}
onPaneContextMenu={onPaneContextMenu}
onPaneClick={onPaneClick}
closeEditModal={closeEditModal}
saveNodeEdit={saveNodeEdit}
deleteNode={deleteNode}
deleteEdge={deleteEdge}
editNode={editNode}
editEdge={editEdge}
copyNode={copyNode}
addNodeOnEdge={addNodeOnEdge}
addNodeOnPane={addNodeOnPane}
handleAddNode={handleAddNode}
saveFlowDataToServer={saveFlowDataToServer}
handleRun={handleRun}
isRunning={isRunning}
/>
</HistoryProvider>
);
};
// 创建一个新的组件来包含 ReactFlow 和其他 UI 元素
const FlowEditorContent: React.FC<any> = (props) => {
const {
nodes,
edges,
setNodes,
setEdges,
reactFlowInstance,
reactFlowWrapper,
menu,
setMenu,
editingNode,
setEditingNode,
isEditModalOpen,
setIsEditModalOpen,
isDelete,
setIsDelete,
edgeForNodeAdd,
setEdgeForNodeAdd,
positionForNodeAdd,
setPositionForNodeAdd,
getGuidelines,
clearGuidelines,
AlignmentGuides,
initialData,
onNodesChange,
onEdgesChange,
onConnect,
onReconnect,
onDragOver,
onDrop,
onNodeDrag,
onNodeDragStop,
onNodeContextMenu,
onNodeDoubleClick,
onEdgeContextMenu,
onPaneContextMenu,
onPaneClick,
closeEditModal,
saveNodeEdit,
deleteNode,
deleteEdge,
editNode,
editEdge,
copyNode,
addNodeOnEdge,
addNodeOnPane,
handleAddNode,
saveFlowDataToServer,
handleRun,
isRunning
} = props;
const { undo, redo, canUndo, canRedo } = useHistory();
// 监听键盘事件实现快捷键
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ctrl+Z 撤销
if (e.ctrlKey && e.key === 'z' && !e.shiftKey && canUndo) {
e.preventDefault();
undo();
}
// Ctrl+Shift+Z 重做
if (e.ctrlKey && e.shiftKey && e.key === 'Z' && canRedo) {
e.preventDefault();
redo();
}
// Ctrl+Y 重做
if (e.ctrlKey && e.key === 'y' && canRedo) {
e.preventDefault();
redo();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [undo, redo, canUndo, canRedo]);
// 监听节点和边的变化以拍摄快照
useEffect(() => {
// 获取 HistoryProvider 中的 takeSnapshot 方法
const event = new CustomEvent('takeSnapshot', {
detail: { nodes: [...nodes], edges: [...edges] }
});
document.dispatchEvent(event);
}, [nodes, edges]);
return (
<div ref={reactFlowWrapper} style={{ width: '100%', height: '100%', position: 'relative' }}
onContextMenu={(e) => e.preventDefault()}>
<ReactFlow
id={new Date().getTime().toString()}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapToGrid={true}
snapGrid={[2, 2]}
onNodesDelete={(deleted) => {
setNodes((nds) => nds.filter((n) => !deleted.find(d => d.id === n.id)));
}}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onReconnect={onReconnect}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeDrag={onNodeDrag}
connectionLineType={ConnectionLineType.SmoothStep}
connectionLineComponent={CustomConnectionLine}
onNodeDragStop={onNodeDragStop}
onNodeContextMenu={onNodeContextMenu}
onEdgeContextMenu={onEdgeContextMenu}
onNodeClick={onNodeDoubleClick}
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
onEdgeMouseEnter={(_event, edge) => {
setEdges(eds => eds.map(e => {
if (e.id === edge.id) {
return { ...e, data: { ...e.data, hovered: true } };
}
return e;
}));
}}
onEdgeMouseLeave={(_event, edge) => {
setEdges(eds => eds.map(e => {
if (e.id === edge.id) {
return { ...e, data: { ...e.data, hovered: false } };
}
return e;
}));
}}
fitView
selectionKeyCode={['Meta', 'Control']}
selectionMode={SelectionMode.Partial}
panOnDrag={[0, 1, 2]} //
zoomOnScroll={true}
zoomOnPinch={true}
panOnScrollSpeed={0.5}
>
<Background />
{/*<Controls />*/}
<Panel position="top-left">
<ActionBar
onSave={saveFlowDataToServer}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
onRun={handleRun}
isRunning={isRunning}
></ActionBar>
</Panel>
<AlignmentGuides />
</ReactFlow>
{/*节点右键上下文*/}
{menu && menu.type === 'node' && (
<div
style={{
position: 'absolute',
top: menu.top,
left: menu.left,
zIndex: 1000
}}
>
<NodeContextMenu
node={nodes.find(n => n.id === menu.id)!}
onDelete={deleteNode}
onEdit={editNode}
onCopy={copyNode}
/>
</div>
)}
{/*边右键上下文*/}
{menu && menu.type === 'edge' && (
<div
style={{
position: 'absolute',
top: menu.top,
left: menu.left,
zIndex: 1000
}}
>
<EdgeContextMenu
edge={edges.find(e => e.id === menu.id)!}
onDelete={deleteEdge}
onEdit={editEdge}
onAddNode={(edge) => {
setEdgeForNodeAdd(edge);
setMenu(null); // 关闭上下文菜单
}}
/>
</div>
)}
{/*画布右键上下文*/}
{menu && menu.type === 'pane' && (
<div
style={{
position: 'absolute',
top: menu.top,
left: menu.left,
zIndex: 1000
}}
>
<PaneContextMenu
position={menu.position!}
onAddNode={(nodeType: string, position: { x: number, y: number }, node) => {
addNodeOnPane(nodeType, position, node);
setMenu(null); // 关闭上下文菜单
}}
/>
</div>
)}
{/*节点点击/节点编辑上下文*/}
<NodeEditModal
popupContainer={reactFlowWrapper}
node={editingNode}
isOpen={isEditModalOpen}
isDelete={isDelete}
onSave={saveNodeEdit}
onClose={closeEditModal}
/>
{/*统一的添加节点菜单*/}
{(edgeForNodeAdd || positionForNodeAdd) && (
<div
style={{
position: 'absolute',
top: edgeForNodeAdd ? (edgeForNodeAdd.data?.y as number || 0) : (positionForNodeAdd?.y || 0),
left: edgeForNodeAdd ? ((edgeForNodeAdd.data?.x as number || 0) + 20) : (positionForNodeAdd?.x || 0),
zIndex: 1000,
transform: 'none'
}}
>
<AddNodeMenu
onAddNode={(nodeType, node) => {
handleAddNode(nodeType, node);
// 关闭菜单
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
}}
position={positionForNodeAdd || undefined}
edgeId={edgeForNodeAdd?.id}
/>
</div>
)}
</div>
);
};
export default FlowEditorWithProvider;