From c0f7ffabf833ec4ab1bbcc0c89121fb74d492873 Mon Sep 17 00:00:00 2001 From: ZLY Date: Mon, 15 Sep 2025 15:40:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(flowEditor):=20=E5=AE=9E=E7=8E=B0=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E5=9B=BE=E4=B8=AD=E5=9C=A8=E8=BE=B9=E4=B8=8A=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=8A=82=E7=82=B9=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 CustomEdge 组件中添加悬停状态和添加节点按钮 - 在 EdgeContextMenu 中添加"添加节点"选项 - 在 FlowEditor组件中实现添加节点的逻辑 - 新增 AddNodeMenu、EdgeAddNodeButton 和 PaneContextMenu 组件用于添加节点 - 优化流程图的右键菜单,支持在画布空白处添加节点 --- .../flowEditor/components/addNodeMenu.tsx | 72 ++++++ .../flowEditor/components/customEdge.tsx | 44 +++- .../components/edgeAddNodeButton.tsx | 49 ++++ .../flowEditor/components/edgeContextMenu.tsx | 20 +- .../flowEditor/components/nodeContent.tsx | 1 - .../flowEditor/components/paneContextMenu.tsx | 44 ++++ src/pages/flowEditor/index.tsx | 236 +++++++++++++++++- 7 files changed, 456 insertions(+), 10 deletions(-) create mode 100644 src/pages/flowEditor/components/addNodeMenu.tsx create mode 100644 src/pages/flowEditor/components/edgeAddNodeButton.tsx create mode 100644 src/pages/flowEditor/components/paneContextMenu.tsx diff --git a/src/pages/flowEditor/components/addNodeMenu.tsx b/src/pages/flowEditor/components/addNodeMenu.tsx new file mode 100644 index 0000000..12525af --- /dev/null +++ b/src/pages/flowEditor/components/addNodeMenu.tsx @@ -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 = ({ + onAddNode +}) => { + // 按分组组织节点数据 + const groupedNodes = localNodeData.reduce((acc, node) => { + if (!acc[node.nodeGroup]) { + acc[node.nodeGroup] = []; + } + acc[node.nodeGroup].push(node); + return acc; + }, {} as Record); + + const handleAddNode = (nodeType: string) => { + onAddNode(nodeType); + }; + + // 分组名称映射 + const groupNames: Record = { + 'common': '系统组件' + // 可以根据需要添加更多分组 + // 'application': '应用组件', + // 'composite': '复合组件' + }; + + return ( + + {Object.entries(groupedNodes).map(([group, nodes]) => ( + + {nodes.map((node) => ( + handleAddNode(node.nodeType)} + style={{ + padding: '0 16px', + height: 36, + lineHeight: '36px' + }} + > + {node.nodeName} + + ))} + + ))} + + ); +}; + +export default AddNodeMenu; \ No newline at end of file diff --git a/src/pages/flowEditor/components/customEdge.tsx b/src/pages/flowEditor/components/customEdge.tsx index 60235c3..b392d55 100644 --- a/src/pages/flowEditor/components/customEdge.tsx +++ b/src/pages/flowEditor/components/customEdge.tsx @@ -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 = ({ id, @@ -11,7 +12,8 @@ const CustomEdge: React.FC = ({ targetPosition, style = {}, markerEnd, - selected + selected, + data }) => { const [edgePath, labelX, labelY] = getBezierPath({ sourceX, @@ -22,6 +24,30 @@ const CustomEdge: React.FC = ({ 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 ( <> = ({ }} className="nodrag nopan" > + {hovered && ( + + )} + {/* 悬停时显示的高亮线条 */} + {hovered && ( + + )} ); }; diff --git a/src/pages/flowEditor/components/edgeAddNodeButton.tsx b/src/pages/flowEditor/components/edgeAddNodeButton.tsx new file mode 100644 index 0000000..7fe1fcf --- /dev/null +++ b/src/pages/flowEditor/components/edgeAddNodeButton.tsx @@ -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 = ({ + onClick, + style +}) => { + return ( +
+
+ ); +}; + +export default EdgeAddNodeButton; \ No newline at end of file diff --git a/src/pages/flowEditor/components/edgeContextMenu.tsx b/src/pages/flowEditor/components/edgeContextMenu.tsx index 399a0bd..444a9d2 100644 --- a/src/pages/flowEditor/components/edgeContextMenu.tsx +++ b/src/pages/flowEditor/components/edgeContextMenu.tsx @@ -6,12 +6,14 @@ interface EdgeContextMenuProps { edge: Edge; onDelete?: (edge: Edge) => void; onEdit?: (edge: Edge) => void; + onAddNode?: (edge: Edge) => void; // 添加在边上添加节点的功能 } const EdgeContextMenu: React.FC = ({ edge, onDelete, - onEdit + onEdit, + onAddNode }) => { const handleDelete = () => { onDelete && onDelete(edge); @@ -21,8 +23,22 @@ const EdgeContextMenu: React.FC = ({ onEdit && onEdit(edge); }; + // 添加在边上添加节点的处理函数 + const handleAddNode = () => { + onAddNode && onAddNode(edge); + }; + return ( - + + + 添加节点 + 编辑连接 diff --git a/src/pages/flowEditor/components/nodeContent.tsx b/src/pages/flowEditor/components/nodeContent.tsx index 6e237bd..f2f7793 100644 --- a/src/pages/flowEditor/components/nodeContent.tsx +++ b/src/pages/flowEditor/components/nodeContent.tsx @@ -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'; diff --git a/src/pages/flowEditor/components/paneContextMenu.tsx b/src/pages/flowEditor/components/paneContextMenu.tsx new file mode 100644 index 0000000..2bd3ae5 --- /dev/null +++ b/src/pages/flowEditor/components/paneContextMenu.tsx @@ -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 = ({ + onAddNode, + position + }) => { + // 包装onAddNode函数以适配AddNodeMenu组件的接口 + const handleAddNode = (nodeType: string) => { + onAddNode(nodeType, position); + }; + + return ( + + +
+ +
+
+
+ ); +}; + +export default PaneContextMenu; \ No newline at end of file diff --git a/src/pages/flowEditor/index.tsx b/src/pages/flowEditor/index.tsx index ed0405d..c910c25 100644 --- a/src/pages/flowEditor/index.tsx +++ b/src/pages/flowEditor/index.tsx @@ -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 ( -
+
e.preventDefault()}> @@ -51,9 +54,10 @@ const FlowEditor: React.FC = () => { const reactFlowWrapper = useRef(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(null); const [isEditModalOpen, setIsEditModalOpen] = useState(false); + // 添加节点选择弹窗状态 + const [edgeForNodeAdd, setEdgeForNodeAdd] = useState(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 ( -
+
e.preventDefault()}> { 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); // 关闭上下文菜单 + }} + /> +
+ )} + + {/*画布右键上下文*/} + {menu && menu.type === 'pane' && ( +
+ { + addNodeOnPane(nodeType, position); + setMenu(null); // 关闭上下文菜单 + }} />
)} @@ -499,6 +705,26 @@ const FlowEditor: React.FC = () => { onSave={saveNodeEdit} onClose={closeEditModal} /> + + {/*统一的添加节点菜单*/} + {(edgeForNodeAdd || positionForNodeAdd) && ( +
+ +
+ )} +
);