diff --git a/src/pages/flowEditor/components/customEdge.tsx b/src/pages/flowEditor/components/customEdge.tsx new file mode 100644 index 0000000..60235c3 --- /dev/null +++ b/src/pages/flowEditor/components/customEdge.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react'; + +const CustomEdge: React.FC = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style = {}, + markerEnd, + selected + }) => { + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition + }); + + return ( + <> + + +
+
+
+ + ); +}; + +export default CustomEdge; \ No newline at end of file diff --git a/src/pages/flowEditor/components/nodeContent.tsx b/src/pages/flowEditor/components/nodeContent.tsx index 74ca191..f3a4444 100644 --- a/src/pages/flowEditor/components/nodeContent.tsx +++ b/src/pages/flowEditor/components/nodeContent.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styles from '@/pages/flowEditor/node/style/base.module.less'; -import { Handle, Position } from '@xyflow/react'; +import { Handle, Position, useStore } from '@xyflow/react'; import { Divider } from '@arco-design/web-react'; interface NodeContentData { @@ -24,8 +24,13 @@ const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, inpu id={isStartNode ? 'start-source' : 'end-target'} style={{ background: '#2290f6', - top: '40px' + top: '40px', + width: '8px', + height: '8px', + border: '2px solid #fff', + boxShadow: '0 0 4px rgba(0,0,0,0.2)' }} + className="node-handle" /> {/* 为特殊节点的参数也添加句柄 */} @@ -37,8 +42,13 @@ const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, inpu id={outputs[index].name || `output-${index}`} style={{ background: '#555', - top: `${60 + index * 20}px` + top: `${60 + index * 20}px`, + width: '6px', + height: '6px', + border: '1px solid #fff', + boxShadow: '0 0 2px rgba(0,0,0,0.2)' }} + className="node-handle" /> ))} @@ -50,8 +60,13 @@ const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, inpu id={inputs[index].name || `input-${index}`} style={{ background: '#555', - top: `${60 + index * 20}px` + top: `${60 + index * 20}px`, + width: '6px', + height: '6px', + border: '1px solid #fff', + boxShadow: '0 0 2px rgba(0,0,0,0.2)' }} + className="node-handle" /> ))} @@ -68,8 +83,13 @@ const renderRegularNodeHandles = (inputs: any[], outputs: any[]) => { id="start-source" style={{ background: '#2290f6', - top: '40px' + top: '40px', + width: '8px', + height: '8px', + border: '2px solid #fff', + boxShadow: '0 0 4px rgba(0,0,0,0.2)' }} + className="node-handle" /> { id="end-target" style={{ background: '#2290f6', - top: '40px' + top: '40px', + width: '8px', + height: '8px', + border: '2px solid #fff', + boxShadow: '0 0 4px rgba(0,0,0,0.2)' }} + className="node-handle" /> {/* 输入参数连接端点 */} @@ -90,8 +115,13 @@ const renderRegularNodeHandles = (inputs: any[], outputs: any[]) => { id={inputs[index].name || `input-${index}`} style={{ background: '#555', - top: `${40 + (index + 1) * 20}px` + top: `${40 + (index + 1) * 20}px`, + width: '6px', + height: '6px', + border: '1px solid #fff', + boxShadow: '0 0 2px rgba(0,0,0,0.2)' }} + className="node-handle" /> ))} @@ -104,8 +134,13 @@ const renderRegularNodeHandles = (inputs: any[], outputs: any[]) => { id={outputs[index].name || `output-${index}`} style={{ background: '#555', - top: `${40 + (index + 1) * 20}px` + top: `${40 + (index + 1) * 20}px`, + width: '6px', + height: '6px', + border: '1px solid #fff', + boxShadow: '0 0 2px rgba(0,0,0,0.2)' }} + className="node-handle" /> ))} diff --git a/src/pages/flowEditor/index.tsx b/src/pages/flowEditor/index.tsx index 63bf0b8..5f4bed4 100644 --- a/src/pages/flowEditor/index.tsx +++ b/src/pages/flowEditor/index.tsx @@ -11,7 +11,9 @@ import { ReactFlowProvider, useReactFlow, NodeTypes, - Panel + EdgeTypes, + Panel, + SelectionMode } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import TextUpdaterNode from './node/textUpdateNode/TextUpdaterNode'; @@ -22,6 +24,7 @@ import BasicNode from './node/basicNode/BasicNode'; import SideBar from './sideBar/sideBar'; import { convertFlowData } from '@/utils/convertFlowData'; import { exampleFlowData } from '@/pages/flowEditor/test/exampleFlowData'; +import CustomEdge from './components/customEdge'; const nodeTypes: NodeTypes = { textUpdater: TextUpdaterNode, @@ -31,8 +34,18 @@ const nodeTypes: NodeTypes = { BASIC: BasicNode }; +const edgeTypes: EdgeTypes = { + custom: CustomEdge +}; + const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(exampleFlowData); +// 为所有边添加类型 +const initialEdges: Edge[] = convertedEdges.map(edge => ({ + ...edge, + type: 'custom' +})); + const FlowEditorWithProvider: React.FC = () => { return (
@@ -46,7 +59,7 @@ const FlowEditorWithProvider: React.FC = () => { const FlowEditor: React.FC = () => { const [nodes, setNodes] = useState(convertedNodes); - const [edges, setEdges] = useState(convertedEdges); + const [edges, setEdges] = useState(initialEdges); const reactFlowInstance = useReactFlow(); const reactFlowWrapper = useRef(null); @@ -59,7 +72,7 @@ const FlowEditor: React.FC = () => { [] ); const onConnect = useCallback( - (params: any) => setEdges((edgesSnapshot) => addEdge(params, edgesSnapshot)), + (params: any) => setEdges((edgesSnapshot) => addEdge({ ...params, type: 'custom' }, edgesSnapshot)), [] ); @@ -102,12 +115,19 @@ const FlowEditor: React.FC = () => { nodes={nodes} edges={edges} nodeTypes={nodeTypes} + edgeTypes={edgeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onDrop={onDrop} onDragOver={onDragOver} fitView + selectionKeyCode={['Meta', 'Control']} + selectionMode={SelectionMode.Partial} + panOnDrag={[0, 1, 2]} // 支持多点触控平移 + zoomOnScroll={true} + zoomOnPinch={true} + panOnScrollSpeed={0.5} > diff --git a/src/pages/flowEditor/node/basicNode/BasicNode.tsx b/src/pages/flowEditor/node/basicNode/BasicNode.tsx index d0747c4..333fd4d 100644 --- a/src/pages/flowEditor/node/basicNode/BasicNode.tsx +++ b/src/pages/flowEditor/node/basicNode/BasicNode.tsx @@ -1,6 +1,7 @@ import React from 'react'; import styles from '@/pages/flowEditor/node/style/base.module.less'; import NodeContent from '@/pages/flowEditor/components/nodeContent'; +import { useStore } from '@xyflow/react'; interface BasicNodeData { title?: string; @@ -13,15 +14,21 @@ interface BasicNodeData { [key: string]: any; } -const BasicNode = ({ data }: { data: BasicNodeData }) => { +const BasicNode = ({ data, id }: { data: BasicNodeData; id: string }) => { const title = data.title || '基础节点'; + + // 获取节点选中状态 - 适配React Flow v12 API + const isSelected = useStore((state) => + state.nodeLookup.get(id)?.selected || false + ); + return ( -
-
+
+
{title}
- +
); }; diff --git a/src/pages/flowEditor/node/draggableNode/DraggableNode.tsx b/src/pages/flowEditor/node/draggableNode/DraggableNode.tsx index 09c9ae7..77a6fb1 100644 --- a/src/pages/flowEditor/node/draggableNode/DraggableNode.tsx +++ b/src/pages/flowEditor/node/draggableNode/DraggableNode.tsx @@ -1,12 +1,18 @@ import React from 'react'; -import { Handle, Position } from '@xyflow/react'; +import { Handle, Position, useStore } from '@xyflow/react'; import styles from '@/pages/flowEditor/node/style/base.module.less'; import NodeContent from '@/pages/flowEditor/components/nodeContent'; -const DraggableNode = ({ data }: { data: any }) => { +const DraggableNode = ({ data, id }: { data: any; id: string }) => { const title = data.title || '任务节点'; + + // 获取节点选中状态 - 适配React Flow v12 API + const isSelected = useStore((state) => + state.nodeLookup.get(id)?.selected || false + ); + return ( -
+
{title}
diff --git a/src/pages/flowEditor/node/endNode/EndNode.tsx b/src/pages/flowEditor/node/endNode/EndNode.tsx index fd8df34..0c89d43 100644 --- a/src/pages/flowEditor/node/endNode/EndNode.tsx +++ b/src/pages/flowEditor/node/endNode/EndNode.tsx @@ -1,6 +1,7 @@ import React from 'react'; import styles from '@/pages/flowEditor/node/style/base.module.less'; import NodeContent from '@/pages/flowEditor/components/nodeContent'; +import { useStore } from '@xyflow/react'; interface EndNodeData { title?: string; @@ -13,11 +14,16 @@ interface EndNodeData { [key: string]: any; } -const EndNode = ({ data }: { data: EndNodeData }) => { +const EndNode = ({ data, id }: { data: EndNodeData; id: string }) => { const title = data.title || '结束'; + // 获取节点选中状态 - 适配React Flow v12 API + const isSelected = useStore((state) => + state.nodeLookup.get(id)?.selected || false + ); + return ( -
+
{title}
diff --git a/src/pages/flowEditor/node/startNode/StartNode.tsx b/src/pages/flowEditor/node/startNode/StartNode.tsx index 0f6e935..e270c51 100644 --- a/src/pages/flowEditor/node/startNode/StartNode.tsx +++ b/src/pages/flowEditor/node/startNode/StartNode.tsx @@ -1,6 +1,7 @@ import React from 'react'; import styles from '@/pages/flowEditor/node/style/base.module.less'; import NodeContent from '@/pages/flowEditor/components/nodeContent'; +import { useStore } from '@xyflow/react'; interface StartNodeData { title?: string; @@ -13,15 +14,21 @@ interface StartNodeData { [key: string]: any; } -const StartNode = ({ data }: { data: StartNodeData }) => { +const StartNode = ({ data, id }: { data: StartNodeData; id: string }) => { const title = data.title || '开始'; + + // 获取节点选中状态 - 适配React Flow v12 API + const isSelected = useStore((state) => + state.nodeLookup.get(id)?.selected || false + ); + return ( -
+
{title}
- +
); }; diff --git a/src/pages/flowEditor/node/style/base.module.less b/src/pages/flowEditor/node/style/base.module.less index c1b14f0..c677da4 100644 --- a/src/pages/flowEditor/node/style/base.module.less +++ b/src/pages/flowEditor/node/style/base.module.less @@ -5,6 +5,12 @@ min-width: 150px; font-size: 14px; box-shadow: 0px 5px 15px #ccc; + border: 2px solid transparent; + + &.selected { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } .node-header { padding: 5px 15px;