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.
262 lines
7.1 KiB
TypeScript
262 lines
7.1 KiB
TypeScript
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
import {
|
|
ReactFlow,
|
|
applyNodeChanges,
|
|
applyEdgeChanges,
|
|
addEdge,
|
|
Background,
|
|
Controls,
|
|
Node,
|
|
Edge,
|
|
ReactFlowProvider,
|
|
useReactFlow,
|
|
EdgeTypes,
|
|
SelectionMode,
|
|
useStoreApi
|
|
} 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';
|
|
|
|
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 onNodesChange = useCallback(
|
|
(changes: any) => setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot)),
|
|
[]
|
|
);
|
|
const onEdgesChange = useCallback(
|
|
(changes: any) => setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot)),
|
|
[]
|
|
);
|
|
|
|
const onConnect = useCallback(
|
|
(params: any) => setEdges((edgesSnapshot) => addEdge({ ...params, type: 'custom' }, edgesSnapshot)),
|
|
[]
|
|
);
|
|
|
|
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 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 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) => {
|
|
// 这里可以实现节点编辑逻辑
|
|
console.log('编辑节点:', node);
|
|
setMenu(null);
|
|
}, []);
|
|
|
|
// 编辑边
|
|
const editEdge = useCallback((edge: Edge) => {
|
|
// 这里可以实现边编辑逻辑
|
|
console.log('编辑边:', edge);
|
|
setMenu(null);
|
|
}, []);
|
|
|
|
// 复制节点
|
|
const copyNode = useCallback((node: Node) => {
|
|
// 这里可以实现节点复制逻辑
|
|
console.log('复制节点:', node);
|
|
setMenu(null);
|
|
}, []);
|
|
|
|
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}
|
|
onDrop={onDrop}
|
|
onDragOver={onDragOver}
|
|
onNodeContextMenu={onNodeContextMenu}
|
|
onEdgeContextMenu={onEdgeContextMenu}
|
|
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>*/}
|
|
{/*</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>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
};
|
|
|
|
export default FlowEditorWithProvider; |