From ec3bdc9df58a325bf3aa02355bfe1ff743aa898d Mon Sep 17 00:00:00 2001 From: root <295172551@qq.com> Date: Sun, 17 Aug 2025 20:16:12 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9E=E7=BA=BF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/testcases/ReactFlowDesigner.tsx | 593 +++++++++--------- .../src/pages/testcases/ReactFlowTest.tsx | 117 ++++ 2 files changed, 412 insertions(+), 298 deletions(-) create mode 100644 src/X1.WebUI/src/pages/testcases/ReactFlowTest.tsx diff --git a/src/X1.WebUI/src/pages/testcases/ReactFlowDesigner.tsx b/src/X1.WebUI/src/pages/testcases/ReactFlowDesigner.tsx index 557c6ef..3c760ee 100644 --- a/src/X1.WebUI/src/pages/testcases/ReactFlowDesigner.tsx +++ b/src/X1.WebUI/src/pages/testcases/ReactFlowDesigner.tsx @@ -8,24 +8,23 @@ import ReactFlow, { useEdgesState, Controls, Background, - MiniMap, Panel, ReactFlowProvider, useReactFlow, + NodeMouseHandler, + Handle, + Position, } from 'reactflow'; import 'reactflow/dist/style.css'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Save, - RotateCcw, - Download, - Upload, Settings, Eye, EyeOff, Trash2, - Plus, Play, Square, GitBranch, @@ -39,8 +38,7 @@ import { Activity, Signal, SignalHigh, - SignalLow, - MoreHorizontal + SignalLow } from 'lucide-react'; import { TestStep } from '@/services/teststepsService'; @@ -64,7 +62,8 @@ interface ReactFlowDesignerProps { } // 自定义节点组件 -const TestStepNode = ({ data }: { data: any }) => { +const TestStepNode = ({ data, id, selected }: { data: any; id: string; selected?: boolean }) => { + const getIconComponent = (iconName: string) => { // 根据图标名称返回对应的图标组件 switch (iconName) { @@ -161,7 +160,11 @@ const TestStepNode = ({ data }: { data: any }) => {
{/* 开始和结束步骤使用圆形 */} {(data.stepType === 1 || data.stepType === 2) && ( -
+
{getIconComponent(data.icon || 'settings')} @@ -177,7 +180,11 @@ const TestStepNode = ({ data }: { data: any }) => { {/* 处理步骤使用矩形 */} {data.stepType === 3 && ( -
+
{getIconComponent(data.icon || 'settings')} @@ -193,7 +200,11 @@ const TestStepNode = ({ data }: { data: any }) => { {/* 判断步骤使用菱形 */} {data.stepType === 4 && ( -
+
{getIconComponent(data.icon || 'settings')} @@ -206,122 +217,247 @@ const TestStepNode = ({ data }: { data: any }) => {
)} + + {/* 连接点 - 根据节点类型显示不同的连接点 */} + {/* 开始节点 (type=1) - 只有输出连接点 */} + {data.stepType === 1 && ( + <> + + + + )} + + {/* 结束节点 (type=2) - 只有输入连接点 */} + {data.stepType === 2 && ( + <> + + + + )} + + {/* 处理节点 (type=3) - 有输入和输出连接点 */} + {data.stepType === 3 && ( + <> + + + + + + )} + + {/* 判断节点 (type=4) - 有输入和输出连接点 */} + {data.stepType === 4 && ( + <> + + + + + + )}
); }; +// 创建节点类型映射 const nodeTypes = { testStep: TestStepNode, }; function ReactFlowDesignerInner({ - flowSteps, - onSaveFlow, - onLoadFlow + onSaveFlow }: ReactFlowDesignerProps) { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); - const [showMiniMap, setShowMiniMap] = useState(true); const [showControls, setShowControls] = useState(true); - const [isDraggingOver, setIsDraggingOver] = useState(false); const [currentZoom, setCurrentZoom] = useState(1.5); - - // 初始化时打印默认缩放级别 - useEffect(() => { - console.log('React Flow initial zoom:', currentZoom); - }, []); + const [edgeType, setEdgeType] = useState<'step' | 'smoothstep' | 'straight'>('straight'); // 添加一个 ref 来直接访问 React Flow 容器 const reactFlowWrapper = useRef(null); - const { project } = useReactFlow(); + const { screenToFlowPosition } = useReactFlow(); + + // 使用ref存储函数,避免useEffect依赖问题 + const screenToFlowPositionRef = useRef(screenToFlowPosition); + const setNodesRef = useRef(setNodes); - // 备用拖拽事件监听器 + // 更新ref值 useEffect(() => { - const wrapper = reactFlowWrapper.current; - if (wrapper) { - console.log('Setting up backup drag events on wrapper:', wrapper); - - const handleDragOver = (e: DragEvent) => { - e.preventDefault(); - // 检查是否允许 move 操作,否则使用 copy - if (e.dataTransfer!.effectAllowed.includes('move')) { - e.dataTransfer!.dropEffect = 'move'; - } else { - e.dataTransfer!.dropEffect = 'copy'; - } - console.log('Backup dragover event triggered'); - }; - - const handleDrop = (e: DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - console.log('Backup drop event triggered'); - - const data = e.dataTransfer?.getData('application/json'); - console.log('Backup drop data:', data); - - if (data) { - try { - const parsedData = JSON.parse(data); - console.log('Backup parsed data:', parsedData); - if (parsedData.type === 'test-step') { - const { step } = parsedData; - // 获取 React Flow 容器的边界 - const reactFlowBounds = wrapper.getBoundingClientRect(); - const position = project({ - x: e.clientX - reactFlowBounds.left, - y: e.clientY - reactFlowBounds.top, - }); - - // 检查是否可以添加此类型的节点 - if (!canAddNodeType(step.stepType)) { - console.log('Cannot add node: type limit reached'); - return; - } - - const newNode = { - id: `node-${Date.now()}`, - type: 'testStep', - position, - data: { - stepId: step.id, - stepName: step.stepName, - stepType: step.stepType, - stepTypeName: step.stepTypeName, - description: step.description, - icon: step.icon, - }, - }; - - console.log('Adding node via backup drop:', newNode); - setNodes((nds) => nds.concat(newNode)); - } - } catch (error) { - console.error('Error in backup drop handler:', error); - } - } - }; + screenToFlowPositionRef.current = screenToFlowPosition; + setNodesRef.current = setNodes; + }, [screenToFlowPosition, setNodes]); + + // 连线验证规则 + const isValidConnection = useCallback((connection: Connection) => { + const sourceNode = nodes.find(node => node.id === connection.source); + const targetNode = nodes.find(node => node.id === connection.target); + + if (!sourceNode || !targetNode) { + return false; + } - wrapper.addEventListener('dragover', handleDragOver, { passive: false }); - wrapper.addEventListener('drop', handleDrop, { passive: false }); + const sourceType = sourceNode.data.stepType; + const targetType = targetNode.data.stepType; + + // 连线规则: + // 1. 不能从结束节点连出 + // 2. 不能连到开始节点 + // 3. 不能自己连自己 + // 4. 不能重复连线 + if (sourceType === 2) { + alert('结束节点不能作为连线的起点!'); + return false; + } + + if (targetType === 1) { + alert('不能连线到开始节点!'); + return false; + } - return () => { - wrapper.removeEventListener('dragover', handleDragOver); - wrapper.removeEventListener('drop', handleDrop); - }; + if (connection.source === connection.target) { + alert('不能连线到自己!'); + return false; } - }, [setNodes, project]); + + // 检查是否已存在相同的连线 + const existingEdge = edges.find(edge => + edge.source === connection.source && edge.target === connection.target + ); + if (existingEdge) { + alert('该连线已存在!'); + return false; + } + + return true; + }, [nodes, edges]); const onConnect = useCallback( - (params: Connection) => setEdges((eds) => addEdge(params, eds)), - [setEdges] + (params: Connection) => { + console.log('尝试连接:', params); + if (isValidConnection(params) && params.source && params.target) { + const newEdge = { + ...params, + id: `edge-${Date.now()}`, + type: edgeType, + animated: false, + style: { + stroke: '#3b82f6', + strokeWidth: 2, + }, + data: { + condition: 'default' + } + }; + console.log('创建新连线:', newEdge); + setEdges((eds) => { + const updatedEdges = addEdge(newEdge, eds); + console.log('当前所有连线:', updatedEdges); + return updatedEdges; + }); + } else { + console.log('连接验证失败'); + } + }, + [isValidConnection, setEdges, edgeType] ); + // 处理连线点击事件 + const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { + alert(`连线信息:\n从: ${edge.source}\n到: ${edge.target}\n条件: ${edge.data?.condition || '默认'}`); + }, []); + + // 处理连线双击事件 + const onEdgeDoubleClick = useCallback((event: React.MouseEvent, edge: Edge) => { + const newCondition = prompt('请输入连线条件:', edge.data?.condition || '默认'); + if (newCondition !== null) { + setEdges((currentEdges) => currentEdges.map(e => + e.id === edge.id + ? { ...e, data: { ...e.data, condition: newCondition } } + : e + )); + } + }, [setEdges]); + // 处理缩放事件 const onMove = useCallback((event: any, viewport: any) => { const { zoom } = viewport; setCurrentZoom(zoom); - console.log('React Flow zoom changed:', zoom); + }, []); + + // 点击画布取消选择 + const onPaneClick = useCallback(() => { + // 可以在这里添加画布点击逻辑 }, []); // 检查是否可以添加特定类型的节点 @@ -329,14 +465,12 @@ function ReactFlowDesignerInner({ if (stepType === 1) { // 开始步骤 const startNodes = nodes.filter(node => node.data.stepType === 1); if (startNodes.length > 0) { - console.log('Cannot add Start node: already exists'); alert('流程图中只能有一个开始节点!'); return false; } } else if (stepType === 2) { // 结束步骤 const endNodes = nodes.filter(node => node.data.stepType === 2); if (endNodes.length > 0) { - console.log('Cannot add End node: already exists'); alert('流程图中只能有一个结束节点!'); return false; } @@ -344,72 +478,41 @@ function ReactFlowDesignerInner({ return true; }, [nodes]); + // 使用ref存储canAddNodeType函数 + const canAddNodeTypeRef = useRef(canAddNodeType); + useEffect(() => { + canAddNodeTypeRef.current = canAddNodeType; + }, [canAddNodeType]); + // 重置缩放级别 const resetZoom = useCallback(() => { const reactFlowInstance = useReactFlow(); reactFlowInstance.setViewport({ x: 0, y: 0, zoom: 1.5 }); - console.log('React Flow zoom reset to 150%'); }, []); - const handleSave = () => { + const handleSave = useCallback(() => { onSaveFlow(nodes as any[], edges as any[]); - }; + }, [nodes, edges, onSaveFlow]); - const handleClear = () => { + const handleClear = useCallback(() => { setNodes([]); setEdges([]); - }; - - const handleUndo = () => { - // React Flow 内置了撤销/重做功能,通过 Controls 组件提供 - console.log('撤销功能通过 Controls 组件提供'); - }; - - const handleRedo = () => { - console.log('重做功能通过 Controls 组件提供'); - }; - - const handleExport = () => { - const data = { nodes, edges }; - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'test-flow.json'; - a.click(); - URL.revokeObjectURL(url); - }; - - const handleImport = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; - input.onchange = (e: any) => { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e: any) => { - try { - const data = JSON.parse(e.target.result); - setNodes(data.nodes || []); - setEdges(data.edges || []); - } catch (error) { - console.error('导入文件失败:', error); - } - }; - reader.readAsText(file); + }, [setNodes, setEdges]); + + // 删除选中的连线 + const handleDeleteSelected = useCallback(() => { + setEdges((currentEdges) => { + const selectedEdges = currentEdges.filter(edge => edge.selected); + if (selectedEdges.length > 0) { + return currentEdges.filter(edge => !edge.selected); } - }; - input.click(); - }; + return currentEdges; + }); + }, [setEdges]); - const toggleMiniMap = () => { - setShowMiniMap(!showMiniMap); - }; - - const toggleControls = () => { - setShowControls(!showControls); - }; + const toggleControls = useCallback(() => { + setShowControls((prev) => !prev); + }, []); // 处理拖拽事件 const onDragOver = useCallback((event: React.DragEvent) => { @@ -420,38 +523,29 @@ function ReactFlowDesignerInner({ } else { event.dataTransfer.dropEffect = 'copy'; } - console.log('React Flow onDragOver triggered'); }, []); const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); - console.log('React Flow onDrop triggered'); try { const data = event.dataTransfer.getData('application/json'); - console.log('React Flow drop data:', data); if (data) { const parsedData = JSON.parse(data); - console.log('React Flow parsed data:', parsedData); if (parsedData.type === 'test-step') { const { step } = parsedData; - // 使用 React Flow 的 project 函数计算位置 - // 获取 React Flow 容器的边界并计算相对位置 - const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect(); - const position = project({ - x: event.clientX - (reactFlowBounds?.left || 0), - y: event.clientY - (reactFlowBounds?.top || 0), + // 使用新的screenToFlowPosition函数,不需要手动计算边界 + const position = screenToFlowPositionRef.current({ + x: event.clientX, + y: event.clientY, }); - console.log('React Flow calculated position:', position); - // 检查是否可以添加此类型的节点 - if (!canAddNodeType(step.stepType)) { - console.log('Cannot add node: type limit reached'); + if (!canAddNodeTypeRef.current(step.stepType)) { return; } @@ -469,16 +563,14 @@ function ReactFlowDesignerInner({ }, }; - console.log('React Flow new node:', newNode); - setNodes((nds) => nds.concat(newNode)); - console.log('React Flow node added successfully'); + setNodesRef.current((nds) => nds.concat(newNode)); } } } catch (error) { console.error('React Flow drop error:', error); } }, - [setNodes, project] + [] // 移除依赖项,使用ref ); return ( @@ -490,15 +582,21 @@ function ReactFlowDesignerInner({ 流程图设计器
- + {/* 连线类型选择器 */} +
+ 连线类型: + +
+ - -
- {/* 添加一个拖拽区域覆盖整个画布 - 只在拖拽时激活 */} -
{ - e.preventDefault(); - setIsDraggingOver(true); - }} - onDragLeave={(e) => { - e.preventDefault(); - // 只有当真正离开容器时才设置 false - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - setIsDraggingOver(false); - } - }} - onDragOver={(e) => { - e.preventDefault(); - // 检查是否允许 move 操作,否则使用 copy - if (e.dataTransfer.effectAllowed.includes('move')) { - e.dataTransfer.dropEffect = 'move'; - } else { - e.dataTransfer.dropEffect = 'copy'; - } - console.log('Overlay dragover event triggered'); - }} - onDrop={(e) => { - e.preventDefault(); - e.stopPropagation(); - setIsDraggingOver(false); - console.log('Overlay drop event triggered'); - - const data = e.dataTransfer.getData('application/json'); - console.log('Overlay drop data:', data); - - if (data) { - try { - const parsedData = JSON.parse(data); - console.log('Overlay parsed data:', parsedData); - if (parsedData.type === 'test-step') { - const { step } = parsedData; - // 获取 React Flow 容器的边界并计算相对位置 - const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect(); - const position = project({ - x: e.clientX - (reactFlowBounds?.left || 0), - y: e.clientY - (reactFlowBounds?.top || 0), - }); - - // 检查是否可以添加此类型的节点 - if (!canAddNodeType(step.stepType)) { - console.log('Cannot add node: type limit reached'); - return; - } - - const newNode = { - id: `node-${Date.now()}`, - type: 'testStep', - position, - data: { - stepId: step.id, - stepName: step.stepName, - stepType: step.stepType, - stepTypeName: step.stepTypeName, - description: step.description, - icon: step.icon, - }, - }; - - console.log('Adding node via overlay drop:', newNode); - setNodes((nds) => nds.concat(newNode)); - } - } catch (error) { - console.error('Error in overlay drop handler:', error); - } - } - }} - /> {showControls && } - {showMiniMap && }
缩放: {Math.round(currentZoom * 100)}% @@ -678,6 +669,12 @@ function ReactFlowDesignerInner({
+ +
+
节点: {nodes.length}
+
连线: {edges.length}
+
+
diff --git a/src/X1.WebUI/src/pages/testcases/ReactFlowTest.tsx b/src/X1.WebUI/src/pages/testcases/ReactFlowTest.tsx new file mode 100644 index 0000000..daf84ef --- /dev/null +++ b/src/X1.WebUI/src/pages/testcases/ReactFlowTest.tsx @@ -0,0 +1,117 @@ +import React, { useState, useCallback } from 'react'; +import ReactFlow, { + Node, + Edge, + addEdge, + Connection, + useNodesState, + useEdgesState, + Handle, + Position, + ReactFlowProvider, +} from 'reactflow'; +import 'reactflow/dist/style.css'; + +// 简单的测试节点 +const TestNode = ({ data }: { data: any }) => { + return ( +
+
+
+ {data.emoji} +
+
+
{data.label}
+
{data.type}
+
+
+ + + +
+ ); +}; + +const nodeTypes = { + testNode: TestNode, +}; + +const initialNodes: Node[] = [ + { + id: '1', + type: 'testNode', + position: { x: 250, y: 100 }, + data: { label: '开始', type: '开始节点', emoji: '🚀' }, + }, + { + id: '2', + type: 'testNode', + position: { x: 250, y: 300 }, + data: { label: '处理', type: '处理节点', emoji: '⚙️' }, + }, + { + id: '3', + type: 'testNode', + position: { x: 250, y: 500 }, + data: { label: '结束', type: '结束节点', emoji: '🏁' }, + }, +]; + +function ReactFlowTestInner() { + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + const onConnect = useCallback( + (params: Connection) => { + console.log('连接参数:', params); + if (params.source && params.target) { + const newEdge = { + ...params, + id: `edge-${Date.now()}`, + type: 'smoothstep', + animated: false, + style: { stroke: '#3b82f6', strokeWidth: 2 }, + }; + console.log('创建新连线:', newEdge); + setEdges((eds) => { + const updatedEdges = addEdge(newEdge, eds); + console.log('当前所有连线:', updatedEdges); + return updatedEdges; + }); + } + }, + [setEdges] + ); + + return ( +
+ +
+ ); +} + +export default function ReactFlowTest() { + return ( + + + + ); +}