import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; 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 { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { useToast } from '@/components/ui/use-toast'; import type { FlowNode, FlowNodeData, FlowEdge } from '../types'; import type { WorkflowNodeDefinition, JSONSchema } from '../nodes/types'; import { isConfigurableNode } from '../nodes/types'; import { convertJsonSchemaToZod, extractDataSourceTypes } from '../utils/schemaConverter'; import { loadDataSource, DataSourceType, type DataSourceOption } from '../utils/dataSourceLoader'; import { convertObjectToUUID, convertObjectToDisplayName } from '@/utils/workflow/variableConversion'; import VariableInput from '@/components/VariableInput'; interface NodeConfigModalProps { visible: boolean; node: FlowNode | null; allNodes: FlowNode[]; allEdges: FlowEdge[]; onCancel: () => void; onOk: (nodeId: string, updatedData: Partial) => void; } const NodeConfigModal: React.FC = ({ visible, node, allNodes, allEdges, onCancel, onOk }) => { const [loading, setLoading] = useState(false); const { toast } = useToast(); // 固定的节点信息(不通过 schema,直接管理) const [nodeName, setNodeName] = useState(''); const [description, setDescription] = useState(''); // 动态数据源缓存 const [dataSourceCache, setDataSourceCache] = useState>({}); const [loadingDataSources, setLoadingDataSources] = useState(false); // ✅ 性能优化:使用 ref 保存 allNodes 和 allEdges 的最新引用 // 避免这些频繁变化的数组导致 useCallback 重建 const allNodesRef = useRef(allNodes); const allEdgesRef = useRef(allEdges); useEffect(() => { allNodesRef.current = allNodes; allEdgesRef.current = allEdges; }, [allNodes, allEdges]); // 获取节点定义 const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null; // ✅ 生成 Zod Schema(仅输入映射) const inputMappingSchema = useMemo(() => { if (!nodeDefinition || !isConfigurableNode(nodeDefinition) || !nodeDefinition.inputMappingSchema) { return null; } return convertJsonSchemaToZod(nodeDefinition.inputMappingSchema); }, [nodeDefinition]); // ✅ 创建表单实例(仅输入映射) const inputForm = useForm({ resolver: inputMappingSchema ? zodResolver(inputMappingSchema) : undefined, defaultValues: {} }); // ✅ 预加载动态数据源(仅输入映射) useEffect(() => { if (!visible || !nodeDefinition) return; const loadDynamicData = async () => { const inputTypes = isConfigurableNode(nodeDefinition) && nodeDefinition.inputMappingSchema ? extractDataSourceTypes(nodeDefinition.inputMappingSchema) : []; if (inputTypes.length === 0) { return; } setLoadingDataSources(true); const cache: Record = {}; let failedCount = 0; // ✅ 改进:单独处理每个数据源的加载,避免一个失败影响全部 await Promise.allSettled( inputTypes.map(async (type) => { try { const data = await loadDataSource(type as DataSourceType); cache[type] = data; } catch (error) { console.error(`加载数据源 ${type} 失败:`, error); failedCount++; cache[type] = []; // 失败时使用空数组 } }) ); setDataSourceCache(cache); setLoadingDataSources(false); // 如果有失败的数据源,显示提示 if (failedCount > 0) { toast({ title: '部分数据加载失败', description: `${failedCount} 个选项数据加载失败,请检查网络后重新打开节点配置`, variant: 'destructive' }); } }; loadDynamicData(); }, [visible, nodeDefinition, toast]); // ✅ 初始化表单数据 useEffect(() => { if (visible && node && nodeDefinition) { const nodeData = node.data || {}; // 设置固定节点信息(从 configs 或 nodeDefinition 获取) setNodeName(nodeData.configs?.nodeName || nodeDefinition.nodeName); setDescription(nodeData.configs?.description || nodeDefinition.description || ''); // 设置输入映射默认值(转换 UUID 为显示名称) if (isConfigurableNode(nodeDefinition)) { const displayInputMapping = convertObjectToDisplayName( nodeData.inputMapping || {}, allNodes ); inputForm.reset(displayInputMapping); } } // 只在 visible 或 node.id 改变时重置表单 // allNodes 改变不应该触发重置(用户正在编辑时) // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, node?.id]); // ✅ 保存配置 const handleSave = () => { if (!node || !nodeDefinition) return; // 验证节点名称(必填) if (!nodeName || !nodeName.trim()) { toast({ title: '保存失败', description: '节点名称不能为空', variant: 'destructive' }); return; } // 使用 handleSubmit 验证输入映射(如果存在) const submitForm = async () => { setLoading(true); try { // 获取输入映射数据 let inputData = {}; if (isConfigurableNode(nodeDefinition) && nodeDefinition.inputMappingSchema) { inputData = inputForm.getValues(); } // 转换显示名称格式为 UUID 格式(仅输入映射需要) const uuidInputMapping = convertObjectToUUID(inputData as Record, allNodes); // 构建 configs(包含元数据) const configs = { nodeName: nodeName.trim(), nodeCode: nodeDefinition.nodeCode, // 只读,从定义获取 description: description.trim() }; // 构建更新数据 const updatedData: Partial = { label: nodeName.trim(), configs, inputMapping: uuidInputMapping, outputs: isConfigurableNode(nodeDefinition) ? nodeDefinition.outputs || [] : [] }; onOk(node.id, updatedData); toast({ title: '保存成功', description: '节点配置已更新' }); // 关闭抽屉 onCancel(); } catch (error) { console.error('保存节点配置失败:', error); toast({ title: '保存失败', description: '请检查必填字段是否填写完整', variant: 'destructive' }); } finally { setLoading(false); } }; // 如果有输入映射schema,使用表单验证 if (inputMappingSchema) { inputForm.handleSubmit(submitForm)(); } else { submitForm(); } }; // ✅ 重置表单 const handleReset = () => { if (!node || !nodeDefinition) return; // 重置节点信息 setNodeName(nodeDefinition.nodeName); setDescription(nodeDefinition.description || ''); // 重置输入映射 inputForm.reset({}); toast({ title: '已重置', description: '表单已恢复为默认值' }); }; // ✅ 通用 Select 渲染函数(减少代码重复) const renderSelect = useCallback(( options: Array<{ label: string; value: any }>, field: any, prop: any, disabled: boolean, onValueChange?: (value: string) => void ) => ( ), []); // ✅ 渲染字段控件 const renderFieldControl = useCallback((prop: any, field: any, key: string) => { // 动态数据源 if (prop['x-dataSource']) { const options = dataSourceCache[prop['x-dataSource']] || []; return renderSelect( options, field, prop, loadingDataSources || loading, (value) => { // 如果是数字类型,转换为数字 const numValue = Number(value); field.onChange(isNaN(numValue) ? value : numValue); } ); } // 静态枚举值 if (prop.enum && Array.isArray(prop.enum)) { const options = prop.enum.map((value: any, index: number) => { const enumValue = typeof value === 'object' ? value.value : value; const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value); return { label: enumLabel, value: enumValue }; }); return renderSelect(options, field, prop, loading); } // 根据类型渲染 switch (prop.type) { case 'string': { // 黑名单字段:不使用变量输入 const blacklist = ['nodeName', 'nodeCode', 'description']; const isBlacklisted = blacklist.includes(key); const isPassword = prop.format === 'password'; // 密码字段或黑名单字段:使用普通输入 if (isPassword || isBlacklisted) { if (prop.format === 'textarea' || key.includes('description')) { return (