feat(web): if-else/question-classifier node port layout update

This commit is contained in:
zhaoying
2026-02-09 18:40:24 +08:00
parent e19d27f640
commit f076199e3f
7 changed files with 188 additions and 128 deletions

View File

@@ -1,8 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-09 18:31:30
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 18:31:30
*/
import { useState } from 'react'; import { useState } from 'react';
import { Popover } from 'antd'; import { Popover } from 'antd';
import clsx from 'clsx'; import clsx from 'clsx';
import type { ReactShapeConfig } from '@antv/x6-react-shape'; import type { ReactShapeConfig } from '@antv/x6-react-shape';
import { nodeLibrary, graphNodeLibrary, edgeAttrs } from '../../constant'; import { nodeLibrary, graphNodeLibrary, edgeAttrs, nodeWidth } from '../../constant';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
@@ -10,6 +16,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
// Handle node selection from popover and create new node replacing the add-node placeholder
const handleNodeSelect = (selectedNodeType: any) => { const handleNodeSelect = (selectedNodeType: any) => {
const parentBBox = node.getBBox(); const parentBBox = node.getBBox();
const cycleId = data.cycle; const cycleId = data.cycle;
@@ -32,7 +39,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
}, },
}); });
// 将新节点添加为父节点的子节点 // Add new node as child of parent node
if (cycleId) { if (cycleId) {
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId); const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
if (parentNode) { if (parentNode) {
@@ -61,14 +68,14 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
}); });
}); });
// 删除所有add-node类型的节点 // Remove all add-node type nodes
graph.getNodes().forEach((n: any) => { graph.getNodes().forEach((n: any) => {
if (n.getData()?.type === 'add-node' && n.getData()?.cycle === cycleId) { if (n.getData()?.type === 'add-node' && n.getData()?.cycle === cycleId) {
n.remove(); n.remove();
} }
}); });
// 自动调整循环节点大小 // Automatically adjust loop node size
const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId); const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
if (loopNode) { if (loopNode) {
const adjustLoopSize = () => { const adjustLoopSize = () => {
@@ -85,7 +92,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }); }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
const padding = 20; const padding = 20;
const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2); const newWidth = Math.max(nodeWidth, bounds.maxX - bounds.minX + padding * 2);
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2); const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2);
loopNode.prop('size', { width: newWidth, height: newHeight }); loopNode.prop('size', { width: newWidth, height: newHeight });
@@ -94,7 +101,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
adjustLoopSize(); adjustLoopSize();
// 监听子节点移动事件 // Listen to child node movement events
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId); const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
childNodes.forEach((childNode: any) => { childNodes.forEach((childNode: any) => {
childNode.on('change:position', adjustLoopSize); childNode.on('change:position', adjustLoopSize);
@@ -104,7 +111,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
}; };
const content = ( const content = (
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}> <div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: `${nodeWidth}px'` }}>
{nodeLibrary.map((category, categoryIndex) => { {nodeLibrary.map((category, categoryIndex) => {
const filteredNodes = category.nodes.filter(nodeType => const filteredNodes = category.nodes.filter(nodeType =>
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'iteration' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start' nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'iteration' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start'

View File

@@ -1,7 +1,13 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-09 18:30:28
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 18:30:28
*/
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Popover } from 'antd'; import { Popover } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { nodeLibrary, graphNodeLibrary, edgeAttrs } from '../constant'; import { nodeLibrary, graphNodeLibrary, edgeAttrs, nodeWidth } from '../constant';
interface PortClickHandlerProps { interface PortClickHandlerProps {
graph: any; graph: any;
@@ -32,13 +38,14 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
}; };
}, []); }, []);
// Handle node selection from popover menu and create new node with edge connection
const handleNodeSelect = (selectedNodeType: any) => { const handleNodeSelect = (selectedNodeType: any) => {
if (!sourceNode || !graph) return; if (!sourceNode || !graph) return;
const sourceNodeData = sourceNode.getData(); const sourceNodeData = sourceNode.getData();
const sourceNodeType = sourceNodeData?.type; const sourceNodeType = sourceNodeData?.type;
// 如果是cycle-start节点需要处理add-node节点 // If it's a cycle-start node, handle the add-node placeholder
let addNodePosition = null; let addNodePosition = null;
if (sourceNodeType === 'cycle-start' && sourceNodeData.cycle) { if (sourceNodeType === 'cycle-start' && sourceNodeData.cycle) {
const cycleId = sourceNodeData.cycle; const cycleId = sourceNodeData.cycle;
@@ -53,38 +60,38 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
} }
} }
// 计算新节点位置,避免重叠 // Calculate new node position to avoid overlapping
const sourceBBox = sourceNode.getBBox(); const sourceBBox = sourceNode.getBBox();
const nodeWidth = graphNodeLibrary[selectedNodeType.type]?.width || 120; const nodeWidth = graphNodeLibrary[selectedNodeType.type]?.width || 120;
const nodeHeight = graphNodeLibrary[selectedNodeType.type]?.height || 88; const nodeHeight = graphNodeLibrary[selectedNodeType.type]?.height || 88;
const horizontalSpacing = sourceNodeType === 'cycle-start' ? 40 : 80; const horizontalSpacing = sourceNodeType === 'cycle-start' ? 40 : 80;
const verticalSpacing = 10; const verticalSpacing = 10;
// 获取源连接桩的group信息 // Get source port group information
const sourcePortInfo = sourceNode.getPorts().find((p: any) => p.id === sourcePort); const sourcePortInfo = sourceNode.getPorts().find((p: any) => p.id === sourcePort);
const sourcePortGroup = sourcePortInfo?.group || sourcePort; const sourcePortGroup = sourcePortInfo?.group || sourcePort;
console.log('sourcePortGroup', sourcePortGroup, sourcePortInfo) console.log('sourcePortGroup', sourcePortGroup, sourcePortInfo)
// 如果有add-node位置,使用该位置,否则计算新位置 // If add-node position exists, use it; otherwise calculate new position
let newX, newY; let newX, newY;
if (addNodePosition) { if (addNodePosition) {
newX = addNodePosition.x; newX = addNodePosition.x;
newY = addNodePosition.y; newY = addNodePosition.y;
} else { } else {
// 根据连接桩位置决定节点放置方向 // Determine node placement direction based on port position
if (sourcePortGroup === 'left') { if (sourcePortGroup === 'left') {
// 左侧连接桩,在左侧添加节点 // Left port: add node to the left
newX = sourceBBox.x - nodeWidth*2 - horizontalSpacing; newX = sourceBBox.x - nodeWidth*2 - horizontalSpacing;
newY = sourceBBox.y; newY = sourceBBox.y;
} else { } else {
// 右侧连接桩,在右侧添加节点 // Right port: add node to the right
newX = sourceBBox.x + sourceBBox.width + horizontalSpacing; newX = sourceBBox.x + sourceBBox.width + horizontalSpacing;
newY = sourceBBox.y; newY = sourceBBox.y;
} }
// 检查位置是否与现有节点重叠(只考虑与当前节点相连的节点) // Check if position overlaps with existing nodes (only consider connected nodes)
const checkOverlap = (x: number, y: number) => { const checkOverlap = (x: number, y: number) => {
// 获取与源节点相连的节点 // Get nodes connected to the source node
const connectedNodes = new Set(); const connectedNodes = new Set();
graph.getConnectedEdges(sourceNode).forEach((edge: any) => { graph.getConnectedEdges(sourceNode).forEach((edge: any) => {
const sourceId = edge.getSourceCellId(); const sourceId = edge.getSourceCellId();
@@ -95,20 +102,20 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
return graph.getNodes().some((node: any) => { return graph.getNodes().some((node: any) => {
if (node.id === sourceNode.id) return false; if (node.id === sourceNode.id) return false;
if (!connectedNodes.has(node.id)) return false; // 只考虑相连的节点 if (!connectedNodes.has(node.id)) return false; // Only consider connected nodes
const bbox = node.getBBox(); const bbox = node.getBBox();
return !(x + nodeWidth < bbox.x || x > bbox.x + bbox.width || return !(x + nodeWidth < bbox.x || x > bbox.x + bbox.width ||
y + nodeHeight < bbox.y || y > bbox.y + bbox.height); y + nodeHeight < bbox.y || y > bbox.y + bbox.height);
}); });
}; };
// 如果位置被占用,向下寻找空位 // If position is occupied, search downward for empty space
while (checkOverlap(newX, newY)) { while (checkOverlap(newX, newY)) {
newY += nodeHeight + verticalSpacing; newY += nodeHeight + verticalSpacing;
} }
} }
// 创建新节点 // Create new node
const id = `${selectedNodeType.type.replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` const id = `${selectedNodeType.type.replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
const newNode = graph.addNode({ const newNode = graph.addNode({
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default), ...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
@@ -120,12 +127,12 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
type: selectedNodeType.type, type: selectedNodeType.type,
icon: selectedNodeType.icon, icon: selectedNodeType.icon,
name: t(`workflow.${selectedNodeType.type}`), name: t(`workflow.${selectedNodeType.type}`),
cycle: sourceNodeData.cycle, // 继承源节点的cycle cycle: sourceNodeData.cycle, // Inherit cycle from source node
config: selectedNodeType.config || {} config: selectedNodeType.config || {}
}, },
}); });
// 将新节点添加为父节点的子节点 // Add new node as child of parent node
if (sourceNodeData.cycle) { if (sourceNodeData.cycle) {
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle); const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle);
if (parentNode) { if (parentNode) {
@@ -133,16 +140,16 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
} }
} }
// 创建连线 // Create edge connection
setTimeout(() => { setTimeout(() => {
const targetPorts = newNode.getPorts(); const targetPorts = newNode.getPorts();
let targetPort; let targetPort;
if (sourcePortGroup === 'left') { if (sourcePortGroup === 'left') {
// 从左侧连接桩连出,连接到新节点的右侧 // Connect from left port to new node's right side
targetPort = targetPorts.find((port: any) => port.group === 'right')?.id || 'right'; targetPort = targetPorts.find((port: any) => port.group === 'right')?.id || 'right';
} else { } else {
// 从右侧连接桩连出,连接到新节点的左侧 // Connect from right port to new node's left side
targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left'; targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
} }
@@ -153,7 +160,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
// zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0 // zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0
}); });
// 循环节点内子节点通过连接桩添加时,调整循环节点大小 // Adjust loop node size when child node is added via port within loop node
const cycleId = sourceNodeData.cycle; const cycleId = sourceNodeData.cycle;
if (cycleId) { if (cycleId) {
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId); const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
@@ -174,7 +181,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
const padding = 20; const padding = 20;
const bottomPadding = 50; const bottomPadding = 50;
const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2); const newWidth = Math.max(nodeWidth, bounds.maxX - bounds.minX + padding * 2);
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding + bottomPadding); const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding + bottomPadding);
parentNode.prop('size', { width: newWidth, height: newHeight }); parentNode.prop('size', { width: newWidth, height: newHeight });
@@ -183,7 +190,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
adjustLoopSize(); adjustLoopSize();
// 监听子节点移动事件 // Listen to child node movement events
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId); const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
childNodes.forEach((childNode: any) => { childNodes.forEach((childNode: any) => {
childNode.on('change:position', adjustLoopSize); childNode.on('change:position', adjustLoopSize);
@@ -192,7 +199,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
} }
}, 50); }, 50);
// 清理临时元素 // Clean up temporary element
if (tempElement) { if (tempElement) {
document.body.removeChild(tempElement); document.body.removeChild(tempElement);
setTempElement(null); setTempElement(null);
@@ -210,7 +217,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
}; };
const content = ( const content = (
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}> <div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: `${nodeWidth}px` }}>
{nodeLibrary.map((category, categoryIndex) => { {nodeLibrary.map((category, categoryIndex) => {
const sourceNodeData = sourceNode?.getData(); const sourceNodeData = sourceNode?.getData();
const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop'); const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop');

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-09 18:24:53
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 18:24:53
*/
import { type FC } from 'react' import { type FC } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -6,7 +12,7 @@ import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectPr
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect' import VariableSelect from '../VariableSelect'
import Editor from '../../Editor' import Editor from '../../Editor'
import { edgeAttrs, portArgs } from '../../../constant' import { edgeAttrs, portTextAttrs, nodeWidth } from '../../../constant'
interface CaseListProps { interface CaseListProps {
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>; value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
@@ -52,15 +58,16 @@ const CaseList: FC<CaseListProps> = ({
const { t } = useTranslation(); const { t } = useTranslation();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
// Update node ports based on case count changes (add/remove cases)
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => { const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
if (!selectedNode || !graphRef?.current) return; if (!selectedNode || !graphRef?.current) return;
// 获取当前端口数量来判断是添加还是删除操作 // Get current port count to determine if it's an add or remove operation
const currentPorts = selectedNode.getPorts().filter((port: any) => port.group === 'right'); const currentPorts = selectedNode.getPorts().filter((port: any) => port.group === 'right');
const currentCaseCount = currentPorts.length - 1; // 减去ELSE端口 const currentCaseCount = currentPorts.length - 1; // Exclude ELSE port
const isAddingCase = removedCaseIndex === undefined && caseCount > currentCaseCount; const isAddingCase = removedCaseIndex === undefined && caseCount > currentCaseCount;
// 保存现有连线信息(包括左侧端口连线) // Save existing edge connections (including left-side port connections)
const existingEdges = graphRef.current.getEdges().filter((edge: any) => const existingEdges = graphRef.current.getEdges().filter((edge: any) =>
edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id
); );
@@ -73,7 +80,7 @@ const CaseList: FC<CaseListProps> = ({
isIncoming: edge.getTargetCellId() === selectedNode.id isIncoming: edge.getTargetCellId() === selectedNode.id
})); }));
// 移除所有现有的右侧端口 // Remove all existing right-side ports
const existingPorts = selectedNode.getPorts(); const existingPorts = selectedNode.getPorts();
existingPorts.forEach((port: any) => { existingPorts.forEach((port: any) => {
if (port.group === 'right') { if (port.group === 'right') {
@@ -81,43 +88,52 @@ const CaseList: FC<CaseListProps> = ({
} }
}); });
// 计算新的节点高度基础高度88px + 每个额外port增加30px // Calculate new node height: base height 88px + 30px for each additional port
const baseHeight = 88; const baseHeight = 88;
const totalPorts = caseCount + 1; // IF/ELIF + ELSE const totalPorts = caseCount + 1; // IF/ELIF + ELSE
const newHeight = baseHeight + (totalPorts - 2) * 30; const newHeight = baseHeight + (totalPorts - 2) * 30;
selectedNode.prop('size', { width: 240, height: newHeight }) selectedNode.prop('size', { width: nodeWidth, height: newHeight })
// 添加 IF 端口 // Add IF port
selectedNode.addPort({ selectedNode.addPort({
id: 'CASE1', id: 'CASE1',
group: 'right', group: 'right',
args: portArgs, args: {
attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }} x: nodeWidth,
}); y: 42,
},
attrs: { text: { text: 'IF', ...portTextAttrs } }
})
// 添加 ELIF 端口 // Add ELIF ports
for (let i = 1; i < caseCount; i++) { for (let i = 1; i < caseCount; i++) {
selectedNode.addPort({ selectedNode.addPort({
id: `CASE${i + 1}`, id: `CASE${i + 1}`,
group: 'right', group: 'right',
args: portArgs, args: {
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }} x: nodeWidth,
y: 30 * i + 42,
},
attrs: { text: { text: 'ELIF', ...portTextAttrs }}
}); });
} }
// 添加 ELSE 端口 // Add ELSE port
selectedNode.addPort({ selectedNode.addPort({
id: `CASE${caseCount + 1}`, id: `CASE${caseCount + 1}`,
group: 'right', group: 'right',
args: portArgs, args: {
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }} x: nodeWidth,
y: 30 * caseCount + 42,
},
attrs: { text: { text: 'ELSE', ...portTextAttrs }}
}); });
// 恢复连线 // Restore edge connections
setTimeout(() => { setTimeout(() => {
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => { edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
// 如果是进入连线(左侧端口),直接恢复 // If it's an incoming connection (left-side port), restore directly
if (isIncoming) { if (isIncoming) {
const sourceCell = graphRef.current?.getCellById(sourceCellId); const sourceCell = graphRef.current?.getCellById(sourceCellId);
if (sourceCell) { if (sourceCell) {
@@ -131,10 +147,10 @@ const CaseList: FC<CaseListProps> = ({
return; return;
} }
// 处理右侧端口连线 // Handle right-side port connections
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0'); const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
// 如果是删除操作且是被删除的端口,删除连线 // If it's a remove operation and the port is being removed, delete the connection
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) { if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
graphRef.current?.removeCell(edge); graphRef.current?.removeCell(edge);
return; return;
@@ -142,22 +158,22 @@ const CaseList: FC<CaseListProps> = ({
let newPortId = sourcePortId; let newPortId = sourcePortId;
// 如果是删除操作,需要重新映射端口ID // If it's a remove operation, remap port IDs
if (removedCaseIndex !== undefined) { if (removedCaseIndex !== undefined) {
if (originalCaseNumber > removedCaseIndex + 1) { if (originalCaseNumber > removedCaseIndex + 1) {
// 被删除端口之后的端口,编号向前移动 // Ports after the removed port, shift numbering forward
newPortId = `CASE${originalCaseNumber - 1}`; newPortId = `CASE${originalCaseNumber - 1}`;
} }
// ELSE端口始终映射到新的ELSE端口位置 // ELSE port always maps to the new ELSE port position
else if (originalCaseNumber === currentCaseCount + 1) { else if (originalCaseNumber === currentCaseCount + 1) {
newPortId = `CASE${caseCount + 1}`; newPortId = `CASE${caseCount + 1}`;
} }
} else if (isAddingCase) { } else if (isAddingCase) {
// 如果是添加操作ELSE端口需要重新映射 // If it's an add operation, ELSE port needs to be remapped
if (originalCaseNumber === currentCaseCount + 1) { if (originalCaseNumber === currentCaseCount + 1) {
newPortId = `CASE${caseCount + 1}`; // 新的ELSE端口 newPortId = `CASE${caseCount + 1}`; // New ELSE port
} }
// 新添加的端口不恢复任何连线 // Newly added ports don't restore any connections
} }
const newPorts = selectedNode.getPorts(); const newPorts = selectedNode.getPorts();

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-09 18:34:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 18:34:33
*/
import { type FC } from 'react'; import { type FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Form, Space } from 'antd'; import { Button, Form, Space } from 'antd';
@@ -5,7 +11,7 @@ import { Graph, Node } from '@antv/x6';
import Editor from '../../Editor'; import Editor from '../../Editor';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import { edgeAttrs, portArgs } from '../../../constant' import { edgeAttrs, portTextAttrs, nodeWidth } from '../../../constant'
interface CategoryListProps { interface CategoryListProps {
parentName: string; parentName: string;
@@ -19,10 +25,11 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const formValues = Form.useWatch([parentName], form); const formValues = Form.useWatch([parentName], form);
// Update node ports based on category count changes (add/remove categories)
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => { const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
if (!selectedNode || !graphRef?.current) return; if (!selectedNode || !graphRef?.current) return;
// 保存现有连线信息(包括左侧端口连线) // Save existing edge connections (including left-side port connections)
const existingEdges = graphRef.current.getEdges().filter((edge: any) => const existingEdges = graphRef.current.getEdges().filter((edge: any) =>
edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id
); );
@@ -35,7 +42,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
isIncoming: edge.getTargetCellId() === selectedNode.id isIncoming: edge.getTargetCellId() === selectedNode.id
})); }));
// 移除所有现有的右侧端口 // Remove all existing right-side ports
const existingPorts = selectedNode.getPorts(); const existingPorts = selectedNode.getPorts();
existingPorts.forEach((port: any) => { existingPorts.forEach((port: any) => {
if (port.group === 'right') { if (port.group === 'right') {
@@ -43,28 +50,30 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
} }
}); });
// 计算新的节点高度基础高度88px + 每个额外port增加30px // Calculate new node height: base height 88px + 30px for each additional port
const baseHeight = 88; const baseHeight = 88;
const totalPorts = caseCount + 1; // IF/ELIF + ELSE const newHeight = baseHeight + (caseCount - 2) * 30;
const newHeight = baseHeight + (totalPorts - 2) * 30;
selectedNode.prop('size', { width: 240, height: newHeight < baseHeight ? baseHeight : newHeight }) selectedNode.prop('size', { width: nodeWidth, height: newHeight < baseHeight ? baseHeight : newHeight })
// 添加 分类 端口 // Add category ports
for (let i = 0; i < caseCount; i++) { for (let i = 0; i < caseCount; i++) {
selectedNode.addPort({ selectedNode.addPort({
id: `CASE${i + 1}`, id: `CASE${i + 1}`,
group: 'right', group: 'right',
args: portArgs, args: {
attrs: { text: { text: `分类${i + 1}`, fontSize: 12, fill: '#5B6167' } } x: nodeWidth,
y: 30 * i + 42,
},
attrs: { text: { text: `分类${i + 1}`, ...portTextAttrs } }
}); });
} }
// 恢复连线 // Restore edge connections
setTimeout(() => { setTimeout(() => {
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => { edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
graphRef.current?.removeCell(edge); graphRef.current?.removeCell(edge);
// 如果是进入连线(左侧端口),直接恢复 // If it's an incoming connection (left-side port), restore directly
if (isIncoming) { if (isIncoming) {
const sourceCell = graphRef.current?.getCellById(sourceCellId); const sourceCell = graphRef.current?.getCellById(sourceCellId);
if (sourceCell) { if (sourceCell) {
@@ -77,22 +86,22 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
return; return;
} }
// 处理右侧端口连线 // Handle right-side port connections
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0'); const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
// 如果是被删除的端口,不重新创建连线 // If it's a removed port, don't recreate the connection
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) { if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
return; return;
} }
let newPortId = sourcePortId; let newPortId = sourcePortId;
// 如果删除了某个端口,需要重新映射后续端口的ID // If a port was removed, remap subsequent port IDs
if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) { if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) {
newPortId = `CASE${originalCaseNumber - 1}`; newPortId = `CASE${originalCaseNumber - 1}`;
} }
// 检查新端口是否存在 // Check if the new port exists
const newPorts = selectedNode.getPorts(); const newPorts = selectedNode.getPorts();
const matchingPort = newPorts.find((port: any) => port.id === newPortId); const matchingPort = newPorts.find((port: any) => port.id === newPortId);

View File

@@ -1,7 +1,14 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-09 18:35:43
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 18:35:43
*/
import { type FC, useRef, useState } from "react"; import { type FC, useRef, useState } from "react";
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd' import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd'
import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons'; import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons';
import Editor from '../../Editor' import Editor from '../../Editor'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import AuthConfigModal from './AuthConfigModal' import AuthConfigModal from './AuthConfigModal'
@@ -9,6 +16,7 @@ import type { AuthConfigModalRef, HttpRequestConfigForm } from './types'
import VariableSelect from "../VariableSelect"; import VariableSelect from "../VariableSelect";
import MessageEditor from '../MessageEditor' import MessageEditor from '../MessageEditor'
import EditableTable from './EditableTable' import EditableTable from './EditableTable'
import { portTextAttrs } from '../../../constant'
const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({
options, options,
@@ -32,6 +40,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
form.setFieldValue(['body', 'data'], undefined) form.setFieldValue(['body', 'data'], undefined)
} }
// Handle error handling method change and update node ports accordingly
const handleChangeErrorHandleMethod = (method: string) => { const handleChangeErrorHandleMethod = (method: string) => {
form.setFieldsValue({ form.setFieldsValue({
error_handle: { error_handle: {
@@ -42,21 +51,21 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
} }
}) })
// 更新节点连接桩 // Update node ports
console.log('handleChangeErrorHandleMethod', selectedNode, graphRef?.current) console.log('handleChangeErrorHandleMethod', selectedNode, graphRef?.current)
if (selectedNode && graphRef?.current) { if (selectedNode && graphRef?.current) {
const existingPorts = selectedNode.getPorts(); const existingPorts = selectedNode.getPorts();
const errorPort = existingPorts.find((port: any) => port.id === 'ERROR'); const errorPort = existingPorts.find((port: any) => port.id === 'ERROR');
if (method === 'branch' && !errorPort) { if (method === 'branch' && !errorPort) {
// 添加异常节点连接桩 // Add error branch port
selectedNode.addPort({ selectedNode.addPort({
id: 'ERROR', id: 'ERROR',
group: 'right', group: 'right',
attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }} attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}
}); });
} else if (method !== 'branch' && errorPort) { } else if (method !== 'branch' && errorPort) {
// 移除异常节点连接桩和相关连线 // Remove error branch port and related edges
const edges = graphRef.current.getEdges().filter((edge: any) => const edges = graphRef.current.getEdges().filter((edge: any) =>
edge.getSourceCellId() === selectedNode.id && edge.getSourcePortId() === 'ERROR' edge.getSourceCellId() === selectedNode.id && edge.getSourcePortId() === 'ERROR'
); );

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 15:06:18 * @Date: 2026-02-03 15:06:18
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-05 14:15:13 * @Last Modified time: 2026-02-09 17:48:46
*/ */
import LoopNode from './components/Nodes/LoopNode'; import LoopNode from './components/Nodes/LoopNode';
import NormalNode from './components/Nodes/NormalNode'; import NormalNode from './components/Nodes/NormalNode';
@@ -518,6 +518,7 @@ export const nodeLibrary: NodeLibrary[] = [
// }, // },
]; ];
export const nodeWidth = 240;
/** /**
* Node registration library for X6 graph * Node registration library for X6 graph
* Maps node shapes to their React components * Maps node shapes to their React components
@@ -525,13 +526,13 @@ export const nodeLibrary: NodeLibrary[] = [
export const nodeRegisterLibrary: ReactShapeConfig[] = [ export const nodeRegisterLibrary: ReactShapeConfig[] = [
{ {
shape: 'loop-node', shape: 'loop-node',
width: 240, width: nodeWidth,
height: 120, height: 120,
component: LoopNode, component: LoopNode,
}, },
{ {
shape: 'iteration-node', shape: 'iteration-node',
width: 240, width: nodeWidth,
height: 120, height: 120,
component: LoopNode, component: LoopNode,
}, },
@@ -543,7 +544,7 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
}, },
{ {
shape: 'condition-node', shape: 'condition-node',
width: 240, width: nodeWidth,
height: 88, height: 88,
component: ConditionNode, component: ConditionNode,
}, },
@@ -625,8 +626,9 @@ export const portAttrs = {
textAnchor: 'middle', textAnchor: 'middle',
textVerticalAnchor: 'middle', textVerticalAnchor: 'middle',
pointerEvents: 'none', pointerEvents: 'none',
} },
} }
export const portTextAttrs = { fontSize: 12, fill: '#5B6167' }
/** /**
* Unified port group configuration * Unified port group configuration
@@ -638,6 +640,12 @@ const defaultPortGroups = {
// bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs }, // bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs },
left: { position: 'left', markup: portMarkup, attrs: portAttrs }, left: { position: 'left', markup: portMarkup, attrs: portAttrs },
} }
export const defaultAbsolutePortGroups = {
// top: { position: 'top', markup: portMarkup, attrs: portAttrs },
right: { position: { name: 'absolute' }, markup: portMarkup, attrs: portAttrs },
// bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs },
left: { position: 'left', markup: portMarkup, attrs: portAttrs },
}
/** /**
* Default port items for standard nodes * Default port items for standard nodes
*/ */
@@ -650,7 +658,7 @@ const defaultPortItems = [
/** /**
* Port position arguments * Port position arguments
*/ */
export const portArgs = { dy: 18 } export const portArgs = { x: nodeWidth, y: 42 }
/** /**
* Graph node library configuration * Graph node library configuration
@@ -658,7 +666,7 @@ export const portArgs = { dy: 18 }
*/ */
export const graphNodeLibrary: Record<string, NodeConfig> = { export const graphNodeLibrary: Record<string, NodeConfig> = {
iteration: { iteration: {
width: 240, width: nodeWidth,
height: 120, height: 120,
shape: 'iteration-node', shape: 'iteration-node',
ports: { ports: {
@@ -667,7 +675,7 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
}, },
}, },
loop: { loop: {
width: 240, width: nodeWidth,
height: 120, height: 120,
shape: 'loop-node', shape: 'loop-node',
ports: { ports: {
@@ -676,33 +684,47 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
}, },
}, },
'if-else': { 'if-else': {
width: 240, width: nodeWidth,
height: 88, height: 88,
shape: 'condition-node', shape: 'condition-node',
ports: { ports: {
groups: defaultPortGroups, groups: defaultAbsolutePortGroups,
items: [ items: [
{ group: 'left' }, { group: 'left' },
{ group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: 'IF', fontSize: 12, color: '#5B6167' }} }, ...(['IF', 'ELSE'].map((text, index) => ({
{ group: 'right', id: 'CASE2', args: portArgs, attrs: { text: { text: 'ELSE', fontSize: 12, color: '#5B6167' }} } group: 'right',
id: `CASE${index}`,
args: {
...portArgs,
y: 30 * index + 42,
},
attrs: { text: { text: text, ...portTextAttrs } }
}))),
], ],
}, },
}, },
'question-classifier': { 'question-classifier': {
width: 240, width: nodeWidth,
height: 88, height: 88,
shape: 'condition-node', shape: 'condition-node',
ports: { ports: {
groups: defaultPortGroups, groups: defaultAbsolutePortGroups,
items: [ items: [
{ group: 'left' }, { group: 'left' },
{ group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: '分类1', fontSize: 12, color: '#5B6167' } } }, ...(['分类1', '分类2'].map((text, index) => ({
{ group: 'right', id: 'CASE2', args: portArgs, attrs: { text: { text: '分类2', fontSize: 12, color: '#5B6167' } } } group: 'right',
id: `CASE${index}`,
args: {
...portArgs,
y: 30 * index + 42,
},
attrs: { text: { text: text, ...portTextAttrs } }
}))),
], ],
}, },
}, },
start: { start: {
width: 240, width: nodeWidth,
height: 64, height: 64,
shape: 'normal-node', shape: 'normal-node',
ports: { ports: {
@@ -711,7 +733,7 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
}, },
}, },
end: { end: {
width: 240, width: nodeWidth,
height: 64, height: 64,
shape: 'normal-node', shape: 'normal-node',
ports: { ports: {
@@ -738,7 +760,7 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
}, },
}, },
default: { default: {
width: 240, width: nodeWidth,
height: 64, height: 64,
shape: 'normal-node', shape: 'normal-node',
ports: { ports: {

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 15:17:48 * @Date: 2026-02-03 15:17:48
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 15:17:48 * @Last Modified time: 2026-02-09 18:37:01
*/ */
import { useRef, useEffect, useState } from 'react'; import { useRef, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@@ -12,7 +12,7 @@ import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from '
import { register } from '@antv/x6-react-shape'; import { register } from '@antv/x6-react-shape';
import type { PortMetadata } from '@antv/x6/lib/model/port'; import type { PortMetadata } from '@antv/x6/lib/model/port';
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portArgs } from '../constant'; import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth } from '../constant';
import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types'; import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application' import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
@@ -168,6 +168,7 @@ export const useWorkflowGraph = ({
} }
}) })
} }
const nodeConfig = { const nodeConfig = {
...(graphNodeLibrary[type] ?? graphNodeLibrary.default), ...(graphNodeLibrary[type] ?? graphNodeLibrary.default),
id, id,
@@ -179,39 +180,28 @@ export const useWorkflowGraph = ({
// Generate ports dynamically for if-else node based on cases // Generate ports dynamically for if-else node based on cases
if (type === 'if-else' && config.cases && Array.isArray(config.cases)) { if (type === 'if-else' && config.cases && Array.isArray(config.cases)) {
const caseCount = config.cases.length; const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE
const totalPorts = caseCount + 1; // IF/ELIF + ELSE
const baseHeight = 88; const baseHeight = 88;
const newHeight = baseHeight + (totalPorts - 2) * 30; const newHeight = baseHeight + (totalPorts - 2) * 30;
const portItems: PortMetadata[] = [ const portItems: PortMetadata[] = [
{ group: 'left' }, { group: 'left' },
{ group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }} }
]; ];
// Add IF/ELIF/ELSE ports
// Add ELIF ports for (let i = 0; i < totalPorts; i++) {
for (let i = 1; i < caseCount; i++) {
portItems.push({ portItems.push({
group: 'right', group: 'right',
id: `CASE${i + 1}`, id: `CASE${i + 1}`,
args: portArgs, args: {
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }} x: nodeWidth,
y: 30 * i + 42,
},
attrs: { text: { text: i === 0 ? 'IF' : i === totalPorts - 1 ? 'ELSE' : 'ELIF', ...portTextAttrs } }
}); });
} }
// Add ELSE port
portItems.push({
group: 'right',
id: `CASE${caseCount + 1}`,
args: portArgs,
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }}
});
nodeConfig.ports = { nodeConfig.ports = {
groups: { groups: defaultAbsolutePortGroups,
right: { position: 'right', markup: portMarkup, attrs: portAttrs },
left: { position: 'left', markup: portMarkup, attrs: portAttrs },
},
items: portItems items: portItems
}; };
@@ -222,7 +212,7 @@ export const useWorkflowGraph = ({
if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) { if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) {
const categoryCount = config.categories.length; const categoryCount = config.categories.length;
const baseHeight = 88; const baseHeight = 88;
const newHeight = baseHeight + (categoryCount - 1) * 30; const newHeight = baseHeight + (categoryCount - 2) * 30;
const portItems: PortMetadata[] = [ const portItems: PortMetadata[] = [
{ group: 'left' } { group: 'left' }
@@ -233,16 +223,16 @@ export const useWorkflowGraph = ({
portItems.push({ portItems.push({
group: 'right', group: 'right',
id: `CASE${index + 1}`, id: `CASE${index + 1}`,
args: portArgs, args: {
attrs: { text: { text: `分类${index + 1}`, fontSize: 12, fill: '#5B6167' }} x: nodeWidth,
y: 30 * index + 42,
},
attrs: { text: { text: `分类${index + 1}`, ...portTextAttrs }}
}); });
}); });
nodeConfig.ports = { nodeConfig.ports = {
groups: { groups: defaultAbsolutePortGroups,
right: { position: 'right', markup: portMarkup, attrs: portAttrs },
left: { position: 'left', markup: portMarkup, attrs: portAttrs },
},
items: portItems items: portItems
}; };
@@ -259,7 +249,7 @@ export const useWorkflowGraph = ({
items: [ items: [
{ group: 'left' }, { group: 'left' },
{ group: 'right', id: 'right' }, { group: 'right', id: 'right' },
{ group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}} { group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}}
] ]
}; };
} }