refactor(flowEditor)!:重构流程编辑器组件结构
- 将 FlowEditorContent 组件拆分为独立的 FlowEditorMain 组件 - 提取状态管理逻辑到 useFlowEditorState 自定义 Hook - 提取回调函数到 useFlowCallbacks 自定义 Hook - 移除临时事件列表数据引用 - 优化组件间数据传递和事件处理 - 清理未使用的导入和组件引用 - 统一节点类型管理方式 BREAKING CHANGE: 整体结构重构,更新后的单文件逻辑不在使用,但整体业务逻辑不变master
parent
d4a23472b0
commit
1395bb735b
@ -0,0 +1,696 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
addEdge,
|
||||
reconnectEdge,
|
||||
Node,
|
||||
Edge
|
||||
} from '@xyflow/react';
|
||||
import { setMainFlow } from '@/api/appRes';
|
||||
import { getUserToken } from '@/api/user';
|
||||
import { Message } from '@arco-design/web-react';
|
||||
import { nodeTypeMap, registerNodeType } from '@/components/FlowEditor/node';
|
||||
import { convertFlowData, revertFlowData } from '@/utils/convertFlowData';
|
||||
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
|
||||
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
|
||||
import useWebSocket from '@/hooks/useWebSocket';
|
||||
import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
|
||||
import LocalNode from '@/components/FlowEditor/node/localNode/LocalNode';
|
||||
import BasicNode from '@/components/FlowEditor/node/basicNode/BasicNode';
|
||||
import { updateCanvasDataMap } from '@/store/ideContainer';
|
||||
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
export const useFlowCallbacks = (
|
||||
nodes: Node[],
|
||||
setNodes: React.Dispatch<React.SetStateAction<Node[]>>,
|
||||
edges: Edge[],
|
||||
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>,
|
||||
reactFlowInstance: any,
|
||||
canvasDataMap: any,
|
||||
dispatch: Dispatch<any>,
|
||||
updateCanvasDataMapDebounced: (
|
||||
dispatch: Dispatch<any>,
|
||||
canvasDataMap: any,
|
||||
id: string,
|
||||
nodes: Node[],
|
||||
edges: Edge[]
|
||||
) => void,
|
||||
initialData: any,
|
||||
historyTimeoutRef: React.MutableRefObject<NodeJS.Timeout | null>,
|
||||
setHistoryInitialized: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
editingNode: Node | null,
|
||||
setEditingNode: React.Dispatch<React.SetStateAction<Node | null>>,
|
||||
setIsEditModalOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
edgeForNodeAdd: Edge | null,
|
||||
setEdgeForNodeAdd: React.Dispatch<React.SetStateAction<Edge | null>>,
|
||||
positionForNodeAdd: { x: number, y: number } | null,
|
||||
setPositionForNodeAdd: React.Dispatch<React.SetStateAction<{ x: number, y: number } | null>>,
|
||||
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setIsRunning: React.Dispatch<React.SetStateAction<boolean>>
|
||||
) => {
|
||||
const { getGuidelines, clearGuidelines } = 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 || 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;
|
||||
};
|
||||
|
||||
// 修改 onNodesChange 函数,添加防抖机制
|
||||
const onNodesChange = useCallback(
|
||||
(changes: any) => {
|
||||
const newNodes = applyNodeChanges(changes, nodes);
|
||||
setNodes(newNodes);
|
||||
// 如果需要在节点变化时执行某些操作,可以在这里添加
|
||||
|
||||
// 只有当变化是节点位置变化时才不立即记录历史
|
||||
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);
|
||||
// 如果需要在边变化时执行某些操作,可以在这里添加
|
||||
|
||||
// 边的变化立即记录历史
|
||||
const event = new CustomEvent('takeSnapshot', {
|
||||
detail: { nodes: [...nodes], edges: [...newEdges] }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
},
|
||||
[edges, nodes]
|
||||
);
|
||||
|
||||
const onNodesDelete = useCallback((deletedNodes: any) => {
|
||||
setIsDelete(true);
|
||||
}, []);
|
||||
|
||||
// 修改 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;
|
||||
}
|
||||
|
||||
// 如果验证通过,创建连接
|
||||
setEdges((edgesSnapshot: Edge[]) => {
|
||||
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: Node[]) => {
|
||||
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]);
|
||||
|
||||
// 初始化画布数据
|
||||
const initializeCanvasData = useCallback(() => {
|
||||
if (canvasDataMap[initialData?.id]) {
|
||||
const { edges, nodes } = canvasDataMap[initialData?.id];
|
||||
setNodes(nodes);
|
||||
setEdges(edges);
|
||||
}
|
||||
else {
|
||||
// 首次进入
|
||||
const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(initialData);
|
||||
// 为所有边添加类型
|
||||
const initialEdges: Edge[] = convertedEdges.map(edge => ({
|
||||
...edge,
|
||||
type: 'custom'
|
||||
}));
|
||||
|
||||
setNodes(convertedNodes);
|
||||
setEdges(initialEdges);
|
||||
|
||||
if (initialData?.id) {
|
||||
dispatch(updateCanvasDataMap({
|
||||
...canvasDataMap,
|
||||
[initialData.id]: { nodes: convertedNodes, edges: initialEdges }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 标记历史记录已初始化
|
||||
setHistoryInitialized(true);
|
||||
}, [initialData, canvasDataMap]);
|
||||
|
||||
// 实时更新 canvasDataMap
|
||||
const updateCanvasDataMapEffect = useCallback(() => {
|
||||
if (initialData?.id) {
|
||||
updateCanvasDataMapDebounced(dispatch, canvasDataMap, initialData.id, nodes, edges);
|
||||
}
|
||||
|
||||
// 清理函数,在组件卸载时取消防抖
|
||||
return () => {
|
||||
// 取消防抖函数
|
||||
};
|
||||
}, [nodes, edges, initialData?.id, dispatch, canvasDataMap]);
|
||||
|
||||
// 关闭编辑弹窗
|
||||
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: Node[]) => nds.filter((n) => n.id !== node.id));
|
||||
setEdges((eds: Edge[]) => eds.filter((e) => e.source !== node.id && e.target !== node.id));
|
||||
|
||||
// 删除节点后记录历史
|
||||
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: Edge[]) => eds.filter((e) => e.id !== edge.id));
|
||||
|
||||
// 删除边后记录历史
|
||||
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) => {
|
||||
setEditingNode(node);
|
||||
setIsEditModalOpen(true);
|
||||
}, []);
|
||||
|
||||
// 编辑边
|
||||
const editEdge = useCallback((edge: Edge) => {
|
||||
// 这里可以实现边编辑逻辑
|
||||
console.log('编辑边:', edge);
|
||||
}, []);
|
||||
|
||||
// 复制节点
|
||||
const copyNode = useCallback((node: Node) => {
|
||||
// 这里可以实现节点复制逻辑
|
||||
console.log('复制节点:', node);
|
||||
}, []);
|
||||
|
||||
// 在边上添加节点的具体实现
|
||||
// 修改 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: Node[]) => [...nds, newNode]);
|
||||
|
||||
// 删除旧边
|
||||
setEdges((eds: Edge[]) => 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) => {
|
||||
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: Node[]) => {
|
||||
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: any) => {
|
||||
// 如果是通过边添加节点
|
||||
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('保存失败');
|
||||
}
|
||||
}, [nodes, edges]);
|
||||
|
||||
// 初始化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]);
|
||||
|
||||
return {
|
||||
// Event handlers
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
onConnect,
|
||||
onReconnect,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onNodeDrag,
|
||||
onNodeDragStop,
|
||||
onNodesDelete,
|
||||
|
||||
// Menu handlers
|
||||
closeEditModal,
|
||||
saveNodeEdit,
|
||||
deleteNode,
|
||||
deleteEdge,
|
||||
editNode,
|
||||
editEdge,
|
||||
copyNode,
|
||||
|
||||
// Node operations
|
||||
addNodeOnEdge,
|
||||
addNodeOnPane,
|
||||
handleAddNode,
|
||||
|
||||
// Initialization
|
||||
initializeCanvasData,
|
||||
updateCanvasDataMapEffect,
|
||||
|
||||
// Actions
|
||||
saveFlowDataToServer,
|
||||
handleRun,
|
||||
|
||||
// Utilities
|
||||
getHandleType,
|
||||
validateDataType
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,68 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { Node, Edge } from '@xyflow/react';
|
||||
import { debounce } from 'lodash';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateCanvasDataMap } from '@/store/ideContainer';
|
||||
|
||||
export const useFlowEditorState = (initialData?: any) => {
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [edges, setEdges] = useState<Edge[]>([]);
|
||||
const { canvasDataMap } = useSelector((state: any) => state.ideContainer);
|
||||
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 [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
// 在组件顶部添加历史记录相关状态
|
||||
const [historyInitialized, setHistoryInitialized] = useState(false);
|
||||
const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const updateCanvasDataMapDebounced = useRef(
|
||||
debounce((dispatch: Function, canvasDataMap: any, id: string, nodes: Node[], edges: Edge[]) => {
|
||||
dispatch(updateCanvasDataMap({
|
||||
...canvasDataMap,
|
||||
[id]: { nodes, edges }
|
||||
}));
|
||||
}, 500)
|
||||
).current;
|
||||
|
||||
return {
|
||||
// State values
|
||||
nodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
canvasDataMap,
|
||||
editingNode,
|
||||
setEditingNode,
|
||||
isEditModalOpen,
|
||||
setIsEditModalOpen,
|
||||
isDelete,
|
||||
setIsDelete,
|
||||
edgeForNodeAdd,
|
||||
setEdgeForNodeAdd,
|
||||
positionForNodeAdd,
|
||||
setPositionForNodeAdd,
|
||||
isRunning,
|
||||
setIsRunning,
|
||||
historyInitialized,
|
||||
setHistoryInitialized,
|
||||
historyTimeoutRef,
|
||||
updateCanvasDataMapDebounced,
|
||||
|
||||
// Redux
|
||||
dispatch,
|
||||
|
||||
// Initial data
|
||||
initialData: initialData
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,340 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Panel,
|
||||
SelectionMode,
|
||||
ConnectionLineType,
|
||||
Node,
|
||||
Edge,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
OnConnect,
|
||||
OnReconnect
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
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 { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines';
|
||||
import { useHistory } from './components/historyContext';
|
||||
import { NodeTypes } from '@xyflow/react';
|
||||
|
||||
const edgeTypes = {
|
||||
custom: CustomEdge
|
||||
};
|
||||
|
||||
interface FlowEditorMainProps {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
nodeTypes: NodeTypes;
|
||||
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
|
||||
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
|
||||
reactFlowInstance: any;
|
||||
reactFlowWrapper: React.RefObject<HTMLDivElement>;
|
||||
menu: any;
|
||||
setMenu: React.Dispatch<React.SetStateAction<any>>;
|
||||
editingNode: Node | null;
|
||||
setEditingNode: React.Dispatch<React.SetStateAction<Node | null>>;
|
||||
isEditModalOpen: boolean;
|
||||
setIsEditModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isDelete: boolean;
|
||||
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
edgeForNodeAdd: Edge | null;
|
||||
setEdgeForNodeAdd: React.Dispatch<React.SetStateAction<Edge | null>>;
|
||||
positionForNodeAdd: { x: number, y: number } | null;
|
||||
setPositionForNodeAdd: React.Dispatch<React.SetStateAction<{ x: number, y: number } | null>>;
|
||||
isRunning: boolean;
|
||||
setIsRunning: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
initialData: any;
|
||||
canvasDataMap: any;
|
||||
|
||||
// Callbacks
|
||||
onNodesChange: OnNodesChange;
|
||||
onEdgesChange: OnEdgesChange;
|
||||
onConnect: OnConnect;
|
||||
onReconnect: OnReconnect;
|
||||
onDragOver: (event: React.DragEvent) => void;
|
||||
onDrop: (event: React.DragEvent) => void;
|
||||
onNodeDrag: (event: React.MouseEvent, node: Node) => void;
|
||||
onNodeDragStop: () => void;
|
||||
onNodeContextMenu: (event: React.MouseEvent, node: Node) => void;
|
||||
onNodeDoubleClick: (event: React.MouseEvent, node: Node) => void;
|
||||
onEdgeContextMenu: (event: React.MouseEvent, edge: Edge) => void;
|
||||
onPaneContextMenu: (event: React.MouseEvent) => void;
|
||||
onPaneClick: () => void;
|
||||
closeEditModal: () => void;
|
||||
saveNodeEdit: (updatedData: any) => void;
|
||||
deleteNode: (node: Node) => void;
|
||||
deleteEdge: (edge: Edge) => void;
|
||||
editNode: (node: Node) => void;
|
||||
editEdge: (edge: Edge) => void;
|
||||
copyNode: (node: Node) => void;
|
||||
addNodeOnEdge: (nodeType: string, node: any) => void;
|
||||
addNodeOnPane: (nodeType: string, position: { x: number; y: number }, node?: any) => void;
|
||||
handleAddNode: (nodeType: string, node: any) => void;
|
||||
saveFlowDataToServer: () => void;
|
||||
handleRun: (running: boolean) => void;
|
||||
}
|
||||
|
||||
const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
nodeTypes,
|
||||
setNodes,
|
||||
setEdges,
|
||||
reactFlowInstance,
|
||||
reactFlowWrapper,
|
||||
menu,
|
||||
setMenu,
|
||||
editingNode,
|
||||
setEditingNode,
|
||||
isEditModalOpen,
|
||||
setIsEditModalOpen,
|
||||
isDelete,
|
||||
setIsDelete,
|
||||
edgeForNodeAdd,
|
||||
setEdgeForNodeAdd,
|
||||
positionForNodeAdd,
|
||||
setPositionForNodeAdd,
|
||||
isRunning,
|
||||
initialData,
|
||||
canvasDataMap,
|
||||
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
|
||||
} = props;
|
||||
|
||||
const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines();
|
||||
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 />
|
||||
<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: any) => {
|
||||
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 FlowEditorMain;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue