feat(flow):优化循环节点配置与展示逻辑

- 调整 LOOP_START 节点的 apiIns 和 apiOuts 参数结构
- 新增 buildNodeId 工具函数统一管理节点句柄 ID 生成逻辑- 在 convertFlowData 中增强 nodeId 获取逻辑,兼容 id与 name 字段
- LoopNode 组件新增状态管理,支持动态解析条件表达式并生成 apiOuts
- 引入 NodeContentLoop 组件专门负责循环节点的内容渲染和句柄绘制
- 更新 useFlowCallbacks 中 LOOP_START 节点默认参数配置
- 新增 nodeContentLoop 组件实现节点内容与句柄的模块化渲染
master
钟良源 4 months ago
parent 26fe3794fa
commit bba0197215

@ -1,11 +1,14 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useStore } from '@xyflow/react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import DynamicIcon from '@/components/DynamicIcon';
import { Handle, Position } from '@xyflow/react';
import NodeContentLoop from '@/pages/flowEditor/components/nodeContentLoop';
import { defaultNodeTypes } from '@/components/FlowEditor/node/types/defaultType';
// 循环节点组件,用于显示循环开始和循环结束节点
const LoopNode = ({ data, id }: { data: any; id: string }) => {
const LoopNode = ({ data, id }: { data: defaultNodeTypes; id: string }) => {
const [newData, setNewData] = useState<any[]>([]);
const title = data.title || '循环节点';
const isStartNode = data.type === 'LOOP_START';
@ -24,6 +27,124 @@ const LoopNode = ({ data, id }: { data: any; id: string }) => {
}
};
const getOperator = (expr: string) => {
let operator;
if (expr.includes('==')) {
operator = '==';
}
else if (expr.includes('>=')) {
operator = '>=';
}
else if (expr.includes('<=')) {
operator = '<=';
}
else if (expr.includes('<')) {
operator = '<';
}
else if (expr.includes('>')) {
operator = '>';
}
else {
operator = '!=';
}
return operator;
};
const reverseDataStructure = (processedData: any) => {
try {
const parsedCustomDef = JSON.parse(processedData.customDef);
if (!parsedCustomDef.conditions) {
return [];
}
return parsedCustomDef.conditions.map((condition: any, index: number) => {
// 解析表达式以获取左值、操作符和右值
let lftVal = '';
let operator = '';
let rgtVal = '';
const valueType = condition.valueType || '';
if (condition.expression) {
// 处理布尔值表达式
if (valueType.includes('boolean')) {
const splitStr = valueType.split('-')[1];
operator = getOperator(condition.expression);
const pattern = new RegExp(`\\$\\.(.+)(${operator})${splitStr}`);
const match = condition.expression.match(pattern);
if (match) {
lftVal = match[1];
operator = match[2];
}
}
// 处理其他类型的表达式
else {
// 简单的解析逻辑,可能需要根据实际表达式格式进行调整
const match = condition.expression.match(/\$\.([^=!<>]+)(==|!=|>=|<=|>|<)(.+)/);
if (match) {
lftVal = match[1];
operator = match[2];
rgtVal = match[3];
}
}
}
return {
key: index,
id: Date.now(),
apiOutId: condition.apiOutId || '',
lftVal,
operator,
valueType,
rgtVal
};
});
} catch (e) {
console.error('Error parsing customDef:', e);
return [];
}
};
useEffect(() => {
if (data) {
const reverseData = reverseDataStructure(data.component);
if (reverseData.length > 0) {
const list = reverseData.map(item => {
let expression = '';
if (item.valueType.includes('boolean')) {
const splitStr = item.valueType.split('-')[1];
expression = `$.${item.lftVal}${item.operator}${splitStr}`;
}
else expression = `$.${item.lftVal}${item.operator}${item.rgtVal}`;
return {
name: item.apiOutId,
id: item.apiOutId,
desc: '',
defaultValue: item.valueType,
dataType: expression
};
});
// 合并list数组与data.parameters.apiOuts数组
const apiOuts = data.parameters?.apiOuts || [];
const mergedApiOuts = [...apiOuts, ...list];
setNewData(mergedApiOuts);
}
else {
// 如果没有reverseData则直接使用原始apiOuts
setNewData(data.parameters?.apiOuts || []);
}
}
}, [data]);
// 创建包含额外apiOuts的新data对象
const modifiedData = {
...data,
parameters: {
...data.parameters,
apiOuts: newData
}
};
return (
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
@ -31,36 +152,6 @@ const LoopNode = ({ data, id }: { data: any; id: string }) => {
{title}
</div>
{/* 左侧输入连接点 */}
<Handle
type="target"
position={Position.Left}
id="loop-input"
style={{
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)',
left: '-4px'
}}
/>
{/* 右侧输出连接点 */}
<Handle
type="source"
position={Position.Right}
id="loop-output"
style={{
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)',
right: '-4px'
}}
/>
{/* 顶部连接点,用于标识循环开始和结束节点是一组 */}
<Handle
type={id.includes('END') ? 'target' : 'source'}
@ -76,13 +167,7 @@ const LoopNode = ({ data, id }: { data: any; id: string }) => {
}}
/>
{/* 节点内容区域 */}
<div className={styles['node-content']}>
<div className={styles['node-inputs']}>
<div className={styles['node-input-label']}>
</div>
</div>
</div>
<NodeContentLoop data={modifiedData} />
</div>
);
};

@ -335,8 +335,8 @@ export const useFlowCallbacks = (
title: '循环开始',
type: 'LOOP_START',
parameters: {
apiIns: [],
apiOuts: [{ name: 'loopStart', desc: '', dataType: '', defaultValue: '' }],
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: []
},

@ -0,0 +1,211 @@
import React from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { deserializeValue } from '@/utils/common';
import cronstrue from 'cronstrue/i18n';
interface NodeContentData {
parameters?: {
dataIns?: any[];
dataOuts?: any[];
apiIns?: any[];
apiOuts?: any[];
};
showFooter?: boolean;
type?: string;
[key: string]: any;
}
// 定义通用的句柄样式
const handleStyles = {
mainSource: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
mainTarget: {
background: '#2290f6',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
},
data: {
background: '#555',
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}
};
// 渲染LOOP节点的句柄
const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => {
return (
<>
{apiOuts.map((_, index) => (
<Handle
key={`api-output-handle-${index}`}
type="source"
position={Position.Right}
id={apiOuts[index].name || apiOuts[index].id || `output-${index}`}
style={{
...handleStyles.mainSource,
top: `${35 + index * 20}px`
}}
/>
))}
{apiIns.map((_, index) => (
<Handle
key={`api-input-handle-${index}`}
type="target"
position={Position.Left}
id={apiIns[index].name || apiIns[index].id || `input-${index}`}
style={{
...handleStyles.mainTarget,
top: `${35 + index * 20}px`
}}
/>
))}
{/* 输入参数连接端点 */}
{dataIns.map((_, index) => (
<Handle
key={`data-input-handle-${index}`}
type="target"
position={Position.Left}
id={dataIns[index].name || dataIns[index].id || `input-${index}`}
style={{
...handleStyles.data,
top: `${65 + (apiOuts.length + index) * 20}px`
}}
/>
))}
{/* 输出参数连接端点 */}
{dataOuts.map((_, index) => (
<Handle
key={`data-output-handle-${index}`}
type="source"
position={Position.Right}
id={dataOuts[index].name || dataOuts[index].id || `output-${index}`}
style={{
...handleStyles.data,
top: `${65 + (apiOuts.length + index) * 20}px`
}}
/>
))}
</>
);
};
const formatFooter = (data: any) => {
try {
switch (data.type) {
case 'WAIT':
const { duration } = deserializeValue(data.customDef);
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration % 60);
return `${hours}小时${minutes}分钟${seconds}`;
case 'CYCLE':
const { intervalSeconds } = deserializeValue(data.customDef);
return cronstrue.toString(intervalSeconds, { locale: 'zh_CN' });
case 'EVENTSEND':
case 'EVENTLISTENE':
const { name } = data.customDef;
return `事件: ${name}`;
default:
return '这个类型还没开发';
}
} catch (e) {
console.log(e);
}
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
const dataOuts = data.parameters?.dataOuts || [];
const showFooter = data?.component?.customDef || false;
const footerData = (showFooter && data.component) || {};
// 判断节点类型
const isStartNode = data.type === 'start';
const isEndNode = data.type === 'end';
return (
<>
{/*content栏-api部分*/}
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{data.type !== 'LOOP_START' ? input.desc || input.id : ''}
</div>
))}
</div>
)}
{apiOuts.length > 0 && (
<div className={styles['node-outputs-api']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{data.type !== 'LOOP_START' ? output.desc || output.id : ''}
</div>
))}
</div>
)}
</div>
</div>
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}
<div className={styles['node-split-line']}></div>
{/*content栏-data部分*/}
<div className={styles['node-data-box']}>
<div className={styles['node-content']}>
{dataIns.length > 0 && !isStartNode && (
<div className={styles['node-inputs']}>
{dataIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{input.id || `输入${index + 1}`}
</div>
))}
</div>
)}
{dataOuts.length > 0 && !isEndNode && (
<div className={styles['node-outputs']}>
{dataOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-input-label']}>
{output.id || `输出${index + 1}`}
</div>
))}
</div>
)}
</div>
</div>
</>
)}
{/*footer栏*/}
{/*{showFooter && (*/}
{/* <div className={styles['node-footer']}>*/}
{/* {formatFooter(footerData)}*/}
{/* </div>*/}
{/*)}*/}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
</>
);
};
export default NodeContent;

@ -93,8 +93,8 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
title: '循环开始',
type: 'LOOP_START',
parameters: {
apiIns: [],
apiOuts: [{ name: 'loopStart', desc: '', dataType: '', defaultValue: '' }],
apiIns: [{ name: 'start', desc: '', dataType: '', defaultValue: '' }],
apiOuts: [{ name: 'done', desc: '', dataType: '', defaultValue: '' }],
dataIns: [],
dataOuts: []
},
@ -143,6 +143,8 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
}
}
console.log('nodeConfig:', nodeConfig);
// 构造节点数据
const node: any = {
id: nodeConfig.nodeId,
@ -152,25 +154,29 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
title: nodeConfig.nodeName || nodeConfig.nodeId,
parameters: {
apiIns: [{
name: 'start',
name: buildNodeId(nodeConfig.nodeId,'in'),
id: buildNodeId(nodeConfig.nodeId,'in'),
desc: '',
dataType: '',
defaultValue: ''
}],
apiOuts: [{
name: nodeConfig.nodeId === 'end' ? 'end' : 'done',
name: buildNodeId(nodeConfig.nodeId,'out'),
id: buildNodeId(nodeConfig.nodeId,'out'),
desc: '',
dataType: '',
defaultValue: ''
}],
dataIns: nodeConfig.dataIns?.map((input: any) => ({
name: input.id,
id: input.id,
desc: input.desc,
dataType: input.dataType,
defaultValue: input.defaultValue
})) || [],
dataOuts: nodeConfig.dataOuts?.map((output: any) => ({
name: output.id,
id: output.id,
desc: output.desc,
dataType: output.dataType,
defaultValue: output.defaultValue
@ -233,7 +239,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
if (nodes && nodes.length > 0) {
flowData.nodeConfigs = nodes.map(node => {
// 确定 nodeId 和 nodeName
const nodeId = node.id;
const nodeId = node.id || node.name;
const nodeName = node.data?.title || nodeId;
// 确定节点类型
@ -343,3 +349,20 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
return flowData;
};
// 通过nodeType先区分是否需要使用特殊nodeId,不需要就正常分类开始结束的句柄id
const buildNodeId = (nodeId, type) => {
if (nodeId.includes('LOOP_START')) {
if (type === 'in') return 'start';
else return 'done';
}
else if (nodeId.includes('LOOP_END')) {
if (type === 'in') return 'continue';
else return 'break';
}
else {
if (type === 'in') return 'start';
else return nodeId === 'end' ? 'end' : 'done';
}
};

Loading…
Cancel
Save