|
|
|
@ -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 }) => { |
|
|
|
<div className={`group relative transition-all duration-200`}> |
|
|
|
{/* 开始和结束步骤使用圆形 */} |
|
|
|
{(data.stepType === 1 || data.stepType === 2) && ( |
|
|
|
<div className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor}`}> |
|
|
|
<div |
|
|
|
className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor} ${ |
|
|
|
selected ? 'ring-2 ring-blue-500' : '' |
|
|
|
}`}
|
|
|
|
> |
|
|
|
<div className="flex items-center space-x-1"> |
|
|
|
<div className={`flex-shrink-0 w-3 h-3 rounded-lg ${getIconBgColor(data.icon || 'settings')} flex items-center justify-center`}> |
|
|
|
{getIconComponent(data.icon || 'settings')} |
|
|
|
@ -177,7 +180,11 @@ const TestStepNode = ({ data }: { data: any }) => { |
|
|
|
|
|
|
|
{/* 处理步骤使用矩形 */} |
|
|
|
{data.stepType === 3 && ( |
|
|
|
<div className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor}`}> |
|
|
|
<div |
|
|
|
className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor} ${ |
|
|
|
selected ? 'ring-2 ring-blue-500' : '' |
|
|
|
}`}
|
|
|
|
> |
|
|
|
<div className="flex items-center space-x-1"> |
|
|
|
<div className={`flex-shrink-0 w-3 h-3 rounded-lg ${getIconBgColor(data.icon || 'settings')} flex items-center justify-center`}> |
|
|
|
{getIconComponent(data.icon || 'settings')} |
|
|
|
@ -193,7 +200,11 @@ const TestStepNode = ({ data }: { data: any }) => { |
|
|
|
|
|
|
|
{/* 判断步骤使用菱形 */} |
|
|
|
{data.stepType === 4 && ( |
|
|
|
<div className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor}`}> |
|
|
|
<div |
|
|
|
className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor} ${ |
|
|
|
selected ? 'ring-2 ring-blue-500' : '' |
|
|
|
}`}
|
|
|
|
> |
|
|
|
<div className="flex items-center space-x-1"> |
|
|
|
<div className={`flex-shrink-0 w-3 h-3 rounded-lg ${getIconBgColor(data.icon || 'settings')} flex items-center justify-center`}> |
|
|
|
{getIconComponent(data.icon || 'settings')} |
|
|
|
@ -206,122 +217,247 @@ const TestStepNode = ({ data }: { data: any }) => { |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* 连接点 - 根据节点类型显示不同的连接点 */} |
|
|
|
{/* 开始节点 (type=1) - 只有输出连接点 */} |
|
|
|
{data.stepType === 1 && ( |
|
|
|
<> |
|
|
|
<Handle |
|
|
|
type="source" |
|
|
|
position={Position.Right} |
|
|
|
id="right" |
|
|
|
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|
|
|
style={{ right: -6 }} |
|
|
|
/> |
|
|
|
<Handle |
|
|
|
type="source" |
|
|
|
position={Position.Bottom} |
|
|
|
id="bottom" |
|
|
|
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|
|
|
style={{ bottom: -6 }} |
|
|
|
/> |
|
|
|
</> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* 结束节点 (type=2) - 只有输入连接点 */} |
|
|
|
{data.stepType === 2 && ( |
|
|
|
<> |
|
|
|
<Handle |
|
|
|
type="target" |
|
|
|
position={Position.Top} |
|
|
|
id="top" |
|
|
|
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|
|
|
style={{ top: -6 }} |
|
|
|
/> |
|
|
|
<Handle |
|
|
|
type="target" |
|
|
|
position={Position.Left} |
|
|
|
id="left" |
|
|
|
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|
|
|
style={{ left: -6 }} |
|
|
|
/> |
|
|
|
</> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* 处理节点 (type=3) - 有输入和输出连接点 */} |
|
|
|
{data.stepType === 3 && ( |
|
|
|
<> |
|
|
|
<Handle |
|
|
|
type="target" |
|
|
|
position={Position.Top} |
|
|
|
id="top" |
|
|
|
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|
|
|
style={{ top: -6 }} |
|
|
|
/> |
|
|
|
<Handle |
|
|
|
type="source" |
|
|
|
position={Position.Right} |
|
|
|
id="right" |
|
|
|
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|
|
|
style={{ right: -6 }} |
|
|
|
/> |
|
|
|
<Handle |
|
|
|
type="source" |
|
|
|
position={Position.Bottom} |
|
|
|
id="bottom" |
|
|
|
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|
|
|
style={{ bottom: -6 }} |
|
|
|
/> |
|
|
|
<Handle |
|
|
|
type="target" |
|
|
|
position={Position.Left} |
|
|
|
id="left" |
|
|
|
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|
|
|
style={{ left: -6 }} |
|
|
|
/> |
|
|
|
</> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* 判断节点 (type=4) - 有输入和输出连接点 */} |
|
|
|
{data.stepType === 4 && ( |
|
|
|
<> |
|
|
|
<Handle |
|
|
|
type="target" |
|
|
|
position={Position.Top} |
|
|
|
id="top" |
|
|
|
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|
|
|
style={{ top: -6 }} |
|
|
|
/> |
|
|
|
<Handle |
|
|
|
type="source" |
|
|
|
position={Position.Right} |
|
|
|
id="right" |
|
|
|
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|
|
|
style={{ right: -6 }} |
|
|
|
/> |
|
|
|
<Handle |
|
|
|
type="source" |
|
|
|
position={Position.Bottom} |
|
|
|
id="bottom" |
|
|
|
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|
|
|
style={{ bottom: -6 }} |
|
|
|
/> |
|
|
|
<Handle |
|
|
|
type="target" |
|
|
|
position={Position.Left} |
|
|
|
id="left" |
|
|
|
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|
|
|
style={{ left: -6 }} |
|
|
|
/> |
|
|
|
</> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
// 创建节点类型映射
|
|
|
|
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<HTMLDivElement>(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({ |
|
|
|
流程图设计器 |
|
|
|
</CardTitle> |
|
|
|
<div className="flex items-center space-x-2"> |
|
|
|
<Button |
|
|
|
size="sm" |
|
|
|
variant="outline" |
|
|
|
onClick={toggleMiniMap} |
|
|
|
className="flex items-center gap-1" |
|
|
|
> |
|
|
|
{showMiniMap ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} |
|
|
|
小地图 |
|
|
|
</Button> |
|
|
|
{/* 连线类型选择器 */} |
|
|
|
<div className="flex items-center space-x-2"> |
|
|
|
<span className="text-sm text-gray-600 dark:text-gray-300">连线类型:</span> |
|
|
|
<Select value={edgeType} onValueChange={(value: 'step' | 'smoothstep' | 'straight') => setEdgeType(value)}> |
|
|
|
<SelectTrigger className="w-32 h-8 text-xs"> |
|
|
|
<SelectValue /> |
|
|
|
</SelectTrigger> |
|
|
|
<SelectContent> |
|
|
|
<SelectItem value="straight">直线</SelectItem> |
|
|
|
<SelectItem value="smoothstep">平滑曲线</SelectItem> |
|
|
|
<SelectItem value="step">折线</SelectItem> |
|
|
|
</SelectContent> |
|
|
|
</Select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<Button |
|
|
|
size="sm" |
|
|
|
variant="outline" |
|
|
|
@ -511,151 +609,42 @@ function ReactFlowDesignerInner({ |
|
|
|
<Button |
|
|
|
size="sm" |
|
|
|
variant="outline" |
|
|
|
onClick={handleClear} |
|
|
|
onClick={handleDeleteSelected} |
|
|
|
className="flex items-center gap-1" |
|
|
|
> |
|
|
|
<Trash2 className="h-4 w-4" /> |
|
|
|
清空 |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
size="sm" |
|
|
|
variant="outline" |
|
|
|
onClick={handleImport} |
|
|
|
className="flex items-center gap-1" |
|
|
|
> |
|
|
|
<Upload className="h-4 w-4" /> |
|
|
|
导入 |
|
|
|
删除连线 |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
size="sm" |
|
|
|
variant="outline" |
|
|
|
onClick={handleExport} |
|
|
|
onClick={handleClear} |
|
|
|
className="flex items-center gap-1" |
|
|
|
> |
|
|
|
<Download className="h-4 w-4" /> |
|
|
|
导出 |
|
|
|
<Trash2 className="h-4 w-4" /> |
|
|
|
清空 |
|
|
|
</Button> |
|
|
|
<Button onClick={handleSave} className="flex items-center gap-1"> |
|
|
|
<Save className="h-4 w-4" /> |
|
|
|
保存 |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
size="sm" |
|
|
|
variant="outline" |
|
|
|
onClick={() => { |
|
|
|
const testNode = { |
|
|
|
id: `test-node-${Date.now()}`, |
|
|
|
type: 'testStep', |
|
|
|
position: { x: 100, y: 100 }, |
|
|
|
data: { |
|
|
|
stepId: 'test', |
|
|
|
stepName: '测试节点', |
|
|
|
stepType: 1, |
|
|
|
stepTypeName: '测试类型', |
|
|
|
icon: 'settings', |
|
|
|
}, |
|
|
|
}; |
|
|
|
setNodes((nds) => nds.concat(testNode)); |
|
|
|
console.log('Test node added'); |
|
|
|
}} |
|
|
|
className="flex items-center gap-1" |
|
|
|
> |
|
|
|
<Plus className="h-4 w-4" /> |
|
|
|
测试节点 |
|
|
|
</Button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</CardHeader> |
|
|
|
<CardContent className="p-0 flex-1 overflow-hidden"> |
|
|
|
<div ref={reactFlowWrapper} className="w-full h-full relative"> |
|
|
|
{/* 添加一个拖拽区域覆盖整个画布 - 只在拖拽时激活 */} |
|
|
|
<div |
|
|
|
className={`absolute inset-0 z-10 transition-all duration-200 ${ |
|
|
|
isDraggingOver |
|
|
|
? 'pointer-events-auto bg-blue-50/20' |
|
|
|
: 'pointer-events-none bg-transparent' |
|
|
|
}`}
|
|
|
|
onDragEnter={(e) => { |
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
<ReactFlow |
|
|
|
nodes={nodes} |
|
|
|
edges={edges} |
|
|
|
onNodesChange={onNodesChange} |
|
|
|
onEdgesChange={onEdgesChange} |
|
|
|
onConnect={onConnect} |
|
|
|
onEdgeClick={onEdgeClick} |
|
|
|
onEdgeDoubleClick={onEdgeDoubleClick} |
|
|
|
onDragOver={onDragOver} |
|
|
|
onDrop={onDrop} |
|
|
|
onMove={onMove} |
|
|
|
onPaneClick={onPaneClick} |
|
|
|
nodeTypes={nodeTypes} |
|
|
|
defaultViewport={{ x: 0, y: 0, zoom: 1.5 }} |
|
|
|
minZoom={1} |
|
|
|
@ -663,10 +652,12 @@ function ReactFlowDesignerInner({ |
|
|
|
style={{ width: '100%', height: '100%' }} |
|
|
|
deleteKeyCode="Delete" |
|
|
|
multiSelectionKeyCode="Shift" |
|
|
|
connectOnClick={true} |
|
|
|
snapToGrid={true} |
|
|
|
snapGrid={[15, 15]} |
|
|
|
> |
|
|
|
<Background /> |
|
|
|
{showControls && <Controls />} |
|
|
|
{showMiniMap && <MiniMap />} |
|
|
|
<Panel position="top-left" className="bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm p-2 rounded shadow"> |
|
|
|
<div className="text-xs text-gray-600 dark:text-gray-300 flex items-center justify-between"> |
|
|
|
<span>缩放: {Math.round(currentZoom * 100)}%</span> |
|
|
|
@ -678,6 +669,12 @@ function ReactFlowDesignerInner({ |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</Panel> |
|
|
|
<Panel position="top-right" className="bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm p-2 rounded shadow"> |
|
|
|
<div className="text-xs text-gray-600 dark:text-gray-300"> |
|
|
|
<div>节点: {nodes.length}</div> |
|
|
|
<div>连线: {edges.length}</div> |
|
|
|
</div> |
|
|
|
</Panel> |
|
|
|
</ReactFlow> |
|
|
|
</div> |
|
|
|
</CardContent> |
|
|
|
|