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.

548 lines
19 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;