From a875b65e8bb4f1a0c9fdb41bbe258a8131d3b954 Mon Sep 17 00:00:00 2001 From: ZLY Date: Fri, 23 Jan 2026 17:18:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(flow):=20=E5=AE=8C=E5=96=84=20JSON=20?= =?UTF-8?q?=E4=B8=8E=E5=AD=97=E7=AC=A6=E4=B8=B2=E8=BD=AC=E6=8D=A2=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/JsonToStringEditor.tsx | 10 +++ .../components/StringToJsonEditor.tsx | 10 +++ .../nodeEditors/validators/nodeValidators.ts | 19 +++++ .../components/nodeContentLocal.tsx | 69 ++++++++++++------- .../sideBar/config/localNodeData.ts | 60 +++++++++------- src/utils/convertFlowData.ts | 10 +++ 6 files changed, 128 insertions(+), 50 deletions(-) diff --git a/src/components/FlowEditor/nodeEditors/components/JsonToStringEditor.tsx b/src/components/FlowEditor/nodeEditors/components/JsonToStringEditor.tsx index 3dbca88..c29033b 100644 --- a/src/components/FlowEditor/nodeEditors/components/JsonToStringEditor.tsx +++ b/src/components/FlowEditor/nodeEditors/components/JsonToStringEditor.tsx @@ -17,6 +17,16 @@ const JsonToStringEditor: React.FC = ({ nodeData, updateNodeDat }); }} /> + 输出参数 + { + updateNodeData('parameters', { + ...nodeData.parameters, + dataOuts: data + }); + }} + /> ); }; diff --git a/src/components/FlowEditor/nodeEditors/components/StringToJsonEditor.tsx b/src/components/FlowEditor/nodeEditors/components/StringToJsonEditor.tsx index afac0f3..ccba8c2 100644 --- a/src/components/FlowEditor/nodeEditors/components/StringToJsonEditor.tsx +++ b/src/components/FlowEditor/nodeEditors/components/StringToJsonEditor.tsx @@ -17,6 +17,16 @@ const StringToJsonEditor: React.FC = ({ nodeData, updateNodeDat }); }} /> + 输出参数 + { + updateNodeData('parameters', { + ...nodeData.parameters, + dataOuts: data + }); + }} + /> ); }; diff --git a/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts b/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts index 39a85a0..db54fe5 100644 --- a/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts +++ b/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts @@ -15,6 +15,14 @@ export interface ValidationResult { export const validateNodeData = (nodeData: any, nodeType: string): ValidationResult => { const errors: string[] = []; + // JSON2STR 和 STR2JSON 组件跳过保存校验,只需要连线校验 + if (nodeType === 'JSON2STR' || nodeType === 'STR2JSON') { + return { + isValid: true, + errors: [] + }; + } + // 检查基本字段 if (!nodeData.title) { errors.push('节点标题不能为空'); @@ -460,6 +468,17 @@ export const validateAllEdges = (edges: Edge[], nodes: any[]): ValidationResult // 特定节点类型可能有特殊的连接规则 switch (nodeType) { + case 'JSON2STR': + case 'STR2JSON': + // JSON 转换组件必须有输入和输出连接(纯数据流组件) + if (targetEdges === 0) { + allErrors.push(`${nodeType === 'JSON2STR' ? 'JSON转字符串' : '字符串转JSON'}节点"${nodeName}"缺少输入连接`); + } + if (sourceEdges === 0) { + allErrors.push(`${nodeType === 'JSON2STR' ? 'JSON转字符串' : '字符串转JSON'}节点"${nodeName}"缺少输出连接`); + } + break; + case 'SWITCH': // 条件节点应该有至少一个输出连接 if (sourceEdges === 0) { diff --git a/src/pages/flowEditor/components/nodeContentLocal.tsx b/src/pages/flowEditor/components/nodeContentLocal.tsx index d5241a4..0d0bd5c 100644 --- a/src/pages/flowEditor/components/nodeContentLocal.tsx +++ b/src/pages/flowEditor/components/nodeContentLocal.tsx @@ -121,6 +121,10 @@ const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, data // 渲染普通节点的句柄 const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[], apiOuts: any[]) => { + // 计算 API 占位数量:如果没有 API 句柄,默认占位 1 个 + const apiInsPlaceholder = Math.max(apiIns.length, 1); + const apiOutsPlaceholder = Math.max(apiOuts.length, 1); + return ( <> {apiOuts.map((_, index) => ( @@ -157,7 +161,7 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[] id={dataIns[index].name || dataIns[index].id || `input-${index}`} style={{ ...handleStyles.data, - top: `${65 + (apiIns.length + index) * 22}px` + top: `${65 + (apiInsPlaceholder + index) * 22}px` }} /> ))} @@ -171,7 +175,7 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[] id={dataOuts[index].name || dataOuts[index].id || `output-${index}`} style={{ ...handleStyles.data, - top: `${65 + (apiIns.length + index) * 22}px` + top: `${65 + (apiOutsPlaceholder + index) * 22}px` }} /> ))} @@ -235,32 +239,49 @@ const NodeContentLocal = ({ data }: { data: NodeContentData }) => { const isEndNode = data.type === 'end'; const isSpecialNode = isStartNode || isEndNode; + // 判断是否需要显示 API 区域(即使为空也显示占位) + const hasApiSection = apiIns.length > 0 || apiOuts.length > 0 || dataIns.length > 0 || dataOuts.length > 0; + return ( <> - {/*content栏-api部分*/} -
-
- {apiIns.length > 0 && ( -
- {apiIns.map((input, index) => ( -
- {formatTitle(input.desc || input.id || input.name)} -
- ))} -
- )} + {/*content栏-api部分 - 始终显示以保持布局一致*/} + {hasApiSection && ( +
+
+ {(apiIns.length > 0 || dataIns.length > 0) && ( +
+ {apiIns.map((input, index) => ( +
+ {formatTitle(input.desc || input.id || input.name)} +
+ ))} + {/* 如果没有 API 输入但有数据输入,添加占位 */} + {apiIns.length === 0 && dataIns.length > 0 && ( +
+ 占位 +
+ )} +
+ )} - {apiOuts.length > 0 && ( -
- {apiOuts.map((output, index) => ( -
- {output.desc} -
- ))} -
- )} + {(apiOuts.length > 0 || dataOuts.length > 0) && ( +
+ {apiOuts.map((output, index) => ( +
+ {output.desc} +
+ ))} + {/* 如果没有 API 输出但有数据输出,添加占位 */} + {apiOuts.length === 0 && dataOuts.length > 0 && ( +
+ 占位 +
+ )} +
+ )} +
-
+ )} {(dataIns.length > 0 || dataOuts.length > 0) && ( <> {/*分割*/} diff --git a/src/pages/flowEditor/sideBar/config/localNodeData.ts b/src/pages/flowEditor/sideBar/config/localNodeData.ts index 4937d0a..a537a7a 100644 --- a/src/pages/flowEditor/sideBar/config/localNodeData.ts +++ b/src/pages/flowEditor/sideBar/config/localNodeData.ts @@ -15,36 +15,44 @@ const defaultParameters = { dataOuts: [] }; const json2strParameters = { - apiIns: [{ - name: 'start', - desc: 'JSON', + apiIns: [], + apiOuts: [], + dataIns: [{ + id: 'jsonInput', + name: 'jsonInput', + desc: 'JSON输入', dataType: 'JSON', - defaultValue: '' - }], - apiOuts: [{ - name: 'done', - desc: 'STR', - dataType: 'STR', - defaultValue: '' + defaultValue: '', + arrayType: null }], - dataIns: [], - dataOuts: [] + dataOuts: [{ + id: 'stringOutput', + name: 'stringOutput', + desc: '字符串输出', + dataType: 'STRING', + defaultValue: '', + arrayType: null + }] }; const str2jsonParameters = { - apiIns: [{ - name: 'start', - desc: 'STR', - dataType: 'STR', - defaultValue: '' + apiIns: [], + apiOuts: [], + dataIns: [{ + id: 'stringInput', + name: 'stringInput', + desc: '字符串输入', + dataType: 'STRING', + defaultValue: '', + arrayType: null }], - apiOuts: [{ - name: 'done', - desc: 'JSON', + dataOuts: [{ + id: 'jsonOutput', + name: 'jsonOutput', + desc: 'JSON输出', dataType: 'JSON', - defaultValue: '' - }], - dataIns: [], - dataOuts: [] + defaultValue: '', + arrayType: null + }] }; const switchParameters = { apiIns: [{ @@ -118,8 +126,8 @@ const nodeDefinitions = [ { nodeName: '事件接收', nodeType: 'EVENTLISTENE', nodeGroup: 'common', icon: 'IconImport' }, { nodeName: '事件发送', nodeType: 'EVENTSEND', nodeGroup: 'common', icon: 'IconExport' }, { nodeName: '语音输入', nodeType: 'MICRO', nodeGroup: 'common', icon: 'IconVoice' }, - // { nodeName: 'JSON转字符串', nodeType: 'JSON2STR', nodeGroup: 'common', icon: 'IconCodeBlock' }, - // { nodeName: '字符串转JSON', nodeType: 'STR2JSON', nodeGroup: 'common', icon: 'IconCodeSquare' }, + { nodeName: 'JSON转字符串', nodeType: 'JSON2STR', nodeGroup: 'common', icon: 'IconCodeBlock' }, + { nodeName: '字符串转JSON', nodeType: 'STR2JSON', nodeGroup: 'common', icon: 'IconCodeSquare' }, // { nodeName: 'JSON封装', nodeType: 'JSONCONVERT', nodeGroup: 'common', icon: 'IconTranslate' }, { nodeName: '结果展示', nodeType: 'RESULT', nodeGroup: 'common', icon: 'IconInteraction' }, { nodeName: '图片展示', nodeType: 'IMAGE', nodeGroup: 'common', icon: 'IconImage' }, diff --git a/src/utils/convertFlowData.ts b/src/utils/convertFlowData.ts index f12ec60..32e3588 100644 --- a/src/utils/convertFlowData.ts +++ b/src/utils/convertFlowData.ts @@ -660,6 +660,11 @@ export const reverseConvertFlowData = (nodes: any[], edges: any[], complexKV: an // 获取节点的API输入参数 const getNodeApiIns = (nodeId: string, nodeConfig: any, currentProjectCompData: any[]) => { + // JSON2STR 和 STR2JSON 不需要 API 输入 + if (nodeConfig.component?.type === 'JSON2STR' || nodeConfig.component?.type === 'STR2JSON') { + return []; + } + // 对于特定类型的节点使用预定义值 if (nodeConfig.component?.type === 'LOOP_START') { return [{ name: 'start', desc: '', dataType: '', defaultValue: '' }]; @@ -691,6 +696,11 @@ const getNodeApiIns = (nodeId: string, nodeConfig: any, currentProjectCompData: // 获取节点的API输出参数 const getNodeApiOuts = (nodeId: string, nodeConfig: any, currentProjectCompData: any[]) => { + // JSON2STR 和 STR2JSON 不需要 API 输出 + if (nodeConfig.component?.type === 'JSON2STR' || nodeConfig.component?.type === 'STR2JSON') { + return []; + } + // 对于特定类型的节点使用预定义值 if (nodeConfig.component?.type === 'LOOP_START') { return [{ name: 'done', desc: '', dataType: '', defaultValue: '' }];