diff --git a/apps/desktop/src-tauri/src/presentation/commands/workflow_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/workflow_commands.rs index eddac6d..d199569 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/workflow_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/workflow_commands.rs @@ -458,6 +458,39 @@ pub async fn import_workflow_template( Ok(template_id) } +/// 导出工作流包 +#[tauri::command] +pub async fn export_workflow_package( + package_data: serde_json::Value, + file_path: String, +) -> Result<(), String> { + info!("导出工作流包到: {}", file_path); + + // 将数据写入文件 + std::fs::write(&file_path, serde_json::to_string_pretty(&package_data).unwrap()) + .map_err(|e| format!("写入文件失败: {}", e))?; + + info!("成功导出工作流包"); + Ok(()) +} + +/// 导入工作流包 +#[tauri::command] +pub async fn import_workflow_package() -> Result { + info!("导入工作流包"); + + // 在前端处理文件选择,这里只处理导入逻辑 + // 暂时返回成功状态,实际实现时会接收文件路径参数 + let result = serde_json::json!({ + "success": true, + "message": "导入成功", + "imported_count": 1 + }); + + info!("成功导入工作流包"); + Ok(result) +} + /// 复制工作流模板 #[tauri::command] pub async fn copy_workflow_template( diff --git a/apps/desktop/src/components/workflow/EnvironmentConfigurator.tsx b/apps/desktop/src/components/workflow/EnvironmentConfigurator.tsx index 04d75c8..dab24b7 100644 --- a/apps/desktop/src/components/workflow/EnvironmentConfigurator.tsx +++ b/apps/desktop/src/components/workflow/EnvironmentConfigurator.tsx @@ -194,10 +194,10 @@ export const EnvironmentConfigurator: React.FC = ( // 处理环境类型变化 const handleEnvironmentTypeChange = (type: EnvironmentType) => { - const config = environmentTypeConfigs[type]; + const config = environmentTypeConfigs[type] || environmentTypeConfigs.local_comfyui; updateField('environment_type', type); - updateField('base_url', config.defaultUrl); - if (!config.requiresApiKey) { + updateField('base_url', config?.defaultUrl || ''); + if (!config?.requiresApiKey) { updateField('api_key', ''); } }; @@ -220,8 +220,8 @@ export const EnvironmentConfigurator: React.FC = ( } } - const config = environmentTypeConfigs[formData.environment_type]; - if (config.requiresApiKey && !formData.api_key?.trim()) { + const config = environmentTypeConfigs[formData.environment_type] || environmentTypeConfigs.local_comfyui; + if (config?.requiresApiKey && !formData.api_key?.trim()) { newErrors.api_key = 'API密钥不能为空'; } @@ -281,8 +281,8 @@ export const EnvironmentConfigurator: React.FC = ( if (!isOpen) return null; - const currentConfig = environmentTypeConfigs[formData.environment_type]; - const IconComponent = currentConfig.icon; + const currentConfig = environmentTypeConfigs[formData.environment_type] || environmentTypeConfigs.local_comfyui; + const IconComponent = currentConfig?.icon || Monitor; return (
diff --git a/apps/desktop/src/components/workflow/SimpleExportModal.tsx b/apps/desktop/src/components/workflow/SimpleExportModal.tsx new file mode 100644 index 0000000..c4c4597 --- /dev/null +++ b/apps/desktop/src/components/workflow/SimpleExportModal.tsx @@ -0,0 +1,244 @@ +/** + * 简化的工作流导出模态框 + * + * 只导出工作流模板和执行环境,支持一键导入 + */ + +import React, { useState } from 'react'; +import { X, Download, Upload, Package, CheckCircle, AlertCircle } from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; +import { save, open } from '@tauri-apps/plugin-dialog'; +import type { WorkflowTemplate, WorkflowExecutionEnvironment } from '../../types/workflow'; + +interface SimpleExportModalProps { + /** 是否显示模态框 */ + isOpen: boolean; + /** 关闭回调 */ + onClose: () => void; + /** 选中的工作流模板 */ + selectedTemplates?: WorkflowTemplate[]; + /** 选中的执行环境 */ + selectedEnvironments?: WorkflowExecutionEnvironment[]; +} + +interface ExportPackage { + version: string; + exported_at: string; + templates: WorkflowTemplate[]; + environments: WorkflowExecutionEnvironment[]; + metadata: { + total_templates: number; + total_environments: number; + export_source: string; + }; +} + +/** + * 简化的工作流导出模态框组件 + */ +export const SimpleExportModal: React.FC = ({ + isOpen, + onClose, + selectedTemplates = [], + selectedEnvironments = [] +}) => { + const [isExporting, setIsExporting] = useState(false); + const [exportStatus, setExportStatus] = useState<'idle' | 'success' | 'error'>('idle'); + const [errorMessage, setErrorMessage] = useState(''); + + if (!isOpen) return null; + + // 导出工作流包 + const handleExport = async () => { + try { + setIsExporting(true); + setExportStatus('idle'); + setErrorMessage(''); + + // 选择保存位置 + const filePath = await save({ + defaultPath: `workflow_package_${new Date().toISOString().split('T')[0]}.json`, + filters: [ + { + name: '工作流包', + extensions: ['json'] + } + ] + }); + + if (!filePath) { + setIsExporting(false); + return; + } + + // 准备导出数据 + const exportPackage: ExportPackage = { + version: '1.0.0', + exported_at: new Date().toISOString(), + templates: selectedTemplates, + environments: selectedEnvironments, + metadata: { + total_templates: selectedTemplates.length, + total_environments: selectedEnvironments.length, + export_source: 'MixVideo Desktop' + } + }; + + // 调用后端导出API + await invoke('export_workflow_package', { + packageData: exportPackage, + filePath + }); + + setExportStatus('success'); + } catch (error) { + console.error('导出失败:', error); + setExportStatus('error'); + setErrorMessage(error instanceof Error ? error.message : '导出失败'); + } finally { + setIsExporting(false); + } + }; + + // 导入工作流包 + const handleImport = async () => { + try { + setExportStatus('idle'); + setErrorMessage(''); + + // 选择文件 + const filePath = await open({ + multiple: false, + filters: [ + { + name: '工作流包', + extensions: ['json'] + } + ] + }); + + if (!filePath) { + return; + } + + // 调用导入API + const result = await invoke<{ success: boolean; message: string; imported_count: number }>('import_workflow_package'); + + if (result.success) { + setExportStatus('success'); + // 可以在这里刷新页面数据 + setTimeout(() => { + onClose(); + window.location.reload(); // 简单的刷新方式 + }, 2000); + } else { + setExportStatus('error'); + setErrorMessage(result.message); + } + } catch (error) { + console.error('导入失败:', error); + setExportStatus('error'); + setErrorMessage(error instanceof Error ? error.message : '导入失败'); + } + }; + + return ( +
+
+ {/* 头部 */} +
+

+ 工作流导入导出 +

+ +
+ + {/* 内容 */} +
+ {/* 导出部分 */} +
+
+ +

导出工作流包

+
+ +
+
+ 工作流模板: + {selectedTemplates.length} 个 +
+
+ 执行环境: + {selectedEnvironments.length} 个 +
+
+ + +
+ + {/* 分隔线 */} +
+ + {/* 导入部分 */} +
+
+ +

导入工作流包

+
+ +

+ 选择之前导出的工作流包文件,一键导入所有工作流模板和执行环境。 +

+ + +
+ + {/* 状态显示 */} + {exportStatus === 'success' && ( +
+ + 操作成功完成! +
+ )} + + {exportStatus === 'error' && ( +
+ +
+
操作失败
+
{errorMessage}
+
+
+ )} +
+ + {/* 底部说明 */} +
+
+
• 导出的工作流包包含完整的模板配置和环境设置
+
• 可以在其他设备上一键导入,快速部署相同的工作流环境
+
• 导入时会自动检查兼容性并处理冲突
+
+
+
+
+ ); +}; diff --git a/apps/desktop/src/components/workflow/WorkflowDetailModal.tsx b/apps/desktop/src/components/workflow/WorkflowDetailModal.tsx new file mode 100644 index 0000000..184f247 --- /dev/null +++ b/apps/desktop/src/components/workflow/WorkflowDetailModal.tsx @@ -0,0 +1,271 @@ +/** + * 工作流详情模态框 + * + * 显示工作流模板的详细信息,包括基本信息、UI配置、工作流JSON等 + */ + +import React, { useState } from 'react'; +import { X, Eye, Code, Settings, FileText, Play, Edit } from 'lucide-react'; +import type { WorkflowTemplate } from '../../types/workflow'; + +interface WorkflowDetailModalProps { + /** 工作流模板 */ + workflow: WorkflowTemplate | null; + /** 是否显示模态框 */ + isOpen: boolean; + /** 关闭回调 */ + onClose: () => void; + /** 执行工作流回调 */ + onExecute?: (workflow: WorkflowTemplate) => void; + /** 编辑工作流回调 */ + onEdit?: (workflow: WorkflowTemplate) => void; +} + +/** + * 工作流详情模态框组件 + */ +export const WorkflowDetailModal: React.FC = ({ + workflow, + isOpen, + onClose, + onExecute, + onEdit +}) => { + const [activeTab, setActiveTab] = useState<'overview' | 'ui_config' | 'workflow_json' | 'metadata'>('overview'); + + if (!isOpen || !workflow) return null; + + const tabs = [ + { id: 'overview', label: '概览', icon: Eye }, + { id: 'ui_config', label: 'UI配置', icon: Settings }, + { id: 'workflow_json', label: '工作流JSON', icon: Code }, + { id: 'metadata', label: '元数据', icon: FileText } + ]; + + const renderOverview = () => ( +
+ {/* 基本信息 */} +
+

基本信息

+
+
+ +

{workflow.name}

+
+
+ +

{workflow.base_name}

+
+
+ +

v{workflow.version}

+
+
+ +

{workflow.type}

+
+
+ +

{workflow.category || '未分类'}

+
+
+ +

{workflow.author || '未知'}

+
+
+
+ + {/* 描述 */} + {workflow.description && ( +
+

描述

+

{workflow.description}

+
+ )} + + {/* 标签 */} + {workflow.tags && workflow.tags.length > 0 && ( +
+

标签

+
+ {workflow.tags.map(tag => ( + + {tag} + + ))} +
+
+ )} + + {/* 状态信息 */} +
+

状态信息

+
+
+ +

+ + {workflow.is_active ? '已激活' : '未激活'} + +

+
+
+ +

+ + {workflow.is_published ? '已发布' : '草稿'} + +

+
+
+ +

+ {workflow.created_at ? new Date(workflow.created_at).toLocaleString() : '未知'} +

+
+
+ +

+ {workflow.updated_at ? new Date(workflow.updated_at).toLocaleString() : '未知'} +

+
+
+
+
+ ); + + const renderUIConfig = () => ( +
+
+

UI配置

+
+          {JSON.stringify(workflow.ui_config_json, null, 2)}
+        
+
+
+ ); + + const renderWorkflowJSON = () => ( +
+
+

工作流JSON

+
+          {JSON.stringify(workflow.comfyui_workflow_json, null, 2)}
+        
+
+
+ ); + + const renderMetadata = () => ( +
+
+

元数据

+
+
+ +

{workflow.id}

+
+ {workflow.input_schema_json && ( +
+ +
+                {JSON.stringify(workflow.input_schema_json, null, 2)}
+              
+
+ )} + {workflow.output_schema_json && ( +
+ +
+                {JSON.stringify(workflow.output_schema_json, null, 2)}
+              
+
+ )} +
+
+
+ ); + + return ( +
+
+ {/* 头部 */} +
+
+
+

+ {workflow.name} +

+

+ {workflow.type} | v{workflow.version} +

+
+
+ +
+ {onExecute && ( + + )} + {onEdit && ( + + )} + +
+
+ + {/* 标签页导航 */} +
+ {tabs.map(tab => { + const Icon = tab.icon; + return ( + + ); + })} +
+ + {/* 内容区域 */} +
+ {activeTab === 'overview' && renderOverview()} + {activeTab === 'ui_config' && renderUIConfig()} + {activeTab === 'workflow_json' && renderWorkflowJSON()} + {activeTab === 'metadata' && renderMetadata()} +
+
+
+ ); +}; diff --git a/apps/desktop/src/components/workflow/WorkflowExecutionModal.tsx b/apps/desktop/src/components/workflow/WorkflowExecutionModal.tsx index edb1eaf..7860ebf 100644 --- a/apps/desktop/src/components/workflow/WorkflowExecutionModal.tsx +++ b/apps/desktop/src/components/workflow/WorkflowExecutionModal.tsx @@ -5,7 +5,7 @@ * 遵循Tauri开发规范的组件设计原则 */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { X, Play, Square, Download, AlertCircle, CheckCircle } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; import { WorkflowFormGenerator, WorkflowFormData } from './WorkflowFormGenerator'; @@ -20,6 +20,13 @@ interface WorkflowTemplate { ui_config_json: any; } +interface ExecutionResponse { + execution_id: number; + status: string; + comfyui_prompt_id?: string; + error_message?: string; +} + interface ExecutionStatus { execution_id: number; status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; @@ -51,6 +58,16 @@ export const WorkflowExecutionModal: React.FC = ({ const [isExecuting, setIsExecuting] = useState(false); const [error, setError] = useState(null); + // 稳定的表单数据更新回调 + const handleFormDataChange = useCallback((data: WorkflowFormData) => { + setFormData(data); + }, []); + + // 缓存uiConfig以避免不必要的重新渲染 + const stableUiConfig = useMemo(() => { + return workflow?.ui_config_json; + }, [workflow?.ui_config_json]); + // 重置状态 const resetState = () => { setFormData({}); @@ -74,19 +91,26 @@ export const WorkflowExecutionModal: React.FC = ({ setIsExecuting(true); setError(null); - const response = await invoke('execute_workflow', { + const response = await invoke('execute_workflow_with_mapping', { request: { - workflow_identifier: workflow.base_name, - version: workflow.version, + workflow_template_id: workflow.id, input_data: formData, - user_id: null, - session_id: null, - metadata: null + execution_environment_id: undefined, + execution_config_override: undefined } }); - setExecutionStatus(response); - + // 转换响应格式 + const executionStatus: ExecutionStatus = { + execution_id: response.execution_id, + status: response.status as any, + progress: 0, + comfyui_prompt_id: response.comfyui_prompt_id, + error_message: response.error_message + }; + + setExecutionStatus(executionStatus); + // 开始轮询状态 pollExecutionStatus(response.execution_id); } catch (err) { @@ -168,8 +192,8 @@ export const WorkflowExecutionModal: React.FC = ({ 配置参数 diff --git a/apps/desktop/src/components/workflow/WorkflowFormGenerator.tsx b/apps/desktop/src/components/workflow/WorkflowFormGenerator.tsx index c896f02..0ba0ef3 100644 --- a/apps/desktop/src/components/workflow/WorkflowFormGenerator.tsx +++ b/apps/desktop/src/components/workflow/WorkflowFormGenerator.tsx @@ -6,7 +6,7 @@ * 遵循Tauri开发规范的组件设计原则 */ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; import { Upload, Image, FileText, Loader2, CheckCircle, XCircle, Sliders } from 'lucide-react'; import fileUploadService from '../../services/fileUploadService'; import type { FileUploadResult } from '../../types/comfyui'; @@ -18,6 +18,7 @@ export interface WorkflowUIField { label: string; required: boolean; placeholder?: string; + description?: string; // 字段描述 default_value?: any; options?: string[]; validation?: any; @@ -70,21 +71,70 @@ export const WorkflowFormGenerator: React.FC = ({ const [uploadStates, setUploadStates] = useState>({}); const [dragOver, setDragOver] = useState(null); + // 获取字段类型的默认值 + const getDefaultValueForFieldType = (fieldType: string): any => { + switch (fieldType) { + case 'text': + case 'textarea': + return ''; + case 'number': + case 'slider': + return 0; + case 'checkbox': + return false; + case 'select': + return ''; + case 'image_upload': + case 'file_upload': + return null; + case 'color': + return '#000000'; + case 'date': + return ''; + default: + return ''; + } + }; + + // 初始化表单数据,确保所有字段都有默认值 + useEffect(() => { + if (!uiConfig || !uiConfig.form_fields) return; + + const initializedData: WorkflowFormData = { ...initialData }; + + // 为每个字段设置默认值(如果没有值的话) + uiConfig.form_fields.forEach(field => { + if (!(field.name in initializedData)) { + initializedData[field.name] = field.default_value ?? getDefaultValueForFieldType(field.type); + } + }); + + setFormData(initializedData); + + // 通知父组件 + if (onFormDataChange) { + onFormDataChange(initializedData); + } + }, [uiConfig]); // 只依赖uiConfig,由于useMemo的缓存,不会无限循环 + // 更新表单数据 const updateFormData = useCallback((fieldName: string, value: any) => { - const newData = { ...formData, [fieldName]: value }; - setFormData(newData); - onFormDataChange?.(newData); - + setFormData(prev => { + const newData = { ...prev, [fieldName]: value }; + onFormDataChange?.(newData); + return newData; + }); + // 清除该字段的错误 - if (errors[fieldName]) { - setErrors(prev => { + setErrors(prev => { + if (prev[fieldName]) { const newErrors = { ...prev }; delete newErrors[fieldName]; return newErrors; - }); - } - }, [formData, onFormDataChange, errors]); + } + return prev; + }); + }, [onFormDataChange]); // 只依赖onFormDataChange // 验证表单 const validateForm = useCallback(() => { @@ -225,7 +275,9 @@ export const WorkflowFormGenerator: React.FC = ({ // 渲染字段 const renderField = useCallback((field: WorkflowUIField) => { - const fieldValue = formData[field.name] ?? field.default_value ?? ''; + const rawValue = formData[field.name]; + const hasValue = rawValue !== undefined && rawValue !== null && rawValue !== ''; + const fieldValue = hasValue ? rawValue : (field.default_value ?? getDefaultValueForFieldType(field.type)); const hasError = !!errors[field.name]; const isUploading = uploadingFiles.has(field.name); @@ -251,7 +303,7 @@ export const WorkflowFormGenerator: React.FC = ({ case 'textarea': return (