diff --git a/src/components/FlowEditor/nodeEditors/index.tsx b/src/components/FlowEditor/nodeEditors/index.tsx index efef826..4c31aef 100644 --- a/src/components/FlowEditor/nodeEditors/index.tsx +++ b/src/components/FlowEditor/nodeEditors/index.tsx @@ -41,6 +41,8 @@ export const getNodeEditorByType = (nodeType: string, localNodeType?: string) => return nodeEditors[nodeType] || nodeEditors['basic']; }; +export * from './validators'; + export default { nodeEditors, registerNodeEditor, diff --git a/src/components/FlowEditor/nodeEditors/validators/index.ts b/src/components/FlowEditor/nodeEditors/validators/index.ts new file mode 100644 index 0000000..f367b73 --- /dev/null +++ b/src/components/FlowEditor/nodeEditors/validators/index.ts @@ -0,0 +1 @@ +export * from './nodeValidators'; \ No newline at end of file diff --git a/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts b/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts new file mode 100644 index 0000000..19c63a2 --- /dev/null +++ b/src/components/FlowEditor/nodeEditors/validators/nodeValidators.ts @@ -0,0 +1,383 @@ +import { Message } from '@arco-design/web-react'; + +export interface ValidationResult { + isValid: boolean; + errors: string[]; +} + +/** + * 校验节点数据是否完整 + * @param nodeData 节点数据 + * @param nodeType 节点类型 + * @returns 校验结果 + */ +export const validateNodeData = (nodeData: any, nodeType: string): ValidationResult => { + const errors: string[] = []; + + // 检查基本字段 + if (!nodeData.title) { + errors.push('节点标题不能为空'); + } + + // 根据不同节点类型进行特定校验 + switch (nodeType) { + case 'REST': + errors.push(...validateRestNode(nodeData)); + break; + case 'CODE': + errors.push(...validateCodeNode(nodeData)); + break; + case 'SWITCH': + errors.push(...validateSwitchNode(nodeData)); + break; + case 'LOOP_START': + case 'LOOP_END': + errors.push(...validateLoopNode(nodeData, nodeType)); + break; + case 'EVENTSEND': + case 'EVENTLISTENE': + errors.push(...validateEventNode(nodeData)); + break; + case 'WAIT': + errors.push(...validateWaitNode(nodeData)); + break; + case 'CYCLE': + errors.push(...validateCycleNode(nodeData)); + break; + default: + // 对于其他节点类型,检查基本参数 + errors.push(...validateBasicParams(nodeData)); + break; + } + + return { + isValid: errors.length === 0, + errors + }; +}; + +/** + * 校验REST节点 + * @param nodeData 节点数据 + * @returns 错误信息数组 + */ +const validateRestNode = (nodeData: any): string[] => { + const errors: string[] = []; + + if (!nodeData.component || !nodeData.component.customDef) { + errors.push('REST节点配置信息不完整'); + return errors; + } + + let customDef; + try { + customDef = typeof nodeData.component.customDef === 'string' + ? JSON.parse(nodeData.component.customDef) + : nodeData.component.customDef; + } catch (e) { + errors.push('REST节点配置信息格式错误'); + return errors; + } + + if (!customDef.method) { + errors.push('请求方法不能为空'); + } + + if (!customDef.url) { + errors.push('URL地址不能为空'); + } + else { + // 基本的 URL 格式校验 + const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/; + if (!urlPattern.test(customDef.url)) { + errors.push('URL地址格式不正确'); + } + } + + return errors; +}; + +/** + * 校验代码节点 + * @param nodeData 节点数据 + * @returns 错误信息数组 + */ +const validateCodeNode = (nodeData: any): string[] => { + const errors: string[] = []; + + if (!nodeData.component || !nodeData.component.customDef) { + errors.push('代码节点配置信息不完整'); + return errors; + } + + let customDef; + try { + customDef = typeof nodeData.component.customDef === 'string' + ? JSON.parse(nodeData.component.customDef) + : nodeData.component.customDef; + } catch (e) { + errors.push('代码节点配置信息格式错误'); + return errors; + } + + if (!customDef.sourceCode) { + errors.push('代码内容不能为空'); + } + + return errors; +}; + +/** + * 校验条件节点 + * @param nodeData 节点数据 + * @returns 错误信息数组 + */ +const validateSwitchNode = (nodeData: any): string[] => { + const errors: string[] = []; + + // 检查条件表达式 + if (!nodeData.component || !nodeData.component.customDef) { + errors.push('条件节点配置信息不完整'); + return errors; + } + + let customDef; + try { + customDef = typeof nodeData.component.customDef === 'string' + ? JSON.parse(nodeData.component.customDef) + : nodeData.component.customDef; + } catch (e) { + errors.push('条件节点配置信息格式错误'); + return errors; + } + + // 检查是否有条件表达式 + if (!customDef.conditions || customDef.conditions.length === 0) { + errors.push('请至少添加一个条件表达式'); + } + + // 检查每个条件表达式的完整性 + if (customDef.conditions && customDef.conditions.length > 0) { + customDef.conditions.forEach((condition: any, index: number) => { + if (!condition.apiOutId) { + errors.push(`第${index + 1}个条件表达式的逻辑出口不能为空`); + } + if (!condition.expression) { + errors.push(`第${index + 1}个条件表达式的表达式不能为空`); + } + }); + } + + return errors; +}; + +/** + * 校验循环节点 + * @param nodeData 节点数据 + * @param nodeType 节点类型 + * @returns 错误信息数组 + */ +const validateLoopNode = (nodeData: any, nodeType: string): string[] => { + // LOOP_START 类型节点不需要校验 + if (nodeType === 'LOOP_START') { + return []; + } + + const errors: string[] = []; + + // 检查循环条件(仅针对 LOOP_END 节点) + if (!nodeData.component || !nodeData.component.customDef) { + errors.push('循环节点配置信息不完整'); + return errors; + } + + let customDef; + try { + customDef = typeof nodeData.component.customDef === 'string' + ? JSON.parse(nodeData.component.customDef) + : nodeData.component.customDef; + } catch (e) { + errors.push('循环节点配置信息格式错误'); + return errors; + } + + // 检查循环条件 + if (!customDef.conditions || customDef.conditions.length === 0) { + errors.push('请至少添加一个循环条件'); + } + + return errors; +}; + +/** + * 校验事件节点 + * @param nodeData 节点数据 + * @returns 错误信息数组 + */ +const validateEventNode = (nodeData: any): string[] => { + const errors: string[] = []; + + if (!nodeData.component || !nodeData.component.customDef) { + errors.push('事件节点配置信息不完整'); + return errors; + } + + let customDef; + try { + customDef = typeof nodeData.component.customDef === 'string' + ? JSON.parse(nodeData.component.customDef) + : nodeData.component.customDef; + } catch (e) { + errors.push('事件节点配置信息格式错误'); + return errors; + } + + if (!customDef.eventId) { + errors.push('请选择事件'); + } + + return errors; +}; + +/** + * 校验等待节点 + * @param nodeData 节点数据 + * @returns 错误信息数组 + */ +const validateWaitNode = (nodeData: any): string[] => { + const errors: string[] = []; + + if (!nodeData.component || !nodeData.component.customDef) { + errors.push('等待节点配置信息不完整'); + return errors; + } + + let customDef; + try { + customDef = typeof nodeData.component.customDef === 'string' + ? JSON.parse(nodeData.component.customDef) + : nodeData.component.customDef; + } catch (e) { + errors.push('等待节点配置信息格式错误'); + return errors; + } + + // 检查等待时间是否设置 + if (customDef.duration === undefined || customDef.duration === null) { + errors.push('请设置等待时间'); + } + else if (typeof customDef.duration !== 'number' || customDef.duration < 0) { + errors.push('等待时间必须是非负数'); + } + + return errors; +}; + +/** + * 校验周期节点 + * @param nodeData 节点数据 + * @returns 错误信息数组 + */ +const validateCycleNode = (nodeData: any): string[] => { + const errors: string[] = []; + + if (!nodeData.component || !nodeData.component.customDef) { + errors.push('周期节点配置信息不完整'); + return errors; + } + + let customDef; + try { + customDef = typeof nodeData.component.customDef === 'string' + ? JSON.parse(nodeData.component.customDef) + : nodeData.component.customDef; + } catch (e) { + errors.push('周期节点配置信息格式错误'); + return errors; + } + + // 检查 Cron 表达式是否设置 + if (!customDef.intervalSeconds) { + errors.push('请设置 Cron 表达式'); + } + + return errors; +}; + +/** + * 校验基本参数 + * @param nodeData 节点数据 + * @returns 错误信息数组 + */ +const validateBasicParams = (nodeData: any): string[] => { + const errors: string[] = []; + + // 检查输入参数的完整性 + if (nodeData.parameters?.dataIns) { + nodeData.parameters.dataIns.forEach((param: any, index: number) => { + if (!param.id) { + errors.push(`第${index + 1}个输入参数的标识不能为空`); + } + if (!param.dataType) { + errors.push(`第${index + 1}个输入参数的数据类型不能为空`); + } + }); + } + + // 检查输出参数的完整性 + if (nodeData.parameters?.dataOuts) { + nodeData.parameters.dataOuts.forEach((param: any, index: number) => { + if (!param.id) { + errors.push(`第${index + 1}个输出参数的标识不能为空`); + } + if (!param.dataType) { + errors.push(`第${index + 1}个输出参数的数据类型不能为空`); + } + }); + } + + return errors; +}; + +/** + * 校验整个流程的所有节点 + * @param nodes 流程中的所有节点 + * @returns 校验结果 + */ +export const validateAllNodes = (nodes: any[]): ValidationResult => { + const allErrors: string[] = []; + + nodes.forEach((node, index) => { + const nodeType = node.data?.type || node.type; + const nodeName = node.data?.title || `节点${index + 1}`; + + const result = validateNodeData(node.data, nodeType); + if (!result.isValid) { + result.errors.forEach(error => { + allErrors.push(`[${nodeName}] ${error}`); + }); + } + }); + + return { + isValid: allErrors.length === 0, + errors: allErrors + }; +}; + +/** + * 显示校验错误信息 + * @param errors 错误信息数组 + */ +export const showValidationErrors = (errors: string[]) => { + if (errors.length > 0) { + // 创建错误信息内容 + let content = '存在以下问题需要修正:\n'; + content += errors.map((error, index) => `${index + 1}. ${error}`).join('\n'); + + Message.error({ + content: content, + duration: 5000 + }); + } +}; diff --git a/src/hooks/useFlowCallbacks.ts b/src/hooks/useFlowCallbacks.ts index cdc319a..45816e5 100644 --- a/src/hooks/useFlowCallbacks.ts +++ b/src/hooks/useFlowCallbacks.ts @@ -24,6 +24,7 @@ import ImageNode from '@/components/FlowEditor/node/imageNode/ImageNode'; import CodeNode from '@/components/FlowEditor/node/codeNode/CodeNode'; import RestNode from '@/components/FlowEditor/node/restNode/RestNode'; import { updateCanvasDataMap } from '@/store/ideContainer'; +import { validateAllNodes, showValidationErrors } from '@/components/FlowEditor/nodeEditors/validators/nodeValidators'; import { Dispatch } from 'redux'; @@ -214,8 +215,6 @@ export const useFlowCallbacks = ( const sourceParams = sourceNode.data?.parameters || {}; const targetParams = targetNode.data?.parameters || {}; - console.log('params:', params, sourceParams, targetParams); - // 获取源handle和目标handle的类型 (api或data) const sourceHandleType = getHandleType(params.sourceHandle, sourceParams); const targetHandleType = getHandleType(params.targetHandle, targetParams); @@ -782,6 +781,13 @@ export const useFlowCallbacks = ( // 保存所有节点和边数据到服务器 const saveFlowDataToServer = useCallback(async () => { try { + // 首先校验所有节点数据是否完整 + const validation = validateAllNodes(nodes); + if (!validation.isValid) { + showValidationErrors(validation.errors); + return; + } + // 转换会原始数据类型 const revertedData = revertFlowData(nodes, edges); console.log('revertedData(中断):', revertedData);