|
|
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 { Button, Modal } from '@arco-design/web-react';
|
|
|
import { nodeTypeMap, nodeTypes, registerNodeType } from './node';
|
|
|
import SideBar from './sideBar/sideBar';
|
|
|
import { convertFlowData } from '@/utils/convertFlowData';
|
|
|
import { exampleFlowData } from '@/pages/flowEditor/test/exampleFlowData';
|
|
|
import LocalNode from '@/pages/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 { defaultNodeTypes } from '@/pages/flowEditor/node/types/defaultType';
|
|
|
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
|
|
|
import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
|
|
|
|
|
|
const edgeTypes: EdgeTypes = {
|
|
|
custom: CustomEdge
|
|
|
};
|
|
|
|
|
|
const FlowEditorWithProvider: React.FC = () => {
|
|
|
return (
|
|
|
<div style={{ width: '100%', height: '91vh', display: 'flex' }} onContextMenu={(e) => e.preventDefault()}>
|
|
|
<ReactFlowProvider>
|
|
|
{/*<SideBar />*/}
|
|
|
<FlowEditor />
|
|
|
</ReactFlowProvider>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const FlowEditor: React.FC = () => {
|
|
|
const [nodes, setNodes] = useState<Node[]>([]);
|
|
|
const [edges, setEdges] = useState<Edge[]>([]);
|
|
|
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 [editingNode, setEditingNode] = useState<Node | null>(null);
|
|
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
|
|
|
|
// 添加节点选择弹窗状态
|
|
|
const [edgeForNodeAdd, setEdgeForNodeAdd] = useState<Edge | null>(null);
|
|
|
const [positionForNodeAdd, setPositionForNodeAdd] = useState<{ x: number, y: number } | null>(null);
|
|
|
|
|
|
const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines();
|
|
|
|
|
|
// 获取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 === handleId) ||
|
|
|
apiIns.some((api: any) => api.name === handleId)) {
|
|
|
return 'api';
|
|
|
}
|
|
|
|
|
|
// 检查是否为data类型的handle
|
|
|
const dataOuts = nodeParams.dataOuts || [];
|
|
|
const dataIns = nodeParams.dataIns || [];
|
|
|
|
|
|
if (dataOuts.some((data: any) => data.name === handleId) ||
|
|
|
dataIns.some((data: any) => data.name === 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 onNodesChange = useCallback(
|
|
|
(changes: any) => {
|
|
|
const newNodes = applyNodeChanges(changes, nodes);
|
|
|
setNodes(newNodes);
|
|
|
// 如果需要在节点变化时执行某些操作,可以在这里添加
|
|
|
onPaneClick();
|
|
|
},
|
|
|
[nodes]
|
|
|
);
|
|
|
|
|
|
const onEdgesChange = useCallback(
|
|
|
(changes: any) => {
|
|
|
const newEdges = applyEdgeChanges(changes, edges);
|
|
|
setEdges(newEdges);
|
|
|
// 如果需要在边变化时执行某些操作,可以在这里添加
|
|
|
onPaneClick();
|
|
|
},
|
|
|
[edges]
|
|
|
);
|
|
|
|
|
|
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 || {};
|
|
|
|
|
|
// 获取源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) => addEdge({ ...params, type: 'custom' }, edgesSnapshot));
|
|
|
},
|
|
|
[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';
|
|
|
}, []);
|
|
|
|
|
|
// 侧边栏节点实例
|
|
|
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) => nds.concat(newNode));
|
|
|
},
|
|
|
[reactFlowInstance]
|
|
|
);
|
|
|
|
|
|
const onNodeDrag = useCallback(
|
|
|
(_: any, node: Node) => {
|
|
|
// 获取对齐线
|
|
|
getGuidelines(node, nodes);
|
|
|
},
|
|
|
[nodes, getGuidelines]
|
|
|
);
|
|
|
|
|
|
// 节点拖拽结束处理
|
|
|
const onNodeDragStop = useCallback(() => {
|
|
|
// 清除对齐线
|
|
|
clearGuidelines();
|
|
|
}, [clearGuidelines]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(exampleFlowData);
|
|
|
// 为所有边添加类型
|
|
|
const initialEdges: Edge[] = convertedEdges.map(edge => ({
|
|
|
...edge,
|
|
|
type: 'custom'
|
|
|
}));
|
|
|
|
|
|
setNodes(convertedNodes);
|
|
|
setEdges(initialEdges);
|
|
|
}, []);
|
|
|
|
|
|
// 监听边的变化,处理添加节点的触发
|
|
|
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);
|
|
|
}, []);
|
|
|
|
|
|
// 删除边
|
|
|
const deleteEdge = useCallback((edge: Edge) => {
|
|
|
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
|
|
|
setMenu(null);
|
|
|
}, []);
|
|
|
|
|
|
// 编辑节点
|
|
|
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);
|
|
|
}, []);
|
|
|
|
|
|
// 在边上添加节点的具体实现
|
|
|
const addNodeOnEdge = useCallback((nodeType: string) => {
|
|
|
if (!edgeForNodeAdd || !reactFlowInstance) return;
|
|
|
|
|
|
// 查找节点定义
|
|
|
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType);
|
|
|
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, LocalNode, nodeDefinition.nodeName);
|
|
|
|
|
|
// 添加新节点
|
|
|
setNodes((nds) => [...nds, newNode]);
|
|
|
|
|
|
// 删除旧边
|
|
|
setEdges((eds) => eds.filter(e => e.id !== edgeForNodeAdd.id));
|
|
|
|
|
|
// 创建新边: source -> new node, new node -> target
|
|
|
setEdges((eds) => [
|
|
|
...eds,
|
|
|
{
|
|
|
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'
|
|
|
}
|
|
|
]);
|
|
|
|
|
|
// 关闭菜单
|
|
|
setEdgeForNodeAdd(null);
|
|
|
setPositionForNodeAdd(null);
|
|
|
}, [edgeForNodeAdd, nodes, reactFlowInstance]);
|
|
|
|
|
|
// 在画布上添加节点
|
|
|
const addNodeOnPane = useCallback((nodeType: string, position: { x: number; y: number }) => {
|
|
|
setMenu(null);
|
|
|
|
|
|
if (!reactFlowInstance) return;
|
|
|
|
|
|
// 查找节点定义
|
|
|
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType);
|
|
|
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, LocalNode, nodeDefinition.nodeName);
|
|
|
|
|
|
setNodes((nds) => [...nds, newNode]);
|
|
|
}, [reactFlowInstance]);
|
|
|
|
|
|
// 处理添加节点的统一方法
|
|
|
const handleAddNode = useCallback((nodeType: string) => {
|
|
|
// 如果是通过边添加节点
|
|
|
if (edgeForNodeAdd) {
|
|
|
addNodeOnEdge(nodeType);
|
|
|
}
|
|
|
// 如果是通过画布添加节点
|
|
|
else if (positionForNodeAdd) {
|
|
|
addNodeOnPane(nodeType, positionForNodeAdd);
|
|
|
}
|
|
|
|
|
|
// 清除状态
|
|
|
setEdgeForNodeAdd(null);
|
|
|
setPositionForNodeAdd(null);
|
|
|
}, [edgeForNodeAdd, positionForNodeAdd, addNodeOnEdge, addNodeOnPane]);
|
|
|
|
|
|
// 保存所有节点和边数据到服务器
|
|
|
const saveFlowDataToServer = useCallback(async () => {
|
|
|
try {
|
|
|
// 准备要发送到服务器的数据
|
|
|
const flowData = {
|
|
|
nodes: nodes,
|
|
|
edges: edges
|
|
|
};
|
|
|
// TODO 接口对接后修改后续的更新操作
|
|
|
console.log('flowData:', flowData);
|
|
|
return;
|
|
|
// 发送到服务器的示例代码(需要根据您的实际API进行调整)
|
|
|
const response = await fetch('/api/flow/save', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify(flowData)
|
|
|
});
|
|
|
|
|
|
if (response.ok) {
|
|
|
console.log('Flow data saved successfully');
|
|
|
// 可以添加成功提示
|
|
|
}
|
|
|
else {
|
|
|
console.error('Failed to save flow data');
|
|
|
// 可以添加失败提示
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error saving flow data:', error);
|
|
|
// 可以添加错误提示
|
|
|
}
|
|
|
}, [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]}
|
|
|
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-center">
|
|
|
<Button onClick={saveFlowDataToServer} type="primary">保存</Button>
|
|
|
</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 }) => {
|
|
|
addNodeOnPane(nodeType, position);
|
|
|
setMenu(null); // 关闭上下文菜单
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{/*节点双击/节点编辑上下文*/}
|
|
|
<NodeEditModal
|
|
|
popupContainer={reactFlowWrapper}
|
|
|
node={editingNode}
|
|
|
isOpen={isEditModalOpen}
|
|
|
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) => {
|
|
|
handleAddNode(nodeType);
|
|
|
// 关闭菜单
|
|
|
setEdgeForNodeAdd(null);
|
|
|
setPositionForNodeAdd(null);
|
|
|
}}
|
|
|
position={positionForNodeAdd || undefined}
|
|
|
edgeId={edgeForNodeAdd?.id}
|
|
|
/>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
export default FlowEditorWithProvider; |