import { useEffect, useState } from 'react'; import { Popover } from 'antd'; import { useTranslation } from 'react-i18next'; import { nodeLibrary, graphNodeLibrary } from '../constant'; interface PortClickHandlerProps { graph: any; } const PortClickHandler: React.FC = ({ graph }) => { const { t } = useTranslation(); const [popoverVisible, setPopoverVisible] = useState(false); const [popoverPosition, setPopoverPosition] = useState({ x: 0, y: 0 }); const [sourceNode, setSourceNode] = useState(null); const [sourcePort, setSourcePort] = useState(''); const [tempElement, setTempElement] = useState(null); useEffect(() => { const handlePortClick = (event: CustomEvent) => { const { node, port, element, rect } = event.detail; setSourceNode(node); setSourcePort(port); setTempElement(element); setPopoverPosition({ x: rect.left, y: rect.top }); setPopoverVisible(true); }; window.addEventListener('port:click', handlePortClick as EventListener); return () => { window.removeEventListener('port:click', handlePortClick as EventListener); }; }, []); const handleNodeSelect = (selectedNodeType: any) => { if (!sourceNode || !graph) return; const sourceNodeData = sourceNode.getData(); // 计算新节点位置(在源节点右侧) const sourceBBox = sourceNode.getBBox(); const newX = sourceBBox.x + sourceBBox.width + 50; const newY = sourceBBox.y; // 创建新节点 const id = `${selectedNodeType.type.replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` const newNode = graph.addNode({ ...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default), x: newX, y: newY, id, data: { id, type: selectedNodeType.type, icon: selectedNodeType.icon, name: t(`workflow.${selectedNodeType.type}`), cycle: sourceNodeData.cycle, // 继承源节点的cycle config: selectedNodeType.config || {} }, }); // 将新节点添加为父节点的子节点 if (sourceNodeData.cycle) { const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle); if (parentNode) { parentNode.addChild(newNode); } } // 创建连线 setTimeout(() => { const targetPorts = newNode.getPorts(); const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left'; graph.addEdge({ source: { cell: sourceNode.id, port: sourcePort }, target: { cell: newNode.id, port: targetPort }, attrs: { line: { stroke: '#155EEF', strokeWidth: 1, targetMarker: { name: 'block', size: 8, }, }, }, }); // 循环节点内子节点通过连接桩添加时,调整循环节点大小 const cycleId = sourceNodeData.cycle; if (cycleId) { const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId); if (parentNode) { const adjustLoopSize = () => { const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId); if (childNodes.length > 0) { const bounds = childNodes.reduce((acc: any, child: any) => { const bbox = child.getBBox(); return { minX: Math.min(acc.minX, bbox.x), minY: Math.min(acc.minY, bbox.y), maxX: Math.max(acc.maxX, bbox.x + bbox.width), maxY: Math.max(acc.maxY, bbox.y + bbox.height) }; }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }); const padding = 20; const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2); const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2); parentNode.prop('size', { width: newWidth, height: newHeight }); } }; adjustLoopSize(); // 监听子节点移动事件 const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId); childNodes.forEach((childNode: any) => { childNode.on('change:position', adjustLoopSize); }); } } }, 50); // 清理临时元素 if (tempElement) { document.body.removeChild(tempElement); setTempElement(null); } setPopoverVisible(false); }; const handlePopoverClose = () => { setPopoverVisible(false); if (tempElement) { document.body.removeChild(tempElement); setTempElement(null); } }; const content = (
{nodeLibrary.map((category, categoryIndex) => { const sourceNodeData = sourceNode?.getData(); const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop'); const isChildOfIteration = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'iteration'); let filteredNodes; if (isChildOfLoop) { // Use same filtering as AddNode for child nodes of loop filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); } else if (isChildOfIteration) { // Filter out loop and iteration nodes for children of iteration nodes filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'break', 'cycle-start', 'iteration'].includes(nodeType.type)); } else { // Original filtering for non-loop child nodes filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type)); filteredNodes = category.nodes.filter(nodeType => nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break' ); } if (filteredNodes.length === 0) return null; return (
{categoryIndex > 0 &&
}
{t(`workflow.${category.category}`)}
{filteredNodes.map((nodeType) => (
handleNodeSelect(nodeType)} onMouseEnter={(e) => { e.currentTarget.style.background = '#f0f8ff'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'white'; }} > {t(`workflow.${nodeType.type}`)}
))}
); })}
); if (!tempElement) return null; return ( { if (!visible) handlePopoverClose(); }} placement="right" overlayStyle={{ position: 'fixed', left: popoverPosition.x + 10, top: popoverPosition.y - 10, }} >
); }; export default PortClickHandler;