import React, { useState, useEffect, useMemo } from 'react'; import { FormItem, Input, NumberPicker, Select, FormLayout, Switch } from '@formily/antd-v5'; import { createForm } from '@formily/core'; import { createSchemaField, FormProvider, ISchema } from '@formily/react'; import { Save, RotateCcw } from 'lucide-react'; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; import type { FlowNode, FlowNodeData } from '../types'; import type { WorkflowNodeDefinition } from '../nodes/types'; import { isConfigurableNode } from '../nodes/types'; // 创建Schema组件 const SchemaField = createSchemaField({ components: { FormItem, Input, NumberPicker, Select, FormLayout, Switch, 'Input.TextArea': Input.TextArea, }, }); interface NodeConfigModalProps { visible: boolean; node: FlowNode | null; onCancel: () => void; onOk: (nodeId: string, updatedData: Partial) => void; } const NodeConfigModal: React.FC = ({ visible, node, onCancel, onOk }) => { const [loading, setLoading] = useState(false); const { toast } = useToast(); // 获取节点定义 const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null; // ✅ 根据节点 ID 重新创建表单实例(修复切换节点时数据不更新的问题) const configForm = useMemo(() => createForm(), [node?.id]); const inputForm = useMemo(() => createForm(), [node?.id]); // 初始化表单数据 useEffect(() => { if (visible && node && nodeDefinition) { const nodeData = node.data || {}; // 准备默认配置 const defaultConfig = { nodeName: nodeDefinition.nodeName, nodeCode: nodeDefinition.nodeCode, description: nodeDefinition.description }; // 设置表单初始值 configForm.setInitialValues({ ...defaultConfig, ...(nodeData.configs || {}) }); configForm.reset(); if (isConfigurableNode(nodeDefinition)) { inputForm.setInitialValues(nodeData.inputMapping || {}); inputForm.reset(); } } }, [visible, node, nodeDefinition, configForm, inputForm]); // 递归处理表单值,将JSON字符串转换为对象 const processFormValues = (values: Record, schema: ISchema | undefined): Record => { const result: Record = {}; if (!schema?.properties || typeof schema.properties !== 'object') return values; Object.entries(values).forEach(([key, value]) => { const propSchema = (schema.properties as Record)?.[key]; // 如果是object类型且值是字符串,尝试解析 if (propSchema?.type === 'object' && typeof value === 'string') { try { result[key] = JSON.parse(value); } catch { result[key] = value; // 解析失败保持原值 } } else { result[key] = value; } }); return result; }; const handleSubmit = async () => { if (!node || !nodeDefinition) return; try { setLoading(true); // 获取表单值并转换 const configs = processFormValues(configForm.values, nodeDefinition.configSchema); const inputMapping = isConfigurableNode(nodeDefinition) ? processFormValues(inputForm.values, nodeDefinition.inputMappingSchema) : {}; const updatedData: Partial = { label: configs.nodeName || node.data.label, configs, inputMapping, // outputs 保留原值(不修改,因为它是只读的输出能力定义) outputs: node.data.outputs || [], }; onOk(node.id, updatedData); toast({ title: "保存成功", description: "节点配置已成功保存", }); onCancel(); } catch (error) { if (error instanceof Error) { toast({ variant: "destructive", title: "保存失败", description: error.message, }); } } finally { setLoading(false); } }; const handleReset = () => { configForm.reset(); inputForm.reset(); toast({ title: "已重置", description: "表单已重置为初始值", }); }; // 将JSON Schema转换为Formily Schema(扩展配置) const convertToFormilySchema = (jsonSchema: ISchema): ISchema => { const schema: ISchema = { type: 'object', properties: {} }; if (!jsonSchema.properties || typeof jsonSchema.properties !== 'object') return schema; Object.entries(jsonSchema.properties as Record).forEach(([key, prop]: [string, any]) => { const field: any = { title: prop.title || key, description: prop.description, 'x-decorator': 'FormItem', 'x-decorator-props': { tooltip: prop.description, // 垂直布局不需要 labelCol 和 wrapperCol }, }; // 根据类型设置组件 switch (prop.type) { case 'string': if (prop.enum) { field['x-component'] = 'Select'; field['x-component-props'] = { style: { width: '100%' }, // 统一宽度 options: prop.enum.map((v: any, i: number) => ({ label: prop.enumNames?.[i] || v, value: v })) }; } else if (prop.format === 'password') { field['x-component'] = 'Input'; field['x-component-props'] = { type: 'password', style: { width: '100%' } // 统一宽度 }; } else { field['x-component'] = 'Input'; field['x-component-props'] = { style: { width: '100%' } // 统一宽度 }; } break; case 'number': case 'integer': field['x-component'] = 'NumberPicker'; field['x-component-props'] = { min: prop.minimum, max: prop.maximum, style: { width: '100%' } // 统一宽度 }; break; case 'boolean': field['x-component'] = 'Switch'; break; case 'object': field['x-component'] = 'Input.TextArea'; field['x-component-props'] = { rows: 4, style: { width: '100%' }, // 统一宽度 placeholder: '请输入JSON格式,例如:{"key": "value"}' }; // ✅ 关键修复:将object类型的default值转换为JSON字符串 if (prop.default !== undefined && typeof prop.default === 'object') { field.default = JSON.stringify(prop.default, null, 2); } else { field.default = prop.default; } // Formily会自动处理object的序列化 field['x-validator'] = (value: any) => { if (!value) return true; if (typeof value === 'string') { try { JSON.parse(value); return true; } catch { return '请输入有效的JSON格式'; } } return true; }; break; default: field['x-component'] = 'Input'; field['x-component-props'] = { style: { width: '100%' } // 统一宽度 }; } // 设置默认值(非object类型) if (prop.type !== 'object' && prop.default !== undefined) { field.default = prop.default; } // 设置必填 if (Array.isArray(jsonSchema.required) && jsonSchema.required.includes(key)) { field.required = true; } (schema.properties as Record)[key] = field; }); return schema; }; if (!nodeDefinition) { return null; } return ( !open && onCancel()}> {/* Header - 固定在顶部 */}
编辑节点 - {nodeDefinition.nodeName} 配置节点的参数和映射关系
{/* Content - 可滚动区域 */}
{/* 使用 Accordion 支持多个面板同时展开,默认展开基本配置 */} {/* 基本配置 - 始终显示 */} 基本配置 {/* 输入映射 - 条件显示 */} {isConfigurableNode(nodeDefinition) && nodeDefinition.inputMappingSchema && ( 输入映射 )} {/* 输出能力 - 只读展示 */} {isConfigurableNode(nodeDefinition) && nodeDefinition.outputs && nodeDefinition.outputs.length > 0 && ( 输出能力
💡 此节点执行后会产生以下数据,下游节点可以通过 {'${upstream.字段名}'} 引用这些数据
{nodeDefinition.outputs.map((output) => (
{output.title} {output.name} {output.type} {output.required && ( * )}

{output.description}

{output.enum && (
可选值: {output.enum.join(', ')}
)} {output.example !== undefined && (
示例: {typeof output.example === 'object' ? JSON.stringify(output.example) : String(output.example)}
)}
))}
)}
{/* Footer - 固定在底部 */}
); }; export default NodeConfigModal;