From bba0197215da23f77a2cdce96da9a1a7ff835622 Mon Sep 17 00:00:00 2001 From: ZLY Date: Thu, 16 Oct 2025 11:38:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(flow):=E4=BC=98=E5=8C=96=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E9=85=8D=E7=BD=AE=E4=B8=8E=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整 LOOP_START 节点的 apiIns 和 apiOuts 参数结构 - 新增 buildNodeId 工具函数统一管理节点句柄 ID 生成逻辑- 在 convertFlowData 中增强 nodeId 获取逻辑,兼容 id与 name 字段 - LoopNode 组件新增状态管理,支持动态解析条件表达式并生成 apiOuts - 引入 NodeContentLoop 组件专门负责循环节点的内容渲染和句柄绘制 - 更新 useFlowCallbacks 中 LOOP_START 节点默认参数配置 - 新增 nodeContentLoop 组件实现节点内容与句柄的模块化渲染 --- .../FlowEditor/node/loopNode/LoopNode.tsx | 163 ++++++++++---- src/hooks/useFlowCallbacks.ts | 4 +- .../flowEditor/components/nodeContentLoop.tsx | 211 ++++++++++++++++++ src/utils/convertFlowData.ts | 33 ++- 4 files changed, 365 insertions(+), 46 deletions(-) create mode 100644 src/pages/flowEditor/components/nodeContentLoop.tsx diff --git a/src/components/FlowEditor/node/loopNode/LoopNode.tsx b/src/components/FlowEditor/node/loopNode/LoopNode.tsx index 65e3117..b54cacd 100644 --- a/src/components/FlowEditor/node/loopNode/LoopNode.tsx +++ b/src/components/FlowEditor/node/loopNode/LoopNode.tsx @@ -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([]); 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 (
@@ -31,36 +152,6 @@ const LoopNode = ({ data, id }: { data: any; id: string }) => { {title}
- {/* 左侧输入连接点 */} - - - {/* 右侧输出连接点 */} - - {/* 顶部连接点,用于标识循环开始和结束节点是一组 */} { }} /> - {/* 节点内容区域 */} -
-
-
-
-
-
+
); }; diff --git a/src/hooks/useFlowCallbacks.ts b/src/hooks/useFlowCallbacks.ts index eca0cd5..4ebc6bb 100644 --- a/src/hooks/useFlowCallbacks.ts +++ b/src/hooks/useFlowCallbacks.ts @@ -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: [] }, diff --git a/src/pages/flowEditor/components/nodeContentLoop.tsx b/src/pages/flowEditor/components/nodeContentLoop.tsx new file mode 100644 index 0000000..b40d570 --- /dev/null +++ b/src/pages/flowEditor/components/nodeContentLoop.tsx @@ -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) => ( + + ))} + {apiIns.map((_, index) => ( + + ))} + + {/* 输入参数连接端点 */} + {dataIns.map((_, index) => ( + + ))} + + {/* 输出参数连接端点 */} + {dataOuts.map((_, index) => ( + + ))} + + ); +}; + +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部分*/} +
+
+ {apiIns.length > 0 && ( +
+ {apiIns.map((input, index) => ( +
+ {data.type !== 'LOOP_START' ? input.desc || input.id : ''} +
+ ))} +
+ )} + + {apiOuts.length > 0 && ( +
+ {apiOuts.map((output, index) => ( +
+ {data.type !== 'LOOP_START' ? output.desc || output.id : ''} +
+ ))} +
+ )} +
+
+ {(dataIns.length > 0 || dataOuts.length > 0) && ( + <> + {/*分割*/} +
+ + {/*content栏-data部分*/} +
+
+ {dataIns.length > 0 && !isStartNode && ( +
+ {dataIns.map((input, index) => ( +
+ {input.id || `输入${index + 1}`} +
+ ))} +
+ )} + + {dataOuts.length > 0 && !isEndNode && ( +
+ {dataOuts.map((output, index) => ( +
+ {output.id || `输出${index + 1}`} +
+ ))} +
+ )} +
+
+ + )} + + {/*footer栏*/} + {/*{showFooter && (*/} + {/*
*/} + {/* {formatFooter(footerData)}*/} + {/*
*/} + {/*)}*/} + + {renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)} + + ); +}; + +export default NodeContent; \ No newline at end of file diff --git a/src/utils/convertFlowData.ts b/src/utils/convertFlowData.ts index f0aeb9c..ba26d84 100644 --- a/src/utils/convertFlowData.ts +++ b/src/utils/convertFlowData.ts @@ -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'; + } +};