fix: 修复EnvironmentType枚举编译错误
- 移除了EnvironmentType中未使用的ModalCloud、RunpodCloud和Custom变体 - 更新了相关的Display trait实现 - 修复了workflow_execution_environment_repository中的字符串解析逻辑 - 简化了universal_workflow_service中的match语句,只保留LocalComfyui支持 - 添加了批量处理相关的新文件和组件
This commit is contained in:
@@ -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<Mutex<HashMap<String, BatchProcessingTask>>>,
|
||||
/// 文件项映射
|
||||
file_items: Arc<Mutex<HashMap<String, Vec<BatchFileItem>>>>,
|
||||
/// 工作流服务
|
||||
workflow_service: Arc<UniversalWorkflowService>,
|
||||
}
|
||||
|
||||
impl BatchProcessingService {
|
||||
/// 创建新的批量处理服务
|
||||
pub fn new(workflow_service: Arc<UniversalWorkflowService>) -> 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<BatchProcessingTask> {
|
||||
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<Vec<BatchFileItem>> {
|
||||
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<BatchFileItem>,
|
||||
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<UniversalWorkflowService>,
|
||||
task_id: String,
|
||||
file_items: Vec<BatchFileItem>,
|
||||
active_tasks: Arc<Mutex<HashMap<String, BatchProcessingTask>>>,
|
||||
file_items_map: Arc<Mutex<HashMap<String, Vec<BatchFileItem>>>>,
|
||||
) -> 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<PathBuf> {
|
||||
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<BatchProcessingTask> {
|
||||
let tasks = self.active_tasks.lock().unwrap();
|
||||
tasks.get(task_id).cloned()
|
||||
}
|
||||
|
||||
/// 获取所有批量任务
|
||||
pub fn get_all_batch_tasks(&self) -> Vec<BatchProcessingTask> {
|
||||
let tasks = self.active_tasks.lock().unwrap();
|
||||
tasks.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// 获取批量处理进度
|
||||
pub fn get_batch_progress(&self, task_id: &str) -> Option<BatchProcessingProgress> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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. 更新执行结果
|
||||
|
||||
195
apps/desktop/src-tauri/src/data/models/batch_processing.rs
Normal file
195
apps/desktop/src-tauri/src/data/models/batch_processing.rs
Normal file
@@ -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<Utc>,
|
||||
pub started_at: Option<DateTime<Utc>>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
/// 批量处理文件项
|
||||
#[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<PathBuf>,
|
||||
pub status: BatchTaskStatus,
|
||||
pub execution_id: Option<i64>,
|
||||
pub error_message: Option<String>,
|
||||
pub processed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// 创建批量处理任务请求
|
||||
#[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<String>, // 支持的文件扩展名,如 ["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<String>,
|
||||
pub progress_percentage: f32,
|
||||
pub estimated_remaining_time: Option<u64>, // 秒
|
||||
}
|
||||
|
||||
/// 批量处理统计信息
|
||||
#[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<i64> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,7 @@ impl From<String> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 // 默认值
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BatchProcessingTask, String> {
|
||||
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<Option<BatchProcessingTask>, 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<Vec<BatchProcessingTask>, 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<Option<BatchProcessingProgress>, 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<String>,
|
||||
) -> Result<Option<String>, 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<String>,
|
||||
recursive: bool,
|
||||
) -> Result<Vec<String>, 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<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<AppState>) -> Result<Arc<BatchProcessingService>, 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<Vec<String>, 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<bool, String> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
689
apps/desktop/src/components/workflow/BatchFolderProcessor.tsx
Normal file
689
apps/desktop/src/components/workflow/BatchFolderProcessor.tsx
Normal file
@@ -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<BatchFolderProcessorProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onTaskComplete
|
||||
}) => {
|
||||
// 状态管理
|
||||
const [step, setStep] = useState<'config' | 'preview' | 'processing'>('config');
|
||||
const [inputFolder, setInputFolder] = useState('');
|
||||
const [outputFolder, setOutputFolder] = useState('');
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<WorkflowTemplate | null>(null);
|
||||
const [fileExtensions, setFileExtensions] = useState<string[]>(['jpg', 'png', 'jpeg']);
|
||||
const [recursive, setRecursive] = useState(true);
|
||||
const [taskName, setTaskName] = useState('');
|
||||
|
||||
// 预览状态
|
||||
const [previewFiles, setPreviewFiles] = useState<string[]>([]);
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
|
||||
// 处理状态
|
||||
const [currentTask, setCurrentTask] = useState<BatchProcessingTask | null>(null);
|
||||
const [progress, setProgress] = useState<BatchProcessingProgress | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
// 数据加载
|
||||
const [templates, setTemplates] = useState<WorkflowTemplate[]>([]);
|
||||
const [supportedExtensions, setSupportedExtensions] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadInitialData();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// 加载初始数据
|
||||
const loadInitialData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 加载工作流模板
|
||||
const templatesResult = await invoke<WorkflowTemplate[]>('get_workflow_templates');
|
||||
setTemplates(templatesResult);
|
||||
|
||||
// 加载支持的文件扩展名
|
||||
const extensionsResult = await invoke<string[]>('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<string | null>('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<string | null>('select_folder', {
|
||||
title: '选择输出文件夹'
|
||||
});
|
||||
if (result) {
|
||||
setOutputFolder(result);
|
||||
// 验证输出文件夹
|
||||
await invoke<boolean>('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<string[]>('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<BatchProcessingTask>('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<BatchProcessingProgress | null>('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<BatchProcessingTask | null>('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 <Clock className="w-4 h-4 text-gray-500" />;
|
||||
case 'processing':
|
||||
return <RotateCcw className="w-4 h-4 text-blue-500 animate-spin" />;
|
||||
case 'completed':
|
||||
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||||
case 'failed':
|
||||
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
||||
case 'cancelled':
|
||||
return <Square className="w-4 h-4 text-gray-500" />;
|
||||
default:
|
||||
return <Clock className="w-4 h-4 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
批量文件夹处理
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 步骤指示器 */}
|
||||
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`flex items-center space-x-2 ${step === 'config' ? 'text-blue-600' : step === 'preview' || step === 'processing' ? 'text-green-600' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${step === 'config' ? 'bg-blue-100' : step === 'preview' || step === 'processing' ? 'bg-green-100' : 'bg-gray-100'}`}>
|
||||
1
|
||||
</div>
|
||||
<span className="text-sm font-medium">配置</span>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-gray-300"></div>
|
||||
<div className={`flex items-center space-x-2 ${step === 'preview' ? 'text-blue-600' : step === 'processing' ? 'text-green-600' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${step === 'preview' ? 'bg-blue-100' : step === 'processing' ? 'bg-green-100' : 'bg-gray-100'}`}>
|
||||
2
|
||||
</div>
|
||||
<span className="text-sm font-medium">预览</span>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-gray-300"></div>
|
||||
<div className={`flex items-center space-x-2 ${step === 'processing' ? 'text-blue-600' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${step === 'processing' ? 'bg-blue-100' : 'bg-gray-100'}`}>
|
||||
3
|
||||
</div>
|
||||
<span className="text-sm font-medium">处理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertCircle className="w-4 h-4 text-red-500" />
|
||||
<span className="text-sm text-red-700">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 配置步骤 */}
|
||||
{step === 'config' && (
|
||||
<div className="space-y-6">
|
||||
{/* 任务名称 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
任务名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={taskName}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 工作流模板选择 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
工作流模板
|
||||
</label>
|
||||
<select
|
||||
value={selectedTemplate?.id || ''}
|
||||
onChange={(e) => {
|
||||
const template = templates.find(t => t.id === parseInt(e.target.value));
|
||||
setSelectedTemplate(template || null);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">选择工作流模板</option>
|
||||
{templates.map(template => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.name} v{template.version}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 文件夹选择 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
输入文件夹
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputFolder}
|
||||
readOnly
|
||||
placeholder="选择输入文件夹"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
<button
|
||||
onClick={selectInputFolder}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
<span>选择</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
输出文件夹
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={outputFolder}
|
||||
readOnly
|
||||
placeholder="选择输出文件夹"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
<button
|
||||
onClick={selectOutputFolder}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
<span>选择</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件类型和选项 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
文件类型
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{supportedExtensions.map(ext => (
|
||||
<label key={ext} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fileExtensions.includes(ext)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setFileExtensions([...fileExtensions, ext]);
|
||||
} else {
|
||||
setFileExtensions(fileExtensions.filter(e => e !== ext));
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">.{ext}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
扫描选项
|
||||
</label>
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={recursive}
|
||||
onChange={(e) => setRecursive(e.target.checked)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">包含子文件夹</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={scanFilesPreview}
|
||||
disabled={!inputFolder || !selectedTemplate || isScanning}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
{isScanning ? (
|
||||
<>
|
||||
<RotateCcw className="w-4 h-4 animate-spin" />
|
||||
<span>扫描中...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>扫描预览</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 预览步骤 */}
|
||||
{step === 'preview' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Image className="w-5 h-5 text-blue-600" />
|
||||
<span className="font-medium text-blue-900">扫描结果</span>
|
||||
</div>
|
||||
<p className="text-sm text-blue-700">
|
||||
在 <strong>{inputFolder}</strong> 中找到 <strong>{previewFiles.length}</strong> 个文件
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 文件列表 */}
|
||||
<div className="border border-gray-200 rounded-lg">
|
||||
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
|
||||
<h3 className="text-sm font-medium text-gray-900">文件列表</h3>
|
||||
</div>
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
{previewFiles.map((file, index) => (
|
||||
<div key={index} className="px-4 py-2 border-b border-gray-100 last:border-b-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Image className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-700 truncate">{file}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => setStep('config')}
|
||||
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
返回配置
|
||||
</button>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={startBatchProcessing}
|
||||
disabled={previewFiles.length === 0 || isProcessing}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
<span>开始处理</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 处理步骤 */}
|
||||
{step === 'processing' && currentTask && (
|
||||
<div className="space-y-6">
|
||||
{/* 任务信息 */}
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getStatusIcon(currentTask.status)}
|
||||
<span className="font-medium text-gray-900">{currentTask.name}</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
{currentTask.status === 'processing' ? '处理中' :
|
||||
currentTask.status === 'completed' ? '已完成' :
|
||||
currentTask.status === 'failed' ? '失败' : '已取消'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{progress && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm text-gray-600">
|
||||
<span>进度: {progress.processed_files} / {progress.total_files}</span>
|
||||
<span>{progress.progress_percentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress.progress_percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
{progress.failed_files > 0 && (
|
||||
<div className="text-sm text-red-600">
|
||||
失败: {progress.failed_files} 个文件
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-end space-x-3">
|
||||
{isProcessing ? (
|
||||
<button
|
||||
onClick={cancelTask}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
<span>取消任务</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user