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.
flow-playform-react/src/pages/flowEditor/FlowEditorMain.tsx

679 lines
25 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';
import HandlerBar from '@/pages/flowEditor/components/handlerBar';
import PublishFlowModal from '@/pages/flowEditor/components/publishFlowModal';
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;
readOnly?: 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;
pasteNode: (position: { x: number; y: number }) => 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;
handlePause: (isPaused: boolean) => void;
handleReRun: () => void;
}
const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
const {
nodes,
edges,
nodeTypes,
setNodes,
setEdges,
useDefault,
readOnly = false, // 解构 readOnly默认为 false
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,
pasteNode,
addNodeOnEdge,
addNodeOnPane,
handleAddNode,
saveFlowDataToServer,
handleRun,
handlePause,
handleReRun
} = 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());
// 流程发布弹窗状态
const [publishModalVisible, setPublishModalVisible] = React.useState(false);
// 监听自定义事件以隐藏/显示节点
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 getCurrentAppKey = () => {
if (!currentAppData) return null;
// 如果是子流程key包含'sub'使用key作为标识符
if (currentAppData.key && currentAppData.key.includes('sub')) {
return currentAppData.key;
}
// 否则使用id
return currentAppData.id;
};
const currentAppKey = getCurrentAppKey();
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].isRunning
: false;
// 监听自定义事件以隐藏/显示节点
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) => {
// 如果当前正在运行或不是默认模式,不处理复制粘贴快捷键
const canEdit = !currentAppIsRunning && useDefault;
// 兼容 Mac (metaKey) 和 Windows/Linux (ctrlKey)
const isModifierKey = e.ctrlKey || e.metaKey;
// 检查事件目标是否在可编辑元素中input、textarea、contenteditable、CodeMirror等
const target = e.target as HTMLElement;
const isEditableElement =
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target?.getAttribute('contenteditable') === 'true' ||
target?.closest('[contenteditable="true"]') !== null ||
target?.closest('.cm-content') !== null || // CodeMirror 编辑器
target?.closest('.cm-editor') !== null ||
target?.closest('.arco-input') !== null || // Arco Design 输入框
target?.closest('.arco-textarea') !== null;
// 检查是否有打开的弹出层Modal、Drawer、Popover等如果有则不拦截快捷键
const hasOpenPopup =
document.querySelector('.arco-modal-wrapper') !== null ||
document.querySelector('.arco-drawer-wrapper') !== null ||
document.querySelector('.arco-popover-popup') !== null;
// 如果在可编辑元素中或有打开的弹出层,不拦截复制粘贴快捷键,让浏览器处理
if (isEditableElement || hasOpenPopup) {
return;
}
// Ctrl/Cmd+Z 撤销
if (isModifierKey && e.key === 'z' && !e.shiftKey && canUndo) {
e.preventDefault();
undo();
}
// Ctrl/Cmd+Shift+Z 重做
if (isModifierKey && e.shiftKey && e.key === 'Z' && canRedo) {
e.preventDefault();
redo();
}
// Ctrl/Cmd+Y 重做
if (isModifierKey && e.key === 'y' && canRedo) {
e.preventDefault();
redo();
}
// Ctrl/Cmd+C 复制选中的节点
if (isModifierKey && e.key === 'c' && canEdit) {
// 获取当前选中的节点
const selectedNode = nodes.find(node => node.selected);
if (selectedNode) {
// 不允许复制开始和结束节点
if (selectedNode.type === 'start' || selectedNode.type === 'end') {
return;
}
e.preventDefault();
copyNode(selectedNode);
}
}
// Ctrl/Cmd+V 粘贴节点
if (isModifierKey && e.key === 'v' && canEdit) {
const copiedNodeStr = localStorage.getItem('copiedNode');
if (copiedNodeStr) {
e.preventDefault();
// 获取当前视口中心位置
if (reactFlowInstance) {
const viewport = reactFlowInstance.getViewport();
const { x, y, zoom } = viewport;
// 获取画布容器的尺寸
const wrapper = reactFlowWrapper.current;
if (wrapper) {
const rect = wrapper.getBoundingClientRect();
// 计算视口中心点在流程图坐标系中的位置
const centerX = (-x + rect.width / 2) / zoom;
const centerY = (-y + rect.height / 2) / zoom;
pasteNode({ x: centerX, y: centerY });
}
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [undo, redo, canUndo, canRedo, currentAppIsRunning, useDefault, nodes, copyNode, pasteNode, reactFlowInstance]);
// 处理流程发布
const handlePublish = () => {
// 先保存流程数据
saveFlowDataToServer();
// 打开发布弹窗
setPublishModalVisible(true);
};
// 发布成功后的回调
const handlePublishSuccess = () => {
setPublishModalVisible(false);
// 可以在这里添加其他逻辑,比如刷新流程列表等
};
// 监听节点和边的变化以拍摄快照
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: readOnly ? false : !currentAppIsRunning, // readOnly 模式下禁止拖拽
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]}
proOptions={{ hideAttribution: true }} //
nodesConnectable={readOnly ? false : !currentAppIsRunning} // readOnly
nodesDraggable={readOnly ? false : !currentAppIsRunning} // readOnly
elementsSelectable={!readOnly} // readOnly
connectOnClick={readOnly ? false : !currentAppIsRunning} // readOnly
disableKeyboardA11y={readOnly || currentAppIsRunning} // readOnly
edgesFocusable={!readOnly} // readOnly
nodesFocusable={!readOnly} // readOnly
edgesReconnectable={readOnly ? false : !currentAppIsRunning} // readOnly
onBeforeDelete={async ({ nodes, edges }) => {
// readOnly 模式下禁止删除
if (readOnly) {
return false;
}
// 在应用编排模式下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={readOnly ? undefined : (!currentAppIsRunning ? onNodesChange : undefined)} // readOnly
onEdgesChange={readOnly ? undefined : (!currentAppIsRunning ? onEdgesChange : undefined)} // readOnly
onConnect={readOnly ? undefined : (!currentAppIsRunning ? onConnect : undefined)} // readOnly
onReconnect={readOnly ? undefined : (!currentAppIsRunning ? onReconnect : undefined)} // readOnly
onDragOver={readOnly ? undefined : (!currentAppIsRunning ? onDragOver : undefined)} // readOnly
onDrop={readOnly ? undefined : (!currentAppIsRunning ? onDrop : undefined)} // readOnly
onNodeDrag={readOnly ? undefined : (!currentAppIsRunning ? onNodeDrag : undefined)} // readOnly
connectionLineType={ConnectionLineType.SmoothStep}
connectionLineComponent={CustomConnectionLine}
onNodeDragStop={readOnly ? undefined : (!currentAppIsRunning ? onNodeDragStop : undefined)} // readOnly
onNodeContextMenu={readOnly ? undefined : ((useDefault || currentAppIsRunning) ? onNodeContextMenu : undefined)} // readOnly
onNodeDoubleClick={readOnly ? undefined : (!currentAppIsRunning ? onNodeDoubleClick : undefined)} // readOnly
onEdgeContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onEdgeContextMenu : undefined)} // readOnly
onPaneClick={(useDefault || currentAppIsRunning) ? onPaneClick : undefined} //
onPaneContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onPaneContextMenu : undefined)} // readOnly
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 />
{!readOnly && (
<Panel position="top-left">
<ActionBar
useDefault={useDefault}
onSave={saveFlowDataToServer}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
onRun={handleRun}
onPause={handlePause}
onReRun={handleReRun}
isRunning={currentAppIsRunning}
></ActionBar>
</Panel>
)}
{useDefault && !readOnly && <Panel position="top-right">
<HandlerBar
onPublish={handlePublish}
isRunning={currentAppIsRunning}
setNodes={setNodes}
setEdges={setEdges}
/>
</Panel>}
<AlignmentGuides />
</ReactFlow>
{/*节点右键上下文 - 在默认模式或运行状态下显示,运行时隐藏编辑复制删除功能*/}
{(useDefault || currentAppIsRunning) && 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}
isRunning={currentAppIsRunning}
/>
</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>
)}
{/*流程发布弹窗*/}
<PublishFlowModal
visible={publishModalVisible}
onCancel={() => setPublishModalVisible(false)}
onSuccess={handlePublishSuccess}
appId={initialData?.id}
flowId={initialData?.flowId}
currentFlowName={initialData?.name}
/>
</div>
);
};
export default FlowEditorMain;