|
|
import React, { useEffect, useMemo } 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';
|
|
|
import { useSelector } from 'react-redux';
|
|
|
|
|
|
const edgeTypes = {
|
|
|
custom: CustomEdge
|
|
|
};
|
|
|
|
|
|
interface FlowEditorMainProps {
|
|
|
nodes: Node[];
|
|
|
edges: Edge[];
|
|
|
nodeTypes: NodeTypes;
|
|
|
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
|
|
|
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
|
|
|
useDefault: boolean;
|
|
|
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;
|
|
|
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,
|
|
|
useDefault,
|
|
|
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();
|
|
|
const reactFlowId = useMemo(() => new Date().getTime().toString(), []);
|
|
|
|
|
|
// 用于存储隐藏的节点ID
|
|
|
const [hiddenNodes, setHiddenNodes] = React.useState<Set<string>>(new Set());
|
|
|
|
|
|
// 监听自定义事件以隐藏/显示节点
|
|
|
useEffect(() => {
|
|
|
const handleToggleNodeVisibility = (event: CustomEvent) => {
|
|
|
const { appId, isVisible } = event.detail;
|
|
|
|
|
|
if (isVisible) {
|
|
|
// 显示节点 - 从隐藏节点集合中移除
|
|
|
setHiddenNodes(prev => {
|
|
|
const newSet = new Set(prev);
|
|
|
newSet.delete(appId);
|
|
|
return newSet;
|
|
|
});
|
|
|
}
|
|
|
else {
|
|
|
// 隐藏节点 - 添加到隐藏节点集合
|
|
|
setHiddenNodes(prev => new Set(prev).add(appId));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
|
|
|
|
|
|
return () => {
|
|
|
document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
|
|
|
};
|
|
|
}, []);
|
|
|
|
|
|
// 从Redux store中获取当前应用的运行状态
|
|
|
const { appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer);
|
|
|
const currentAppIsRunning = currentAppData?.id && appRuntimeData[currentAppData.id]
|
|
|
? appRuntimeData[currentAppData.id].isRunning
|
|
|
: false;
|
|
|
|
|
|
// 在应用编排模式下(useDefault为false)禁用删除功能
|
|
|
const isDeleteDisabled = currentAppIsRunning;
|
|
|
|
|
|
// 监听自定义事件以隐藏/显示节点
|
|
|
useEffect(() => {
|
|
|
const handleToggleNodeVisibility = (event: CustomEvent) => {
|
|
|
const { appId, isVisible } = event.detail;
|
|
|
|
|
|
if (isVisible) {
|
|
|
// 显示节点 - 从隐藏节点集合中移除
|
|
|
setHiddenNodes(prev => {
|
|
|
const newSet = new Set(prev);
|
|
|
newSet.delete(appId);
|
|
|
return newSet;
|
|
|
});
|
|
|
}
|
|
|
else {
|
|
|
// 隐藏节点 - 添加到隐藏节点集合
|
|
|
setHiddenNodes(prev => new Set(prev).add(appId));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
|
|
|
|
|
|
return () => {
|
|
|
document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener);
|
|
|
};
|
|
|
}, []);
|
|
|
|
|
|
// 监听键盘事件实现快捷键
|
|
|
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()}
|
|
|
// 添加点击事件处理器,用于关闭添加节点菜单
|
|
|
onClick={() => {
|
|
|
if (edgeForNodeAdd || positionForNodeAdd) {
|
|
|
setEdgeForNodeAdd(null);
|
|
|
setPositionForNodeAdd(null);
|
|
|
}
|
|
|
}}>
|
|
|
<ReactFlow
|
|
|
id={reactFlowId}
|
|
|
nodes={nodes.map(node => {
|
|
|
// 检查节点是否应该被隐藏
|
|
|
const isHidden = hiddenNodes.has(node.id);
|
|
|
// 应用透明度样式
|
|
|
const style = isHidden ? { opacity: 0.3 } : {};
|
|
|
|
|
|
return {
|
|
|
...node,
|
|
|
draggable: !currentAppIsRunning,
|
|
|
style: {
|
|
|
...node.style,
|
|
|
...style
|
|
|
}
|
|
|
};
|
|
|
})}
|
|
|
edges={edges.map(edge => {
|
|
|
// 检查边连接的节点是否被隐藏
|
|
|
const isSourceHidden = hiddenNodes.has(edge.source);
|
|
|
const isTargetHidden = hiddenNodes.has(edge.target);
|
|
|
// 如果源节点或目标节点被隐藏,则边也应用透明度
|
|
|
const style = (isSourceHidden || isTargetHidden) ? { opacity: 0.3 } : {};
|
|
|
|
|
|
// 更新边的数据,确保选择框也应用透明度
|
|
|
return {
|
|
|
...edge,
|
|
|
style: {
|
|
|
...edge.style,
|
|
|
...style
|
|
|
},
|
|
|
data: {
|
|
|
...edge.data,
|
|
|
...(isSourceHidden || isTargetHidden ? { hidden: true, opacity: 0.3 } : { opacity: 1 })
|
|
|
}
|
|
|
};
|
|
|
})}
|
|
|
nodeTypes={nodeTypes}
|
|
|
edgeTypes={edgeTypes}
|
|
|
snapToGrid={true}
|
|
|
snapGrid={[2, 2]}
|
|
|
nodesConnectable={!currentAppIsRunning} // 运行时禁用节点连接
|
|
|
nodesDraggable={!currentAppIsRunning} // 运行时禁用节点拖拽
|
|
|
elementsSelectable={!currentAppIsRunning} // 运行时禁用元素选择
|
|
|
connectOnClick={!currentAppIsRunning} // 运行时禁用点击连接
|
|
|
disableKeyboardA11y={currentAppIsRunning} // 运行时禁用键盘交互
|
|
|
onBeforeDelete={async ({ nodes, edges }) => {
|
|
|
// 在应用编排模式下(useDefault为false)只允许删除边,不允许删除节点
|
|
|
if (!useDefault && nodes.length > 0) {
|
|
|
console.warn('在应用编排模式下不允许删除节点');
|
|
|
return false; // 阻止删除节点操作
|
|
|
}
|
|
|
|
|
|
// 检查是否有开始或结束节点
|
|
|
const hasStartOrEndNode = nodes.some(node => node.type === 'start' || node.type === 'end');
|
|
|
if (hasStartOrEndNode) {
|
|
|
console.warn('开始和结束节点不允许删除');
|
|
|
return false; // 阻止删除操作
|
|
|
}
|
|
|
|
|
|
// 检查是否有循环节点(这里只是检查,实际删除逻辑在onNodesDelete中处理)
|
|
|
const loopNodes = nodes.filter(node =>
|
|
|
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
|
|
|
);
|
|
|
|
|
|
// 允许删除操作继续进行
|
|
|
return !currentAppIsRunning; // 在运行时禁止删除任何元素
|
|
|
}}
|
|
|
onNodesDelete={(deleted) => {
|
|
|
// 在应用编排模式下(useDefault为false)不允许删除节点
|
|
|
if (!useDefault) {
|
|
|
console.warn('在应用编排模式下不允许删除节点');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 如果在运行时,禁止删除
|
|
|
if (currentAppIsRunning) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 检查是否有循环节点
|
|
|
const loopNodes = deleted.filter(node =>
|
|
|
node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END'
|
|
|
);
|
|
|
|
|
|
if (loopNodes.length > 0) {
|
|
|
// 处理循环节点删除
|
|
|
let nodesToRemove = [...deleted];
|
|
|
|
|
|
// 为每个循环节点找到其配对节点
|
|
|
loopNodes.forEach(loopNode => {
|
|
|
const component = loopNode.data?.component as { customDef?: string } | undefined;
|
|
|
|
|
|
if (loopNode.data?.type === 'LOOP_START' && component?.customDef) {
|
|
|
try {
|
|
|
const customDef = JSON.parse(component.customDef);
|
|
|
const relatedNodeId = customDef.loopEndNodeId;
|
|
|
// 添加关联的结束节点到删除列表
|
|
|
const relatedNode = nodes.find(n => n.id === relatedNodeId);
|
|
|
if (relatedNode) {
|
|
|
nodesToRemove.push(relatedNode);
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('解析循环开始节点数据失败:', e);
|
|
|
}
|
|
|
}
|
|
|
else if (loopNode.data?.type === 'LOOP_END' && component?.customDef) {
|
|
|
try {
|
|
|
const customDef = JSON.parse(component.customDef);
|
|
|
const relatedNodeId = customDef.loopStartNodeId;
|
|
|
// 添加关联的开始节点到删除列表
|
|
|
const relatedNode = nodes.find(n => n.id === relatedNodeId);
|
|
|
if (relatedNode) {
|
|
|
nodesToRemove.push(relatedNode);
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('解析循环结束节点数据失败:', e);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 去重
|
|
|
nodesToRemove = nodesToRemove.filter((node, index, self) =>
|
|
|
index === self.findIndex(n => n.id === node.id)
|
|
|
);
|
|
|
|
|
|
// 删除所有相关节点和边
|
|
|
setNodes((nds) => nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id)));
|
|
|
|
|
|
// 删除与这些节点相关的所有边
|
|
|
const nodeIdsToRemove = nodesToRemove.map(node => node.id);
|
|
|
setEdges((eds) => eds.filter((e) =>
|
|
|
!nodeIdsToRemove.includes(e.source) && !nodeIdsToRemove.includes(e.target)
|
|
|
));
|
|
|
}
|
|
|
else {
|
|
|
// 普通节点删除
|
|
|
setNodes((nds) => nds.filter((n) => !deleted.find((d) => d.id === n.id)));
|
|
|
}
|
|
|
|
|
|
setIsEditModalOpen(false);
|
|
|
}}
|
|
|
onNodesChange={!currentAppIsRunning ? onNodesChange : undefined} // 运行时禁用节点变更
|
|
|
onEdgesChange={!currentAppIsRunning ? onEdgesChange : undefined} // 运行时禁用边变更
|
|
|
onConnect={!currentAppIsRunning ? onConnect : undefined} // 运行时禁用连接
|
|
|
onReconnect={!currentAppIsRunning ? onReconnect : undefined} // 运行时禁用重新连接
|
|
|
onDragOver={!currentAppIsRunning ? onDragOver : undefined} // 运行时禁用拖拽
|
|
|
onDrop={!currentAppIsRunning ? onDrop : undefined} // 运行时禁用放置
|
|
|
onNodeDrag={!currentAppIsRunning ? onNodeDrag : undefined} // 运行时禁用节点拖拽
|
|
|
connectionLineType={ConnectionLineType.SmoothStep}
|
|
|
connectionLineComponent={CustomConnectionLine}
|
|
|
onNodeDragStop={!currentAppIsRunning ? onNodeDragStop : undefined} // 运行时禁用节点拖拽停止
|
|
|
onNodeContextMenu={(!currentAppIsRunning && useDefault) ? onNodeContextMenu : undefined} // 运行时或应用编排模式下禁用节点上下文菜单
|
|
|
onNodeDoubleClick={!currentAppIsRunning ? onNodeDoubleClick : undefined} // 运行时禁用节点双击
|
|
|
onEdgeContextMenu={(!currentAppIsRunning && useDefault) ? onEdgeContextMenu : undefined} // 运行时或应用编排模式下禁用边上下文菜单
|
|
|
onPaneClick={!currentAppIsRunning ? onPaneClick : undefined} // 运行时禁用面板点击
|
|
|
onPaneContextMenu={(!currentAppIsRunning && useDefault) ? onPaneContextMenu : undefined} // 运行时或应用编排模式下禁用面板上下文菜单
|
|
|
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
|
|
|
selectionOnDrag={!currentAppIsRunning} // 运行时禁用拖拽选择
|
|
|
selectionMode={!currentAppIsRunning ? SelectionMode.Partial : undefined} // 运行时禁用选择模式
|
|
|
>
|
|
|
<Background />
|
|
|
<Panel position="top-left">
|
|
|
<ActionBar
|
|
|
useDefault={useDefault}
|
|
|
onSave={saveFlowDataToServer}
|
|
|
onUndo={undo}
|
|
|
onRedo={redo}
|
|
|
canUndo={canUndo}
|
|
|
canRedo={canRedo}
|
|
|
onRun={handleRun}
|
|
|
isRunning={currentAppIsRunning}
|
|
|
></ActionBar>
|
|
|
</Panel>
|
|
|
<AlignmentGuides />
|
|
|
</ReactFlow>
|
|
|
|
|
|
{/*节点右键上下文 - 仅在默认模式且非运行时显示*/}
|
|
|
{!currentAppIsRunning && useDefault && 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}
|
|
|
onCloseMenu={setMenu}
|
|
|
onCloseOpenModal={setIsEditModalOpen}
|
|
|
/>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{/*边右键上下文 - 在非运行时显示,应用编排模式下也允许*/}
|
|
|
{!currentAppIsRunning && 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);
|
|
|
setIsEditModalOpen(false);
|
|
|
setMenu(null); // 关闭上下文菜单
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{/*画布右键上下文 - 仅在默认模式且非运行时显示*/}
|
|
|
{!currentAppIsRunning && useDefault && 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);
|
|
|
setIsEditModalOpen(false);
|
|
|
setMenu(null); // 关闭上下文菜单
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{/*节点点击/节点编辑上下文*/}
|
|
|
<NodeEditModal
|
|
|
popupContainer={reactFlowWrapper}
|
|
|
node={editingNode}
|
|
|
isOpen={isEditModalOpen && !currentAppIsRunning}
|
|
|
isDelete={isDelete}
|
|
|
onSave={saveNodeEdit}
|
|
|
onClose={closeEditModal}
|
|
|
/>
|
|
|
|
|
|
{/*统一的添加节点菜单 - 仅在默认模式且非运行时显示*/}
|
|
|
{!currentAppIsRunning && useDefault && (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'
|
|
|
}}
|
|
|
// 点击事件冒泡到父级处理,这里阻止默认行为
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
>
|
|
|
<AddNodeMenu
|
|
|
onAddNode={(nodeType, node) => {
|
|
|
handleAddNode(nodeType, node);
|
|
|
// 关闭菜单
|
|
|
setEdgeForNodeAdd(null);
|
|
|
setPositionForNodeAdd(null);
|
|
|
}}
|
|
|
position={positionForNodeAdd || undefined}
|
|
|
edgeId={edgeForNodeAdd?.id}
|
|
|
/>
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
export default FlowEditorMain; |