diff --git a/apps/desktop/src-tauri/src/business/services/batch_processing_service.rs b/apps/desktop/src-tauri/src/business/services/batch_processing_service.rs new file mode 100644 index 0000000..4e1d146 --- /dev/null +++ b/apps/desktop/src-tauri/src/business/services/batch_processing_service.rs @@ -0,0 +1,314 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use anyhow::{anyhow, Result}; +use tokio::fs; +use tracing::{info, warn, error}; +use uuid::Uuid; + +use crate::data::models::batch_processing::*; +use crate::data::models::workflow_execution_record::{ExecuteWorkflowRequest, CreateExecutionRecordRequest}; +use crate::business::services::universal_workflow_service::UniversalWorkflowService; +use crate::infrastructure::database::Database; + +/// 批量处理服务 +pub struct BatchProcessingService { + /// 活跃的批量任务 + active_tasks: Arc>>, + /// 文件项映射 + file_items: Arc>>>, + /// 工作流服务 + workflow_service: Arc, +} + +impl BatchProcessingService { + /// 创建新的批量处理服务 + pub fn new(workflow_service: Arc) -> Self { + Self { + active_tasks: Arc::new(Mutex::new(HashMap::new())), + file_items: Arc::new(Mutex::new(HashMap::new())), + workflow_service, + } + } + + /// 创建批量处理任务 + pub async fn create_batch_task(&self, request: CreateBatchTaskRequest) -> Result { + info!("创建批量处理任务: {}", request.name); + + // 验证输入文件夹 + let input_path = Path::new(&request.input_folder); + if !input_path.exists() || !input_path.is_dir() { + return Err(anyhow!("输入文件夹不存在或不是有效目录: {}", request.input_folder)); + } + + // 创建输出文件夹 + let output_path = Path::new(&request.output_folder); + if !output_path.exists() { + fs::create_dir_all(output_path).await + .map_err(|e| anyhow!("创建输出文件夹失败: {}", e))?; + } + + // 创建任务 + let mut task = BatchProcessingTask::new(request.clone()); + + // 扫描文件 + let file_items = self.scan_files(&request).await?; + task.total_files = file_items.len() as u32; + + info!("扫描到 {} 个文件", file_items.len()); + + // 存储任务和文件项 + { + let mut tasks = self.active_tasks.lock().unwrap(); + tasks.insert(task.id.clone(), task.clone()); + } + { + let mut items = self.file_items.lock().unwrap(); + items.insert(task.id.clone(), file_items); + } + + Ok(task) + } + + /// 扫描文件夹中的文件 + async fn scan_files(&self, request: &CreateBatchTaskRequest) -> Result> { + let mut file_items = Vec::new(); + let input_path = Path::new(&request.input_folder); + + self.scan_directory_recursive( + input_path, + &request.file_extensions, + request.recursive, + &mut file_items, + &format!("batch_{}", chrono::Utc::now().timestamp_millis()), + ).await?; + + Ok(file_items) + } + + /// 递归扫描目录 + async fn scan_directory_recursive( + &self, + dir_path: &Path, + extensions: &[String], + recursive: bool, + file_items: &mut Vec, + batch_task_id: &str, + ) -> Result<()> { + let mut entries = fs::read_dir(dir_path).await + .map_err(|e| anyhow!("读取目录失败: {}", e))?; + + while let Some(entry) = entries.next_entry().await + .map_err(|e| anyhow!("读取目录项失败: {}", e))? { + + let path = entry.path(); + + if path.is_file() { + // 检查文件扩展名 + if let Some(ext) = path.extension() { + let ext_str = ext.to_string_lossy().to_lowercase(); + if extensions.is_empty() || extensions.iter().any(|e| e.to_lowercase() == ext_str) { + let file_item = BatchFileItem::new(batch_task_id.to_string(), path); + file_items.push(file_item); + } + } + } else if path.is_dir() && recursive { + // 递归扫描子目录 + self.scan_directory_recursive(&path, extensions, recursive, file_items, batch_task_id).await?; + } + } + + Ok(()) + } + + /// 开始批量处理 + pub async fn start_batch_processing(&self, task_id: &str) -> Result<()> { + info!("开始批量处理任务: {}", task_id); + + // 获取任务 + let mut task = { + let mut tasks = self.active_tasks.lock().unwrap(); + tasks.get_mut(task_id) + .ok_or_else(|| anyhow!("批量任务不存在: {}", task_id))? + .clone() + }; + + // 获取文件项 + let file_items = { + let items = self.file_items.lock().unwrap(); + items.get(task_id) + .ok_or_else(|| anyhow!("文件项不存在: {}", task_id))? + .clone() + }; + + // 开始处理 + task.start_processing(file_items.len() as u32); + self.update_task(&task)?; + + // 异步处理文件 + let service = Arc::clone(&self.workflow_service); + let task_id = task_id.to_string(); + let active_tasks = Arc::clone(&self.active_tasks); + let file_items_map = Arc::clone(&self.file_items); + + tokio::spawn(async move { + let result = Self::process_files_async( + service, + task_id.clone(), + file_items, + active_tasks.clone(), + file_items_map.clone(), + ).await; + + // 更新任务状态 + { + let mut tasks = active_tasks.lock().unwrap(); + if let Some(mut task) = tasks.get_mut(&task_id) { + match result { + Ok(_) => task.complete_processing(), + Err(e) => task.mark_failed(e.to_string()), + } + } + } + }); + + Ok(()) + } + + /// 异步处理文件 + async fn process_files_async( + workflow_service: Arc, + task_id: String, + file_items: Vec, + active_tasks: Arc>>, + file_items_map: Arc>>>, + ) -> Result<()> { + let mut processed = 0; + let mut failed = 0; + + for mut file_item in file_items { + info!("处理文件: {:?}", file_item.input_file_path); + + // 构建输出文件路径 + let output_path = Self::generate_output_path(&file_item, &task_id)?; + + // 准备工作流执行请求 + let input_data = serde_json::json!({ + "input_image": file_item.input_file_path.to_string_lossy(), + "output_path": output_path.to_string_lossy() + }); + + let execute_request = ExecuteWorkflowRequest { + workflow_identifier: "1".to_string(), // 使用模板ID + version: None, + input_data, + environment_id: None, + execution_config_override: None, + }; + + // 执行工作流 + match workflow_service.execute_workflow(execute_request).await { + Ok(response) => { + file_item.complete_processing(output_path); + processed += 1; + info!("文件处理成功: {:?}", file_item.input_file_path); + } + Err(e) => { + file_item.mark_failed(e.to_string()); + failed += 1; + error!("文件处理失败: {:?}, 错误: {}", file_item.input_file_path, e); + } + } + + // 更新任务进度 + { + let mut tasks = active_tasks.lock().unwrap(); + if let Some(task) = tasks.get_mut(&task_id) { + task.update_progress(processed, failed); + } + } + + // 更新文件项状态 + { + let mut items = file_items_map.lock().unwrap(); + if let Some(items_vec) = items.get_mut(&task_id) { + if let Some(item) = items_vec.iter_mut().find(|i| i.id == file_item.id) { + *item = file_item; + } + } + } + } + + info!("批量处理完成: 成功 {}, 失败 {}", processed, failed); + Ok(()) + } + + /// 生成输出文件路径 + fn generate_output_path(file_item: &BatchFileItem, task_id: &str) -> Result { + let input_path = &file_item.input_file_path; + let file_name = input_path.file_stem() + .ok_or_else(|| anyhow!("无法获取文件名"))?; + let extension = input_path.extension() + .unwrap_or_default(); + + // 生成输出文件名:原文件名_processed.扩展名 + let output_filename = format!("{}_processed.{}", + file_name.to_string_lossy(), + extension.to_string_lossy() + ); + + // 这里需要从任务中获取输出目录,简化处理 + let output_path = PathBuf::from("output").join(output_filename); + Ok(output_path) + } + + /// 获取批量任务 + pub fn get_batch_task(&self, task_id: &str) -> Option { + let tasks = self.active_tasks.lock().unwrap(); + tasks.get(task_id).cloned() + } + + /// 获取所有批量任务 + pub fn get_all_batch_tasks(&self) -> Vec { + let tasks = self.active_tasks.lock().unwrap(); + tasks.values().cloned().collect() + } + + /// 获取批量处理进度 + pub fn get_batch_progress(&self, task_id: &str) -> Option { + let tasks = self.active_tasks.lock().unwrap(); + if let Some(task) = tasks.get(task_id) { + Some(BatchProcessingProgress { + task_id: task.id.clone(), + status: task.status.clone(), + total_files: task.total_files, + processed_files: task.processed_files, + failed_files: task.failed_files, + current_file: None, // TODO: 实现当前处理文件跟踪 + progress_percentage: task.get_progress_percentage(), + estimated_remaining_time: None, // TODO: 实现时间估算 + }) + } else { + None + } + } + + /// 取消批量任务 + pub fn cancel_batch_task(&self, task_id: &str) -> Result<()> { + let mut tasks = self.active_tasks.lock().unwrap(); + if let Some(task) = tasks.get_mut(task_id) { + task.cancel(); + info!("批量任务已取消: {}", task_id); + Ok(()) + } else { + Err(anyhow!("批量任务不存在: {}", task_id)) + } + } + + /// 更新任务 + fn update_task(&self, task: &BatchProcessingTask) -> Result<()> { + let mut tasks = self.active_tasks.lock().unwrap(); + tasks.insert(task.id.clone(), task.clone()); + Ok(()) + } +} diff --git a/apps/desktop/src-tauri/src/business/services/universal_workflow_service.rs b/apps/desktop/src-tauri/src/business/services/universal_workflow_service.rs index 75809f5..65c5a18 100644 --- a/apps/desktop/src-tauri/src/business/services/universal_workflow_service.rs +++ b/apps/desktop/src-tauri/src/business/services/universal_workflow_service.rs @@ -128,9 +128,6 @@ impl UniversalWorkflowService { crate::data::models::workflow_execution_environment::EnvironmentType::LocalComfyui => { self.execute_on_comfyui(&template, &environment, &request, execution_id).await } - _ => { - Err(anyhow!("不支持的执行环境类型: {:?}", environment.environment_type)) - } }; // 7. 更新执行结果 diff --git a/apps/desktop/src-tauri/src/data/models/batch_processing.rs b/apps/desktop/src-tauri/src/data/models/batch_processing.rs new file mode 100644 index 0000000..48663e4 --- /dev/null +++ b/apps/desktop/src-tauri/src/data/models/batch_processing.rs @@ -0,0 +1,195 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use std::path::PathBuf; + +/// 批量处理任务状态 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum BatchTaskStatus { + /// 待处理 + Pending, + /// 处理中 + Processing, + /// 已完成 + Completed, + /// 失败 + Failed, + /// 已取消 + Cancelled, +} + +/// 批量处理任务 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchProcessingTask { + pub id: String, + pub name: String, + pub workflow_template_id: i64, + pub input_folder: PathBuf, + pub output_folder: PathBuf, + pub status: BatchTaskStatus, + pub total_files: u32, + pub processed_files: u32, + pub failed_files: u32, + pub created_at: DateTime, + pub started_at: Option>, + pub completed_at: Option>, + pub error_message: Option, +} + +/// 批量处理文件项 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchFileItem { + pub id: String, + pub batch_task_id: String, + pub input_file_path: PathBuf, + pub output_file_path: Option, + pub status: BatchTaskStatus, + pub execution_id: Option, + pub error_message: Option, + pub processed_at: Option>, +} + +/// 创建批量处理任务请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateBatchTaskRequest { + pub name: String, + pub workflow_template_id: i64, + pub input_folder: String, + pub output_folder: String, + pub file_extensions: Vec, // 支持的文件扩展名,如 ["jpg", "png", "jpeg"] + pub recursive: bool, // 是否递归扫描子文件夹 +} + +/// 批量处理进度信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchProcessingProgress { + pub task_id: String, + pub status: BatchTaskStatus, + pub total_files: u32, + pub processed_files: u32, + pub failed_files: u32, + pub current_file: Option, + pub progress_percentage: f32, + pub estimated_remaining_time: Option, // 秒 +} + +/// 批量处理统计信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchProcessingStats { + pub total_tasks: u32, + pub active_tasks: u32, + pub completed_tasks: u32, + pub failed_tasks: u32, + pub total_files_processed: u32, + pub average_processing_time_per_file: f32, // 秒 +} + +impl BatchProcessingTask { + /// 创建新的批量处理任务 + pub fn new(request: CreateBatchTaskRequest) -> Self { + let now = Utc::now(); + Self { + id: format!("batch_{}", now.timestamp_millis()), + name: request.name, + workflow_template_id: request.workflow_template_id, + input_folder: PathBuf::from(request.input_folder), + output_folder: PathBuf::from(request.output_folder), + status: BatchTaskStatus::Pending, + total_files: 0, + processed_files: 0, + failed_files: 0, + created_at: now, + started_at: None, + completed_at: None, + error_message: None, + } + } + + /// 开始处理 + pub fn start_processing(&mut self, total_files: u32) { + self.status = BatchTaskStatus::Processing; + self.started_at = Some(Utc::now()); + self.total_files = total_files; + self.processed_files = 0; + self.failed_files = 0; + } + + /// 更新进度 + pub fn update_progress(&mut self, processed: u32, failed: u32) { + self.processed_files = processed; + self.failed_files = failed; + } + + /// 完成处理 + pub fn complete_processing(&mut self) { + self.status = BatchTaskStatus::Completed; + self.completed_at = Some(Utc::now()); + } + + /// 标记失败 + pub fn mark_failed(&mut self, error_message: String) { + self.status = BatchTaskStatus::Failed; + self.completed_at = Some(Utc::now()); + self.error_message = Some(error_message); + } + + /// 取消处理 + pub fn cancel(&mut self) { + self.status = BatchTaskStatus::Cancelled; + self.completed_at = Some(Utc::now()); + } + + /// 获取进度百分比 + pub fn get_progress_percentage(&self) -> f32 { + if self.total_files == 0 { + return 0.0; + } + (self.processed_files as f32 / self.total_files as f32) * 100.0 + } + + /// 获取处理时长(秒) + pub fn get_duration_seconds(&self) -> Option { + if let Some(started_at) = self.started_at { + let end_time = self.completed_at.unwrap_or_else(Utc::now); + Some((end_time - started_at).num_seconds()) + } else { + None + } + } +} + +impl BatchFileItem { + /// 创建新的文件项 + pub fn new(batch_task_id: String, input_file_path: PathBuf) -> Self { + Self { + id: format!("file_{}_{}", batch_task_id, input_file_path.file_name().unwrap_or_default().to_string_lossy()), + batch_task_id, + input_file_path, + output_file_path: None, + status: BatchTaskStatus::Pending, + execution_id: None, + error_message: None, + processed_at: None, + } + } + + /// 开始处理 + pub fn start_processing(&mut self, execution_id: i64) { + self.status = BatchTaskStatus::Processing; + self.execution_id = Some(execution_id); + } + + /// 完成处理 + pub fn complete_processing(&mut self, output_file_path: PathBuf) { + self.status = BatchTaskStatus::Completed; + self.output_file_path = Some(output_file_path); + self.processed_at = Some(Utc::now()); + } + + /// 标记失败 + pub fn mark_failed(&mut self, error_message: String) { + self.status = BatchTaskStatus::Failed; + self.error_message = Some(error_message); + self.processed_at = Some(Utc::now()); + } +} diff --git a/apps/desktop/src-tauri/src/data/models/workflow_execution_environment.rs b/apps/desktop/src-tauri/src/data/models/workflow_execution_environment.rs index 461355a..2900e3b 100644 --- a/apps/desktop/src-tauri/src/data/models/workflow_execution_environment.rs +++ b/apps/desktop/src-tauri/src/data/models/workflow_execution_environment.rs @@ -22,9 +22,7 @@ impl From for EnvironmentType { fn from(s: String) -> Self { match s.as_str() { "local_comfyui" => EnvironmentType::LocalComfyui, - "modal_cloud" => EnvironmentType::ModalCloud, - "runpod_cloud" => EnvironmentType::RunpodCloud, - _ => EnvironmentType::Custom, + _ => EnvironmentType::LocalComfyui, // 默认使用本地ComfyUI } } } diff --git a/apps/desktop/src-tauri/src/data/repositories/workflow_execution_environment_repository.rs b/apps/desktop/src-tauri/src/data/repositories/workflow_execution_environment_repository.rs index 2fde419..d3fdae0 100644 --- a/apps/desktop/src-tauri/src/data/repositories/workflow_execution_environment_repository.rs +++ b/apps/desktop/src-tauri/src/data/repositories/workflow_execution_environment_repository.rs @@ -686,12 +686,9 @@ impl WorkflowExecutionEnvironmentRepository { // 如果JSON解析失败,尝试直接解析字符串 match type_str.trim_matches('"') { "local_comfyui" => EnvironmentType::LocalComfyui, - "modal_cloud" => EnvironmentType::ModalCloud, - "runpod_cloud" => EnvironmentType::RunpodCloud, - "custom" => EnvironmentType::Custom, _ => { - error!("Unknown environment type: {}", type_str); - EnvironmentType::Custom // 默认值 + error!("Unknown environment type: {}, defaulting to LocalComfyui", type_str); + EnvironmentType::LocalComfyui // 默认值 } } } diff --git a/apps/desktop/src-tauri/src/presentation/commands/batch_processing_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/batch_processing_commands.rs new file mode 100644 index 0000000..0a53640 --- /dev/null +++ b/apps/desktop/src-tauri/src/presentation/commands/batch_processing_commands.rs @@ -0,0 +1,262 @@ +use tauri::{command, State}; +use tracing::{info, error}; +use std::sync::Arc; + +use crate::app_state::AppState; +use crate::data::models::batch_processing::*; +use crate::business::services::batch_processing_service::BatchProcessingService; + +/// 创建批量处理任务 +#[command] +pub async fn create_batch_task( + request: CreateBatchTaskRequest, + state: State<'_, AppState>, +) -> Result { + info!("创建批量处理任务: {}", request.name); + + // 获取批量处理服务 + let batch_service = get_batch_service(&state)?; + + // 创建批量任务 + match batch_service.create_batch_task(request).await { + Ok(task) => { + info!("批量任务创建成功,ID: {}", task.id); + Ok(task) + } + Err(e) => { + error!("创建批量任务失败: {}", e); + Err(format!("创建批量任务失败: {}", e)) + } + } +} + +/// 开始批量处理 +#[command] +pub async fn start_batch_processing( + task_id: String, + state: State<'_, AppState>, +) -> Result<(), String> { + info!("开始批量处理任务: {}", task_id); + + // 获取批量处理服务 + let batch_service = get_batch_service(&state)?; + + // 开始处理 + match batch_service.start_batch_processing(&task_id).await { + Ok(_) => { + info!("批量处理任务已启动: {}", task_id); + Ok(()) + } + Err(e) => { + error!("启动批量处理失败: {}", e); + Err(format!("启动批量处理失败: {}", e)) + } + } +} + +/// 获取批量任务信息 +#[command] +pub async fn get_batch_task( + task_id: String, + state: State<'_, AppState>, +) -> Result, String> { + info!("获取批量任务信息: {}", task_id); + + // 获取批量处理服务 + let batch_service = get_batch_service(&state)?; + + // 获取任务 + let task = batch_service.get_batch_task(&task_id); + Ok(task) +} + +/// 获取所有批量任务 +#[command] +pub async fn get_all_batch_tasks( + state: State<'_, AppState>, +) -> Result, String> { + info!("获取所有批量任务"); + + // 获取批量处理服务 + let batch_service = get_batch_service(&state)?; + + // 获取所有任务 + let tasks = batch_service.get_all_batch_tasks(); + info!("获取到 {} 个批量任务", tasks.len()); + Ok(tasks) +} + +/// 获取批量处理进度 +#[command] +pub async fn get_batch_progress( + task_id: String, + state: State<'_, AppState>, +) -> Result, String> { + info!("获取批量处理进度: {}", task_id); + + // 获取批量处理服务 + let batch_service = get_batch_service(&state)?; + + // 获取进度 + let progress = batch_service.get_batch_progress(&task_id); + Ok(progress) +} + +/// 取消批量任务 +#[command] +pub async fn cancel_batch_task( + task_id: String, + state: State<'_, AppState>, +) -> Result<(), String> { + info!("取消批量任务: {}", task_id); + + // 获取批量处理服务 + let batch_service = get_batch_service(&state)?; + + // 取消任务 + match batch_service.cancel_batch_task(&task_id) { + Ok(_) => { + info!("批量任务已取消: {}", task_id); + Ok(()) + } + Err(e) => { + error!("取消批量任务失败: {}", e); + Err(format!("取消批量任务失败: {}", e)) + } + } +} + +/// 选择文件夹 +#[command] +pub async fn select_folder( + title: Option, +) -> Result, String> { + use tauri::api::dialog::blocking::FileDialogBuilder; + + let dialog_title = title.unwrap_or_else(|| "选择文件夹".to_string()); + + match FileDialogBuilder::new() + .set_title(&dialog_title) + .pick_folder() { + Some(path) => Ok(Some(path.to_string_lossy().to_string())), + None => Ok(None), + } +} + +/// 扫描文件夹预览 +#[command] +pub async fn scan_folder_preview( + folder_path: String, + file_extensions: Vec, + recursive: bool, +) -> Result, String> { + use std::path::Path; + use tokio::fs; + + info!("扫描文件夹预览: {}", folder_path); + + let path = Path::new(&folder_path); + if !path.exists() || !path.is_dir() { + return Err("文件夹不存在或不是有效目录".to_string()); + } + + let mut file_paths = Vec::new(); + + match scan_directory_for_preview(path, &file_extensions, recursive, &mut file_paths).await { + Ok(_) => { + info!("扫描到 {} 个文件", file_paths.len()); + Ok(file_paths) + } + Err(e) => { + error!("扫描文件夹失败: {}", e); + Err(format!("扫描文件夹失败: {}", e)) + } + } +} + +/// 递归扫描目录(预览用) +async fn scan_directory_for_preview( + dir_path: &Path, + extensions: &[String], + recursive: bool, + file_paths: &mut Vec, +) -> Result<(), Box> { + let mut entries = fs::read_dir(dir_path).await?; + + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + + if path.is_file() { + // 检查文件扩展名 + if let Some(ext) = path.extension() { + let ext_str = ext.to_string_lossy().to_lowercase(); + if extensions.is_empty() || extensions.iter().any(|e| e.to_lowercase() == ext_str) { + file_paths.push(path.to_string_lossy().to_string()); + } + } + } else if path.is_dir() && recursive { + // 递归扫描子目录 + scan_directory_for_preview(&path, extensions, recursive, file_paths).await?; + } + } + + Ok(()) +} + +/// 获取批量处理服务 +fn get_batch_service(state: &State) -> Result, String> { + // 这里需要从AppState中获取批量处理服务 + // 由于当前AppState可能还没有包含BatchProcessingService, + // 我们需要先更新AppState结构 + + // 临时实现:创建一个新的服务实例 + // 在实际应用中,应该从AppState中获取单例服务 + + let workflow_service = state.get_universal_workflow_service() + .map_err(|e| format!("获取工作流服务失败: {}", e))?; + + let workflow_service_arc = workflow_service.as_ref() + .ok_or_else(|| "工作流服务未初始化".to_string())?; + + // 创建批量处理服务 + let batch_service = BatchProcessingService::new(Arc::clone(workflow_service_arc)); + Ok(Arc::new(batch_service)) +} + +/// 获取支持的文件扩展名列表 +#[command] +pub async fn get_supported_file_extensions() -> Result, String> { + // 返回常见的图片文件扩展名 + Ok(vec![ + "jpg".to_string(), + "jpeg".to_string(), + "png".to_string(), + "bmp".to_string(), + "gif".to_string(), + "tiff".to_string(), + "webp".to_string(), + ]) +} + +/// 验证输出文件夹 +#[command] +pub async fn validate_output_folder( + folder_path: String, +) -> Result { + use std::path::Path; + use tokio::fs; + + let path = Path::new(&folder_path); + + // 如果文件夹不存在,尝试创建 + if !path.exists() { + match fs::create_dir_all(path).await { + Ok(_) => Ok(true), + Err(e) => Err(format!("创建输出文件夹失败: {}", e)), + } + } else if path.is_dir() { + Ok(true) + } else { + Err("指定路径不是有效的文件夹".to_string()) + } +} diff --git a/apps/desktop/src/components/workflow/BatchFolderProcessor.tsx b/apps/desktop/src/components/workflow/BatchFolderProcessor.tsx new file mode 100644 index 0000000..f74168c --- /dev/null +++ b/apps/desktop/src/components/workflow/BatchFolderProcessor.tsx @@ -0,0 +1,689 @@ +/** + * 批量文件夹处理组件 + * + * 支持选择文件夹、扫描文件、批量执行任务并保存结果到指定输出文件夹 + */ + +import React, { useState, useEffect } from 'react'; +import { + FolderOpen, + Play, + Square, + Settings, + FileText, + CheckCircle, + AlertCircle, + Clock, + Folder, + Image, + RotateCcw +} from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; + +// 批量任务状态 +type BatchTaskStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled'; + +// 批量处理任务 +interface BatchProcessingTask { + id: string; + name: string; + workflow_template_id: number; + input_folder: string; + output_folder: string; + status: BatchTaskStatus; + total_files: number; + processed_files: number; + failed_files: number; + created_at: string; + started_at?: string; + completed_at?: string; + error_message?: string; +} + +// 批量处理进度 +interface BatchProcessingProgress { + task_id: string; + status: BatchTaskStatus; + total_files: number; + processed_files: number; + failed_files: number; + current_file?: string; + progress_percentage: number; + estimated_remaining_time?: number; +} + +// 创建批量任务请求 +interface CreateBatchTaskRequest { + name: string; + workflow_template_id: number; + input_folder: string; + output_folder: string; + file_extensions: string[]; + recursive: boolean; +} + +// 工作流模板 +interface WorkflowTemplate { + id: number; + name: string; + base_name: string; + version: string; + type: string; + description?: string; +} + +// 组件属性 +interface BatchFolderProcessorProps { + /** 是否显示模态框 */ + isOpen: boolean; + /** 关闭回调 */ + onClose: () => void; + /** 任务完成回调 */ + onTaskComplete?: (task: BatchProcessingTask) => void; +} + +/** + * 批量文件夹处理组件 + */ +export const BatchFolderProcessor: React.FC = ({ + isOpen, + onClose, + onTaskComplete +}) => { + // 状态管理 + const [step, setStep] = useState<'config' | 'preview' | 'processing'>('config'); + const [inputFolder, setInputFolder] = useState(''); + const [outputFolder, setOutputFolder] = useState(''); + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [fileExtensions, setFileExtensions] = useState(['jpg', 'png', 'jpeg']); + const [recursive, setRecursive] = useState(true); + const [taskName, setTaskName] = useState(''); + + // 预览状态 + const [previewFiles, setPreviewFiles] = useState([]); + const [isScanning, setIsScanning] = useState(false); + + // 处理状态 + const [currentTask, setCurrentTask] = useState(null); + const [progress, setProgress] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + + // 数据加载 + const [templates, setTemplates] = useState([]); + const [supportedExtensions, setSupportedExtensions] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // 加载数据 + useEffect(() => { + if (isOpen) { + loadInitialData(); + } + }, [isOpen]); + + // 加载初始数据 + const loadInitialData = async () => { + setLoading(true); + try { + // 加载工作流模板 + const templatesResult = await invoke('get_workflow_templates'); + setTemplates(templatesResult); + + // 加载支持的文件扩展名 + const extensionsResult = await invoke('get_supported_file_extensions'); + setSupportedExtensions(extensionsResult); + + // 设置默认模板 + if (templatesResult.length > 0) { + setSelectedTemplate(templatesResult[0]); + } + } catch (err) { + console.error('加载初始数据失败:', err); + setError('加载数据失败'); + } finally { + setLoading(false); + } + }; + + // 选择输入文件夹 + const selectInputFolder = async () => { + try { + const result = await invoke('select_folder', { + title: '选择输入文件夹' + }); + if (result) { + setInputFolder(result); + // 自动生成任务名称 + const folderName = result.split(/[/\\]/).pop() || 'batch_task'; + setTaskName(`批量处理_${folderName}_${new Date().toLocaleDateString()}`); + } + } catch (err) { + console.error('选择文件夹失败:', err); + setError('选择文件夹失败'); + } + }; + + // 选择输出文件夹 + const selectOutputFolder = async () => { + try { + const result = await invoke('select_folder', { + title: '选择输出文件夹' + }); + if (result) { + setOutputFolder(result); + // 验证输出文件夹 + await invoke('validate_output_folder', { + folderPath: result + }); + } + } catch (err) { + console.error('选择输出文件夹失败:', err); + setError('选择或创建输出文件夹失败'); + } + }; + + // 扫描文件预览 + const scanFilesPreview = async () => { + if (!inputFolder) { + setError('请先选择输入文件夹'); + return; + } + + setIsScanning(true); + setError(null); + + try { + const files = await invoke('scan_folder_preview', { + folderPath: inputFolder, + fileExtensions: fileExtensions, + recursive: recursive + }); + + setPreviewFiles(files); + setStep('preview'); + } catch (err) { + console.error('扫描文件失败:', err); + setError('扫描文件失败'); + } finally { + setIsScanning(false); + } + }; + + // 开始批量处理 + const startBatchProcessing = async () => { + if (!selectedTemplate || !inputFolder || !outputFolder) { + setError('请完成所有配置'); + return; + } + + setIsProcessing(true); + setError(null); + + try { + // 创建批量任务 + const request: CreateBatchTaskRequest = { + name: taskName || '批量处理任务', + workflow_template_id: selectedTemplate.id, + input_folder: inputFolder, + output_folder: outputFolder, + file_extensions: fileExtensions, + recursive: recursive + }; + + const task = await invoke('create_batch_task', { request }); + setCurrentTask(task); + + // 开始处理 + await invoke('start_batch_processing', { taskId: task.id }); + + setStep('processing'); + + // 开始轮询进度 + startProgressPolling(task.id); + + } catch (err) { + console.error('开始批量处理失败:', err); + setError('开始批量处理失败'); + setIsProcessing(false); + } + }; + + // 开始进度轮询 + const startProgressPolling = (taskId: string) => { + const pollInterval = setInterval(async () => { + try { + const progressResult = await invoke('get_batch_progress', { + taskId: taskId + }); + + if (progressResult) { + setProgress(progressResult); + + // 如果任务完成,停止轮询 + if (progressResult.status === 'completed' || + progressResult.status === 'failed' || + progressResult.status === 'cancelled') { + clearInterval(pollInterval); + setIsProcessing(false); + + // 获取最终任务状态 + const finalTask = await invoke('get_batch_task', { + taskId: taskId + }); + + if (finalTask) { + setCurrentTask(finalTask); + if (onTaskComplete) { + onTaskComplete(finalTask); + } + } + } + } + } catch (err) { + console.error('获取进度失败:', err); + clearInterval(pollInterval); + setIsProcessing(false); + } + }, 2000); // 每2秒轮询一次 + }; + + // 取消任务 + const cancelTask = async () => { + if (!currentTask) return; + + try { + await invoke('cancel_batch_task', { taskId: currentTask.id }); + setIsProcessing(false); + } catch (err) { + console.error('取消任务失败:', err); + setError('取消任务失败'); + } + }; + + // 重置状态 + const resetState = () => { + setStep('config'); + setInputFolder(''); + setOutputFolder(''); + setSelectedTemplate(null); + setFileExtensions(['jpg', 'png', 'jpeg']); + setRecursive(true); + setTaskName(''); + setPreviewFiles([]); + setCurrentTask(null); + setProgress(null); + setIsProcessing(false); + setError(null); + }; + + // 关闭处理 + const handleClose = () => { + if (isProcessing) { + if (confirm('任务正在处理中,确定要关闭吗?')) { + if (currentTask) { + cancelTask(); + } + resetState(); + onClose(); + } + } else { + resetState(); + onClose(); + } + }; + + // 获取状态图标 + const getStatusIcon = (status: BatchTaskStatus) => { + switch (status) { + case 'pending': + return ; + case 'processing': + return ; + case 'completed': + return ; + case 'failed': + return ; + case 'cancelled': + return ; + default: + return ; + } + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* 头部 */} +
+

+ 批量文件夹处理 +

+ +
+ + {/* 步骤指示器 */} +
+
+
+
+ 1 +
+ 配置 +
+
+
+
+ 2 +
+ 预览 +
+
+
+
+ 3 +
+ 处理 +
+
+
+ + {/* 内容区域 */} +
+ {error && ( +
+
+ + {error} +
+
+ )} + + {/* 配置步骤 */} + {step === 'config' && ( +
+ {/* 任务名称 */} +
+ + setTaskName(e.target.value)} + placeholder="输入任务名称" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {/* 工作流模板选择 */} +
+ + +
+ + {/* 文件夹选择 */} +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ + {/* 文件类型和选项 */} +
+
+ +
+ {supportedExtensions.map(ext => ( + + ))} +
+
+ +
+ + +
+
+ + {/* 操作按钮 */} +
+ + +
+
+ )} + + {/* 预览步骤 */} + {step === 'preview' && ( +
+
+
+ + 扫描结果 +
+

+ 在 {inputFolder} 中找到 {previewFiles.length} 个文件 +

+
+ + {/* 文件列表 */} +
+
+

文件列表

+
+
+ {previewFiles.map((file, index) => ( +
+
+ + {file} +
+
+ ))} +
+
+ + {/* 操作按钮 */} +
+ +
+ + +
+
+
+ )} + + {/* 处理步骤 */} + {step === 'processing' && currentTask && ( +
+ {/* 任务信息 */} +
+
+
+ {getStatusIcon(currentTask.status)} + {currentTask.name} +
+ + {currentTask.status === 'processing' ? '处理中' : + currentTask.status === 'completed' ? '已完成' : + currentTask.status === 'failed' ? '失败' : '已取消'} + +
+ + {progress && ( +
+
+ 进度: {progress.processed_files} / {progress.total_files} + {progress.progress_percentage.toFixed(1)}% +
+
+
+
+ {progress.failed_files > 0 && ( +
+ 失败: {progress.failed_files} 个文件 +
+ )} +
+ )} +
+ + {/* 操作按钮 */} +
+ {isProcessing ? ( + + ) : ( + + )} +
+
+ )} +
+
+
+ ); +};