feat(flowEditor): 实现流程图中在边上添加节点的功能

- 在 CustomEdge 组件中添加悬停状态和添加节点按钮
- 在 EdgeContextMenu 中添加"添加节点"选项
- 在 FlowEditor组件中实现添加节点的逻辑
- 新增 AddNodeMenu、EdgeAddNodeButton 和 PaneContextMenu 组件用于添加节点
- 优化流程图的右键菜单,支持在画布空白处添加节点
master
钟良源 5 months ago
parent 09222ca3b9
commit c0f7ffabf8

@ -0,0 +1,72 @@
import React from 'react';
import { Menu } from '@arco-design/web-react';
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
interface AddNodeMenuProps {
onAddNode: (nodeType: string) => void;
position?: { x: number; y: number }; // 用于画布上下文菜单
edgeId?: string; // 用于边上下文菜单
}
const AddNodeMenu: React.FC<AddNodeMenuProps> = ({
onAddNode
}) => {
// 按分组组织节点数据
const groupedNodes = localNodeData.reduce((acc, node) => {
if (!acc[node.nodeGroup]) {
acc[node.nodeGroup] = [];
}
acc[node.nodeGroup].push(node);
return acc;
}, {} as Record<string, typeof localNodeData>);
const handleAddNode = (nodeType: string) => {
onAddNode(nodeType);
};
// 分组名称映射
const groupNames: Record<string, string> = {
'common': '系统组件'
// 可以根据需要添加更多分组
// 'application': '应用组件',
// 'composite': '复合组件'
};
return (
<Menu
style={{
width: 200,
border: '1px solid #e4e7ed',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
mode="vertical"
hasCollapseButton={false}
>
{Object.entries(groupedNodes).map(([group, nodes]) => (
<Menu.SubMenu
key={group}
title={groupNames[group] || group}
popup
trigger="hover"
>
{nodes.map((node) => (
<Menu.Item
key={node.nodeType}
onClick={() => handleAddNode(node.nodeType)}
style={{
padding: '0 16px',
height: 36,
lineHeight: '36px'
}}
>
{node.nodeName}
</Menu.Item>
))}
</Menu.SubMenu>
))}
</Menu>
);
};
export default AddNodeMenu;

@ -1,5 +1,6 @@
import React from 'react';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath, useReactFlow } from '@xyflow/react';
import EdgeAddNodeButton from './edgeAddNodeButton';
const CustomEdge: React.FC<EdgeProps> = ({
id,
@ -11,7 +12,8 @@ const CustomEdge: React.FC<EdgeProps> = ({
targetPosition,
style = {},
markerEnd,
selected
selected,
data
}) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
@ -22,6 +24,30 @@ const CustomEdge: React.FC<EdgeProps> = ({
targetPosition
});
// 从数据中获取悬停状态
const hovered = data?.hovered || false;
// 使用useReactFlow钩子获取setEdges方法
const { setEdges } = useReactFlow();
// 边点击处理函数
const handleEdgeAddNode = () => {
console.log('handleEdgeAddNode called for edge:', id);
// 更新边的数据,触发边上添加节点的流程
setEdges(eds => eds.map(edge => {
if (edge.id === id) {
return {
...edge,
data: {
...edge.data,
addNodeTrigger: true
}
};
}
return edge;
}));
};
return (
<>
<BaseEdge
@ -43,8 +69,22 @@ const CustomEdge: React.FC<EdgeProps> = ({
}}
className="nodrag nopan"
>
{hovered && (
<EdgeAddNodeButton
onClick={handleEdgeAddNode}
/>
)}
</div>
</EdgeLabelRenderer>
{/* 悬停时显示的高亮线条 */}
{hovered && (
<path
d={edgePath}
fill="none"
stroke="#1890ff"
strokeWidth={2}
/>
)}
</>
);
};

@ -0,0 +1,49 @@
import React from 'react';
import { Button } from '@arco-design/web-react';
import { IconPlus } from '@arco-design/web-react/icon';
interface EdgeAddNodeButtonProps {
onClick: () => void;
style?: React.CSSProperties;
}
const EdgeAddNodeButton: React.FC<EdgeAddNodeButtonProps> = ({
onClick,
style
}) => {
return (
<div
style={{
position: 'absolute',
transform: 'translate(-50%, -50%)',
pointerEvents: 'all',
...style
}}
className="nodrag nopan"
>
<Button
size="mini"
icon={<IconPlus />}
onClick={(e) => {
e.stopPropagation();
onClick();
}}
style={{
width: 20,
height: 20,
minWidth: 20,
padding: 0,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1890ff', // 使用项目主题蓝色
borderColor: '#1890ff',
color: '#ffffff'
}}
/>
</div>
);
};
export default EdgeAddNodeButton;

@ -6,12 +6,14 @@ interface EdgeContextMenuProps {
edge: Edge;
onDelete?: (edge: Edge) => void;
onEdit?: (edge: Edge) => void;
onAddNode?: (edge: Edge) => void; // 添加在边上添加节点的功能
}
const EdgeContextMenu: React.FC<EdgeContextMenuProps> = ({
edge,
onDelete,
onEdit
onEdit,
onAddNode
}) => {
const handleDelete = () => {
onDelete && onDelete(edge);
@ -21,8 +23,22 @@ const EdgeContextMenu: React.FC<EdgeContextMenuProps> = ({
onEdit && onEdit(edge);
};
// 添加在边上添加节点的处理函数
const handleAddNode = () => {
onAddNode && onAddNode(edge);
};
return (
<Menu>
<Menu
style={{
border: '1px solid #e4e7ed',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
>
<Menu.Item key="add-node" onClick={handleAddNode}>
</Menu.Item>
<Menu.Item key="edit" onClick={handleEdit}>
</Menu.Item>

@ -206,7 +206,6 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
const dataOuts = data.parameters?.dataOuts || [];
const showFooter = data?.component?.customDef || false;
const footerData = (showFooter && data.component) || {};
console.log('apiIns,apiOuts:', apiIns, apiOuts);
// 判断节点类型
const isStartNode = data.type === 'start';

@ -0,0 +1,44 @@
import React from 'react';
import { Menu } from '@arco-design/web-react';
import AddNodeMenu from './addNodeMenu';
interface PaneContextMenuProps {
onAddNode: (nodeType: string, position: { x: number; y: number }) => void;
position: { x: number; y: number };
}
const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
onAddNode,
position
}) => {
// 包装onAddNode函数以适配AddNodeMenu组件的接口
const handleAddNode = (nodeType: string) => {
onAddNode(nodeType, position);
};
return (
<Menu
mode="pop"
style={{
minWidth: 200,
border: '1px solid #e4e7ed',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
>
<Menu.SubMenu
key="add-node"
title="添加节点"
popup
>
<div style={{
padding: '4px 0'
}}>
<AddNodeMenu onAddNode={handleAddNode} />
</div>
</Menu.SubMenu>
</Menu>
);
};
export default PaneContextMenu;

@ -17,7 +17,7 @@ import {
Panel
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { Button } from '@arco-design/web-react';
import { Button, Modal } from '@arco-design/web-react';
import { nodeTypeMap, nodeTypes, registerNodeType } from './node';
import SideBar from './sideBar/sideBar';
import { convertFlowData } from '@/utils/convertFlowData';
@ -26,8 +26,11 @@ import LocalNode from '@/pages/flowEditor/node/localNode/LocalNode';
import CustomEdge from './components/customEdge';
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 { defaultNodeTypes } from '@/pages/flowEditor/node/types/defaultType';
import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData';
const edgeTypes: EdgeTypes = {
custom: CustomEdge
@ -35,7 +38,7 @@ const edgeTypes: EdgeTypes = {
const FlowEditorWithProvider: React.FC = () => {
return (
<div style={{ width: '100%', height: '91vh', display: 'flex' }}>
<div style={{ width: '100%', height: '91vh', display: 'flex' }} onContextMenu={(e) => e.preventDefault()}>
<ReactFlowProvider>
<SideBar />
<FlowEditor />
@ -51,9 +54,10 @@ const FlowEditor: React.FC = () => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [menu, setMenu] = useState<{
id: string;
type: 'node' | 'edge';
type: 'node' | 'edge' | 'pane';
top: number;
left: number;
position?: { x: number; y: number };
} | null>(null);
const store = useStoreApi();
@ -61,6 +65,10 @@ const FlowEditor: React.FC = () => {
const [editingNode, setEditingNode] = useState<Node | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
// 添加节点选择弹窗状态
const [edgeForNodeAdd, setEdgeForNodeAdd] = useState<Edge | null>(null);
const [positionForNodeAdd, setPositionForNodeAdd] = useState<{ x: number, y: number } | null>(null);
// 获取handle类型 (api或data)
const getHandleType = (handleId: string, nodeParams: any) => {
// 检查是否为api类型的handle
@ -273,10 +281,32 @@ const FlowEditor: React.FC = () => {
...edge,
type: 'custom'
}));
setNodes(convertedNodes);
setEdges(initialEdges);
}, []);
// 监听边的变化,处理添加节点的触发
useEffect(() => {
const edgeToAddNode = edges.find(edge => edge.data?.addNodeTrigger);
if (edgeToAddNode) {
console.log('Triggering add node for edge:', edgeToAddNode.id);
setEdgeForNodeAdd(edgeToAddNode);
// 清除触发标志
setEdges(eds => eds.map(edge => {
if (edge.id === edgeToAddNode.id) {
const { addNodeTrigger, ...restData } = edge.data || {};
return {
...edge,
data: restData
};
}
return edge;
}));
}
}, [edges]);
// 节点右键菜单处理
const onNodeContextMenu = useCallback(
(event: React.MouseEvent, node: Node) => {
@ -324,6 +354,31 @@ const FlowEditor: React.FC = () => {
[setMenu]
);
// 画布右键菜单处理
const onPaneContextMenu = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
const pane = reactFlowWrapper.current?.getBoundingClientRect();
if (!pane || !reactFlowInstance) return;
// 计算在画布中的位置
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY
});
setMenu({
id: 'pane-context-menu',
type: 'pane',
top: event.clientY - pane.top,
left: event.clientX - pane.left,
position
});
},
[reactFlowInstance]
);
// 点击画布其他区域关闭菜单
const onPaneClick = useCallback(() => setMenu(null), [setMenu]);
@ -387,6 +442,116 @@ const FlowEditor: React.FC = () => {
setMenu(null);
}, []);
// 在边上添加节点的具体实现
const addNodeOnEdge = useCallback((nodeType: string) => {
if (!edgeForNodeAdd || !reactFlowInstance) return;
// 查找节点定义
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType);
if (!nodeDefinition) return;
// 获取源节点和目标节点
const sourceNode = nodes.find(n => n.id === edgeForNodeAdd.source);
const targetNode = nodes.find(n => n.id === edgeForNodeAdd.target);
if (!sourceNode || !targetNode) return;
// 计算中点位置
const position = {
x: (sourceNode.position.x + targetNode.position.x) / 2,
y: (sourceNode.position.y + targetNode.position.y) / 2
};
// 创建新节点
const newNode = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
...nodeDefinition.data,
title: nodeDefinition.nodeName,
type: nodeType
}
};
// 将未定义的节点动态追加进nodeTypes
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, LocalNode, nodeDefinition.nodeName);
// 添加新节点
setNodes((nds) => [...nds, newNode]);
// 删除旧边
setEdges((eds) => eds.filter(e => e.id !== edgeForNodeAdd.id));
// 创建新边: source -> new node, new node -> target
setEdges((eds) => [
...eds,
{
id: `e${edgeForNodeAdd.source}-${newNode.id}`,
source: edgeForNodeAdd.source,
target: newNode.id,
type: 'custom'
},
{
id: `e${newNode.id}-${edgeForNodeAdd.target}`,
source: newNode.id,
target: edgeForNodeAdd.target,
type: 'custom'
}
]);
// 关闭菜单
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
}, [edgeForNodeAdd, nodes, reactFlowInstance]);
// 在画布上添加节点
const addNodeOnPane = useCallback((nodeType: string, position: { x: number; y: number }) => {
setMenu(null);
if (!reactFlowInstance) return;
// 查找节点定义
const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType);
if (!nodeDefinition) return;
// 创建新节点
const newNode = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
...nodeDefinition.data,
title: nodeDefinition.nodeName,
type: nodeType
}
};
// 将未定义的节点动态追加进nodeTypes
const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key));
// 目前默认添加的都是系统组件/本地组件
if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, LocalNode, nodeDefinition.nodeName);
setNodes((nds) => [...nds, newNode]);
}, [reactFlowInstance]);
// 处理添加节点的统一方法
const handleAddNode = useCallback((nodeType: string) => {
// 如果是通过边添加节点
if (edgeForNodeAdd) {
addNodeOnEdge(nodeType);
}
// 如果是通过画布添加节点
else if (positionForNodeAdd) {
addNodeOnPane(nodeType, positionForNodeAdd);
}
// 清除状态
setEdgeForNodeAdd(null);
setPositionForNodeAdd(null);
}, [edgeForNodeAdd, positionForNodeAdd, addNodeOnEdge, addNodeOnPane]);
// 保存所有节点和边数据到服务器
const saveFlowDataToServer = useCallback(async () => {
try {
@ -422,7 +587,8 @@ const FlowEditor: React.FC = () => {
}, [nodes, edges]);
return (
<div ref={reactFlowWrapper} style={{ width: '100%', height: '100%', position: 'relative' }}>
<div ref={reactFlowWrapper} style={{ width: '100%', height: '100%', position: 'relative' }}
onContextMenu={(e) => e.preventDefault()}>
<ReactFlow
nodes={nodes}
edges={edges}
@ -438,7 +604,23 @@ const FlowEditor: React.FC = () => {
onEdgeContextMenu={onEdgeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
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
selectionKeyCode={['Meta', 'Control']}
selectionMode={SelectionMode.Partial}
@ -488,6 +670,30 @@ const FlowEditor: React.FC = () => {
edge={edges.find(e => e.id === menu.id)!}
onDelete={deleteEdge}
onEdit={editEdge}
onAddNode={(edge) => {
setEdgeForNodeAdd(edge);
setMenu(null); // 关闭上下文菜单
}}
/>
</div>
)}
{/*画布右键上下文*/}
{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 }) => {
addNodeOnPane(nodeType, position);
setMenu(null); // 关闭上下文菜单
}}
/>
</div>
)}
@ -499,6 +705,26 @@ const FlowEditor: React.FC = () => {
onSave={saveNodeEdit}
onClose={closeEditModal}
/>
{/*统一的添加节点菜单*/}
{(edgeForNodeAdd || positionForNodeAdd) && (
<div
style={{
position: 'absolute',
top: edgeForNodeAdd ? '50%' : (positionForNodeAdd?.y || 0),
left: edgeForNodeAdd ? '50%' : (positionForNodeAdd?.x || 0),
zIndex: 1000,
transform: edgeForNodeAdd ? 'translate(-50%, -50%)' : 'none'
}}
>
<AddNodeMenu
onAddNode={handleAddNode}
position={positionForNodeAdd || undefined}
edgeId={edgeForNodeAdd?.id}
/>
</div>
)}
</div>
);

Loading…
Cancel
Save