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.

368 lines
10 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, { useState, useCallback, useRef, useEffect } from 'react';
import {
ReactFlow,
applyNodeChanges,
applyEdgeChanges,
addEdge,
reconnectEdge,
Background,
Controls,
Node,
Edge,
ReactFlowProvider,
useReactFlow,
EdgeTypes,
SelectionMode,
useStoreApi,
Panel
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
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 NodeContextMenu from './components/nodeContextMenu';
import EdgeContextMenu from './components/edgeContextMenu';
import NodeEditModal from './components/nodeEditModal';
const edgeTypes: EdgeTypes = {
custom: CustomEdge
};
const FlowEditorWithProvider: React.FC = () => {
return (
<div style={{ width: '100%', height: '91vh', display: 'flex' }}>
<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';
top: number;
left: number;
} | null>(null);
const store = useStoreApi();
// 添加编辑弹窗相关状态
const [editingNode, setEditingNode] = useState<Node | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const onNodesChange = useCallback(
(changes: any) => {
const newNodes = applyNodeChanges(changes, nodes);
setNodes(newNodes);
// 如果需要在节点变化时执行某些操作,可以在这里添加
},
[nodes]
);
const onEdgesChange = useCallback(
(changes: any) => {
const newEdges = applyEdgeChanges(changes, edges);
setEdges(newEdges);
// 如果需要在边变化时执行某些操作,可以在这里添加
},
[edges]
);
const onConnect = useCallback(
(params: any) => setEdges((edgesSnapshot) => addEdge({ ...params, type: 'custom' }, edgesSnapshot)),
[]
);
// 边重新连接处理
const onReconnect = useCallback(
(oldEdge: Edge, newConnection: any) =>
setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
[]
);
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]
);
useEffect(() => {
const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(exampleFlowData);
// 为所有边添加类型
const initialEdges: Edge[] = convertedEdges.map(edge => ({
...edge,
type: 'custom'
}));
setNodes(convertedNodes);
setEdges(initialEdges);
}, []);
// 节点右键菜单处理
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) => {
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 onPaneClick = useCallback(() => setMenu(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 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' }}>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onReconnect={onReconnect}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeContextMenu={onNodeContextMenu}
onEdgeContextMenu={onEdgeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneClick}
fitView
selectionKeyCode={['Meta', 'Control']}
selectionMode={SelectionMode.Partial}
panOnDrag={[0, 1, 2]} //
zoomOnScroll={true}
zoomOnPinch={true}
panOnScrollSpeed={0.5}
>
<Background />
<Controls />
<Panel position="top-right">
<div></div>
<button onClick={saveFlowDataToServer}></button>
</Panel>
</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}
/>
</div>
)}
{/*节点双击/节点编辑上下文*/}
<NodeEditModal
node={editingNode}
isOpen={isEditModalOpen}
onSave={saveNodeEdit}
onClose={closeEditModal}
/>
</div>
);
};
export default FlowEditorWithProvider;