feat(flow): 完善 JSON 与字符串转换节点功能

master
钟良源 2 weeks ago
parent 72802cb143
commit a875b65e8b

@ -17,6 +17,16 @@ const JsonToStringEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeDat
});
}}
/>
<Typography.Title heading={5} style={{ marginTop: 16 }}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataOuts || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataOuts: data
});
}}
/>
</>
);
};

@ -17,6 +17,16 @@ const StringToJsonEditor: React.FC<NodeEditorProps> = ({ nodeData, updateNodeDat
});
}}
/>
<Typography.Title heading={5} style={{ marginTop: 16 }}><IconUnorderedList style={{ marginRight: 5 }} /></Typography.Title>
<ParamsTable
initialData={nodeData.parameters.dataOuts || []}
onUpdateData={(data) => {
updateNodeData('parameters', {
...nodeData.parameters,
dataOuts: data
});
}}
/>
</>
);
};

@ -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) {

@ -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部分*/}
{/*content栏-api部分 - 始终显示以保持布局一致*/}
{hasApiSection && (
<div className={styles['node-api-box']}>
<div className={styles['node-content-api']}>
{apiIns.length > 0 && (
{(apiIns.length > 0 || dataIns.length > 0) && (
<div className={styles['node-inputs']}>
{apiIns.map((input, index) => (
<div key={input.id || `input-${index}`} className={styles['node-input-label']}>
{formatTitle(input.desc || input.id || input.name)}
</div>
))}
{/* 如果没有 API 输入但有数据输入,添加占位 */}
{apiIns.length === 0 && dataIns.length > 0 && (
<div className={styles['node-input-label']} style={{ visibility: 'hidden' }}>
</div>
)}
</div>
)}
{apiOuts.length > 0 && (
{(apiOuts.length > 0 || dataOuts.length > 0) && (
<div className={styles['node-outputs']}>
{apiOuts.map((output, index) => (
<div key={output.id || `output-${index}`} className={styles['node-output-label']}>
{output.desc}
</div>
))}
{/* 如果没有 API 输出但有数据输出,添加占位 */}
{apiOuts.length === 0 && dataOuts.length > 0 && (
<div className={styles['node-output-label']} style={{ visibility: 'hidden' }}>
</div>
)}
</div>
)}
</div>
</div>
)}
{(dataIns.length > 0 || dataOuts.length > 0) && (
<>
{/*分割*/}

@ -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' },

@ -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: '' }];

Loading…
Cancel
Save