diff --git a/apps/desktop/src-tauri/src/business/services/material_matching_service.rs b/apps/desktop/src-tauri/src/business/services/material_matching_service.rs index a6c248f..e4a1d4d 100644 --- a/apps/desktop/src-tauri/src/business/services/material_matching_service.rs +++ b/apps/desktop/src-tauri/src/business/services/material_matching_service.rs @@ -12,7 +12,6 @@ use crate::data::repositories::{ material_repository::MaterialRepository, material_usage_repository::MaterialUsageRepository, video_classification_repository::VideoClassificationRepository, - template_matching_result_repository::TemplateMatchingResultRepository, }; use crate::business::services::template_service::TemplateService; use crate::business::services::template_matching_result_service::TemplateMatchingResultService; @@ -50,6 +49,60 @@ pub struct MaterialMatchingResult { pub failed_segments: Vec, } +/// 一键匹配请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchMatchingRequest { + pub project_id: String, + pub overwrite_existing: bool, + pub result_name_prefix: Option, // 结果名称前缀,默认为"一键匹配" +} + +/// 一键匹配结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchMatchingResult { + pub project_id: String, + pub total_bindings: u32, + pub successful_matches: u32, + pub failed_matches: u32, + pub skipped_bindings: u32, + pub matching_results: Vec, + pub total_duration_ms: u64, + pub summary: BatchMatchingSummary, +} + +/// 单个绑定的匹配结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchMatchingItemResult { + pub binding_id: String, + pub template_id: String, + pub template_name: String, + pub binding_name: Option, + pub status: BatchMatchingItemStatus, + pub matching_result: Option, + pub saved_result_id: Option, + pub error_message: Option, + pub duration_ms: u64, +} + +/// 单个绑定匹配状态 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum BatchMatchingItemStatus { + Success, + Failed, + Skipped, +} + +/// 一键匹配汇总信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchMatchingSummary { + pub total_segments_matched: u32, + pub total_materials_used: u32, + pub total_models_used: u32, + pub average_success_rate: f64, + pub best_matching_template: Option, + pub worst_matching_template: Option, +} + /// 片段匹配结果 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SegmentMatch { @@ -533,4 +586,171 @@ impl MaterialMatchingService { best_match } + + /// 执行一键匹配 - 遍历项目的所有活跃模板绑定并逐一匹配 + pub async fn batch_match_all_templates(&self, request: BatchMatchingRequest, database: Arc) -> Result { + let start_time = std::time::Instant::now(); + + // 获取项目的所有活跃模板绑定 + let active_bindings = self.get_active_project_bindings(&request.project_id, database).await?; + + if active_bindings.is_empty() { + return Ok(BatchMatchingResult { + project_id: request.project_id, + total_bindings: 0, + successful_matches: 0, + failed_matches: 0, + skipped_bindings: 0, + matching_results: Vec::new(), + total_duration_ms: start_time.elapsed().as_millis() as u64, + summary: BatchMatchingSummary { + total_segments_matched: 0, + total_materials_used: 0, + total_models_used: 0, + average_success_rate: 0.0, + best_matching_template: None, + worst_matching_template: None, + }, + }); + } + + let mut matching_results = Vec::new(); + let mut successful_matches = 0u32; + let mut failed_matches = 0u32; + let skipped_bindings = 0u32; + + // 逐一执行匹配 + for binding_detail in &active_bindings { + let binding_start_time = std::time::Instant::now(); + + let matching_request = MaterialMatchingRequest { + project_id: request.project_id.clone(), + template_id: binding_detail.binding.template_id.clone(), + binding_id: binding_detail.binding.id.clone(), + overwrite_existing: request.overwrite_existing, + }; + + let result_name = format!( + "{}-{}", + request.result_name_prefix.as_deref().unwrap_or("一键匹配"), + binding_detail.template_name + ); + + match self.match_materials_and_save(matching_request, result_name, None).await { + Ok((matching_result, saved_result)) => { + successful_matches += 1; + matching_results.push(BatchMatchingItemResult { + binding_id: binding_detail.binding.id.clone(), + template_id: binding_detail.binding.template_id.clone(), + template_name: binding_detail.template_name.clone(), + binding_name: binding_detail.binding.binding_name.clone(), + status: BatchMatchingItemStatus::Success, + matching_result: Some(matching_result), + saved_result_id: saved_result.map(|r| r.id), + error_message: None, + duration_ms: binding_start_time.elapsed().as_millis() as u64, + }); + } + Err(error) => { + failed_matches += 1; + matching_results.push(BatchMatchingItemResult { + binding_id: binding_detail.binding.id.clone(), + template_id: binding_detail.binding.template_id.clone(), + template_name: binding_detail.template_name.clone(), + binding_name: binding_detail.binding.binding_name.clone(), + status: BatchMatchingItemStatus::Failed, + matching_result: None, + saved_result_id: None, + error_message: Some(error.to_string()), + duration_ms: binding_start_time.elapsed().as_millis() as u64, + }); + } + } + } + + // 计算汇总信息 + let summary = self.calculate_batch_summary(&matching_results); + + Ok(BatchMatchingResult { + project_id: request.project_id, + total_bindings: active_bindings.len() as u32, + successful_matches, + failed_matches, + skipped_bindings, + matching_results, + total_duration_ms: start_time.elapsed().as_millis() as u64, + summary, + }) + } + + /// 获取项目的活跃模板绑定 + async fn get_active_project_bindings(&self, project_id: &str, database: Arc) -> Result> { + use crate::data::repositories::project_template_binding_repository::ProjectTemplateBindingRepository; + use crate::data::models::project_template_binding::BindingStatus; + + let binding_repo = ProjectTemplateBindingRepository::new(database); + + // 查询活跃的绑定 + let bindings = binding_repo.get_templates_by_project(project_id)?; + + // 过滤出活跃的绑定 + let active_bindings: Vec<_> = bindings.into_iter() + .filter(|detail| detail.binding.is_active && detail.binding.binding_status == BindingStatus::Active) + .collect(); + + Ok(active_bindings) + } + + /// 计算批量匹配汇总信息 + fn calculate_batch_summary(&self, results: &[BatchMatchingItemResult]) -> BatchMatchingSummary { + let mut total_segments_matched = 0u32; + let mut total_materials_used = 0u32; + let mut total_models_used = 0u32; + let mut success_rates = Vec::new(); + let mut best_template: Option<(String, f64)> = None; + let mut worst_template: Option<(String, f64)> = None; + + for result in results { + if let Some(matching_result) = &result.matching_result { + total_segments_matched += matching_result.statistics.matched_segments; + total_materials_used += matching_result.statistics.used_materials; + total_models_used += matching_result.statistics.used_models; + + let success_rate = matching_result.statistics.success_rate; + success_rates.push(success_rate); + + // 更新最佳和最差模板 + match &best_template { + None => best_template = Some((result.template_name.clone(), success_rate)), + Some((_, best_rate)) if success_rate > *best_rate => { + best_template = Some((result.template_name.clone(), success_rate)); + } + _ => {} + } + + match &worst_template { + None => worst_template = Some((result.template_name.clone(), success_rate)), + Some((_, worst_rate)) if success_rate < *worst_rate => { + worst_template = Some((result.template_name.clone(), success_rate)); + } + _ => {} + } + } + } + + let average_success_rate = if success_rates.is_empty() { + 0.0 + } else { + success_rates.iter().sum::() / success_rates.len() as f64 + }; + + BatchMatchingSummary { + total_segments_matched, + total_materials_used, + total_models_used, + average_success_rate, + best_matching_template: best_template.map(|(name, _)| name), + worst_matching_template: worst_template.map(|(name, _)| name), + } + } } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 92d3958..3eb3a84 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -194,6 +194,7 @@ pub fn run() { commands::material_matching_commands::execute_material_matching_with_save, commands::material_matching_commands::get_project_material_stats_for_matching, commands::material_matching_commands::validate_template_binding_for_matching, + commands::material_matching_commands::batch_match_all_templates, // 素材使用记录命令 commands::material_usage_commands::create_material_usage_record, commands::material_usage_commands::create_material_usage_records_batch, diff --git a/apps/desktop/src-tauri/src/presentation/commands/material_matching_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/material_matching_commands.rs index 9887fc9..3fb0ec6 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/material_matching_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/material_matching_commands.rs @@ -7,7 +7,8 @@ use tauri::{command, State}; use std::sync::Arc; use crate::business::services::material_matching_service::{ - MaterialMatchingService, MaterialMatchingRequest, MaterialMatchingResult + MaterialMatchingService, MaterialMatchingRequest, MaterialMatchingResult, + BatchMatchingRequest, BatchMatchingResult, }; use crate::business::services::template_service::TemplateService; use crate::business::services::template_matching_result_service::TemplateMatchingResultService; @@ -257,3 +258,45 @@ pub struct TemplateBindingMatchingValidation { pub total_segments: u32, pub matchable_segments: u32, } + +/// 一键匹配所有模板 +/// 遍历项目的所有活跃模板绑定并逐一执行匹配 +#[tauri::command] +pub async fn batch_match_all_templates( + request: BatchMatchingRequest, + state: State<'_, crate::app_state::AppState>, +) -> Result { + let database = state.get_database(); + + // 创建所需的仓储实例 + let material_repo = Arc::new( + MaterialRepository::new(database.clone()) + .map_err(|e| format!("创建素材仓储失败: {}", e))? + ); + + let template_service = Arc::new(TemplateService::new(database.clone())); + + let video_classification_repo = Arc::new( + VideoClassificationRepository::new(database.clone()) + ); + + // 创建匹配结果服务 + let material_usage_repo = Arc::new( + crate::data::repositories::material_usage_repository::MaterialUsageRepository::new(database.clone()) + ); + let matching_result_repo = Arc::new(TemplateMatchingResultRepository::new(database.clone())); + let matching_result_service = Arc::new(TemplateMatchingResultService::new(matching_result_repo)); + + let matching_service = MaterialMatchingService::new_with_result_service( + material_repo, + material_usage_repo, + template_service, + video_classification_repo, + matching_result_service, + ); + + // 执行一键匹配 + matching_service.batch_match_all_templates(request, database) + .await + .map_err(|e| e.to_string()) +} diff --git a/apps/desktop/src/components/BatchMatchingProgressDialog.tsx b/apps/desktop/src/components/BatchMatchingProgressDialog.tsx new file mode 100644 index 0000000..4ded778 --- /dev/null +++ b/apps/desktop/src/components/BatchMatchingProgressDialog.tsx @@ -0,0 +1,224 @@ +/** + * 一键匹配进度对话框组件 + * 遵循前端开发规范的组件设计原则 + */ + +import React from 'react'; +import { + X, + Loader2, + CheckCircle, + XCircle, + Clock, + Target, + AlertCircle, +} from 'lucide-react'; +import { + BatchMatchingProgress, + BatchMatchingProgressStatus, + getBatchMatchingProgressStatusDisplay, + formatDuration, +} from '../types/batchMatching'; + +interface BatchMatchingProgressDialogProps { + isOpen: boolean; + onClose: () => void; + onCancel?: () => void; + progress: BatchMatchingProgress | null; + canCancel?: boolean; +} + +export const BatchMatchingProgressDialog: React.FC = ({ + isOpen, + onClose, + onCancel, + progress, + canCancel = true, +}) => { + if (!isOpen || !progress) return null; + + const getStatusIcon = () => { + switch (progress.status) { + case BatchMatchingProgressStatus.InProgress: + return ; + case BatchMatchingProgressStatus.Completed: + return ; + case BatchMatchingProgressStatus.Failed: + return ; + case BatchMatchingProgressStatus.Cancelled: + return ; + default: + return ; + } + }; + + const getStatusColor = () => { + switch (progress.status) { + case BatchMatchingProgressStatus.InProgress: + return 'text-blue-600'; + case BatchMatchingProgressStatus.Completed: + return 'text-green-600'; + case BatchMatchingProgressStatus.Failed: + return 'text-red-600'; + case BatchMatchingProgressStatus.Cancelled: + return 'text-yellow-600'; + default: + return 'text-gray-600'; + } + }; + + const progressPercentage = progress.total_bindings > 0 + ? Math.round((progress.current_binding_index / progress.total_bindings) * 100) + : 0; + + const isInProgress = progress.status === BatchMatchingProgressStatus.InProgress; + const isCompleted = progress.status === BatchMatchingProgressStatus.Completed; + const isFailed = progress.status === BatchMatchingProgressStatus.Failed; + + return ( +
+
+ {/* 头部 */} +
+
+ {getStatusIcon()} +
+

+ {getBatchMatchingProgressStatusDisplay(progress.status)} +

+

+ 一键匹配进度 +

+
+
+ {(isCompleted || isFailed) && ( + + )} +
+ + {/* 内容 */} +
+ {/* 进度条 */} +
+
+ 总进度 + {progress.current_binding_index} / {progress.total_bindings} +
+
+
+
+
+ {progressPercentage}% +
+
+ + {/* 当前状态 */} + {isInProgress && progress.current_template_name && ( +
+
+ + 正在匹配 +
+

+ {progress.current_template_name} +

+
+ )} + + {/* 统计信息 */} +
+
+
+ + 已完成 +
+

{progress.completed_bindings}

+
+ +
+
+ + 失败 +
+

{progress.failed_bindings}

+
+
+ + {/* 时间信息 */} +
+
+
+ 已用时间: +

{formatDuration(progress.elapsed_time_ms)}

+
+ {progress.estimated_remaining_ms && isInProgress && ( +
+ 预计剩余: +

{formatDuration(progress.estimated_remaining_ms)}

+
+ )} +
+
+ + {/* 完成状态消息 */} + {isCompleted && ( +
+
+ + 匹配完成 +
+

+ 所有模板匹配已完成,点击关闭查看详细结果。 +

+
+ )} + + {/* 失败状态消息 */} + {isFailed && ( +
+
+ + 匹配失败 +
+

+ 匹配过程中发生错误,请检查项目配置后重试。 +

+
+ )} +
+ + {/* 底部 */} +
+ {isInProgress && canCancel && onCancel && ( + + )} + {(isCompleted || isFailed) && ( + + )} +
+
+
+ ); +}; diff --git a/apps/desktop/src/components/BatchMatchingResultDialog.tsx b/apps/desktop/src/components/BatchMatchingResultDialog.tsx new file mode 100644 index 0000000..e56e2c5 --- /dev/null +++ b/apps/desktop/src/components/BatchMatchingResultDialog.tsx @@ -0,0 +1,350 @@ +/** + * 一键匹配结果对话框组件 + * 遵循前端开发规范的组件设计原则 + */ + +import React from 'react'; +import { + X, + CheckCircle, + XCircle, + AlertCircle, + Clock, + Target, + Users, + Film, + TrendingUp, + TrendingDown, +} from 'lucide-react'; +import { + BatchMatchingResult, + BatchMatchingItemResult, + getBatchMatchingStatusDisplay, + getBatchMatchingStatusColor, + formatDuration, + calculateSuccessRate, +} from '../types/batchMatching'; +import { BatchMatchingSummaryCard } from './BatchMatchingSummaryCard'; + +interface BatchMatchingResultDialogProps { + isOpen: boolean; + onClose: () => void; + result: BatchMatchingResult | null; + loading?: boolean; +} + +export const BatchMatchingResultDialog: React.FC = ({ + isOpen, + onClose, + result, + loading = false, +}) => { + if (!isOpen) return null; + + // 导出匹配报告 + const exportMatchingReport = (result: BatchMatchingResult) => { + const report = generateDetailedReport(result); + const blob = new Blob([report], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `一键匹配报告_${new Date().toISOString().split('T')[0]}.txt`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + // 复制匹配摘要 + const copyMatchingSummary = async (result: BatchMatchingResult) => { + const summary = generateSummaryText(result); + try { + await navigator.clipboard.writeText(summary); + alert('摘要已复制到剪贴板'); + } catch (error) { + console.error('复制失败:', error); + alert('复制失败,请手动复制'); + } + }; + + // 生成详细报告 + const generateDetailedReport = (result: BatchMatchingResult): string => { + const lines = [ + '一键匹配详细报告', + '='.repeat(50), + '', + `项目ID: ${result.project_id}`, + `执行时间: ${new Date().toLocaleString()}`, + `总耗时: ${formatDuration(result.total_duration_ms)}`, + '', + '匹配统计:', + `---------`, + `总模板绑定数: ${result.total_bindings}`, + `成功匹配数: ${result.successful_matches}`, + `失败匹配数: ${result.failed_matches}`, + `跳过匹配数: ${result.skipped_bindings}`, + `成功率: ${calculateSuccessRate(result.successful_matches, result.total_bindings)}%`, + '', + '汇总信息:', + `---------`, + `匹配片段总数: ${result.summary.total_segments_matched}`, + `使用素材总数: ${result.summary.total_materials_used}`, + `使用模特总数: ${result.summary.total_models_used}`, + `平均成功率: ${(result.summary.average_success_rate * 100).toFixed(1)}%`, + ]; + + if (result.summary.best_matching_template) { + lines.push(`最佳匹配模板: ${result.summary.best_matching_template}`); + } + if (result.summary.worst_matching_template) { + lines.push(`最差匹配模板: ${result.summary.worst_matching_template}`); + } + + lines.push('', '详细结果:', '---------'); + + result.matching_results.forEach((item, index) => { + lines.push(`${index + 1}. ${item.template_name}`); + lines.push(` 状态: ${getBatchMatchingStatusDisplay(item.status)}`); + lines.push(` 耗时: ${formatDuration(item.duration_ms)}`); + if (item.binding_name) { + lines.push(` 绑定名称: ${item.binding_name}`); + } + if (item.error_message) { + lines.push(` 错误信息: ${item.error_message}`); + } + if (item.matching_result) { + lines.push(` 成功率: ${(item.matching_result.statistics?.success_rate * 100 || 0).toFixed(1)}%`); + } + lines.push(''); + }); + + return lines.join('\n'); + }; + + // 生成摘要文本 + const generateSummaryText = (result: BatchMatchingResult): string => { + const successRate = calculateSuccessRate(result.successful_matches, result.total_bindings); + return `一键匹配完成!总计 ${result.total_bindings} 个模板绑定,成功 ${result.successful_matches} 个,失败 ${result.failed_matches} 个,成功率 ${successRate}%。耗时 ${formatDuration(result.total_duration_ms)},匹配片段 ${result.summary.total_segments_matched} 个,使用素材 ${result.summary.total_materials_used} 个。`; + }; + + const getOverallStatusIcon = () => { + if (!result) return null; + + if (result.failed_matches === 0 && result.successful_matches > 0) { + return ; + } else if (result.successful_matches > 0 && result.failed_matches > 0) { + return ; + } else { + return ; + } + }; + + const getOverallStatusText = () => { + if (!result) return ''; + + if (result.failed_matches === 0 && result.successful_matches > 0) { + return '一键匹配成功完成'; + } else if (result.successful_matches > 0 && result.failed_matches > 0) { + return '一键匹配部分成功'; + } else { + return '一键匹配失败'; + } + }; + + const successRate = result ? calculateSuccessRate(result.successful_matches, result.total_bindings) : 0; + + return ( +
+
+ {/* 头部 */} +
+
+ {getOverallStatusIcon()} +
+

+ {loading ? '正在执行一键匹配...' : getOverallStatusText()} +

+ {result && ( +

+ 总计 {result.total_bindings} 个模板绑定,耗时 {formatDuration(result.total_duration_ms)} +

+ )} +
+
+ +
+ + {/* 内容 */} +
+ {loading ? ( +
+
+ 正在匹配模板... +
+ ) : result ? ( +
+ {/* 汇总卡片 */} + + + {/* 统计概览 */} +
+
+
+ + 成功 +
+

{result.successful_matches}

+
+ +
+
+ + 失败 +
+

{result.failed_matches}

+
+ +
+
+ + 成功率 +
+

{successRate}%

+
+ +
+
+ + 总耗时 +
+

{formatDuration(result.total_duration_ms)}

+
+
+ + {/* 汇总信息 */} +
+

匹配汇总

+
+
+ + 匹配片段: + {result.summary.total_segments_matched} +
+
+ + 使用素材: + {result.summary.total_materials_used} +
+
+ + 使用模特: + {result.summary.total_models_used} +
+
+ + 平均成功率: + {(result.summary.average_success_rate * 100).toFixed(1)}% +
+
+ + {(result.summary.best_matching_template || result.summary.worst_matching_template) && ( +
+ {result.summary.best_matching_template && ( +
+ + 最佳匹配模板: + {result.summary.best_matching_template} +
+ )} + {result.summary.worst_matching_template && ( +
+ + 最差匹配模板: + {result.summary.worst_matching_template} +
+ )} +
+ )} +
+ + {/* 详细结果列表 */} +
+

详细结果

+
+ {result.matching_results.map((item: BatchMatchingItemResult) => ( +
+
+
+
+ {item.template_name} + + {getBatchMatchingStatusDisplay(item.status)} + +
+ {item.binding_name && ( +

绑定名称: {item.binding_name}

+ )} + {item.error_message && ( +

错误: {item.error_message}

+ )} +
+
+

耗时: {formatDuration(item.duration_ms)}

+ {item.matching_result && ( +

+ 成功率: {(item.matching_result.statistics?.success_rate * 100 || 0).toFixed(1)}% +

+ )} +
+
+
+ ))} +
+
+
+ ) : ( +
+

暂无匹配结果

+
+ )} +
+ + {/* 底部 */} +
+
+ {result && ( + <> + + + + )} +
+ +
+
+
+ ); +}; diff --git a/apps/desktop/src/components/BatchMatchingSummaryCard.tsx b/apps/desktop/src/components/BatchMatchingSummaryCard.tsx new file mode 100644 index 0000000..a10f0d3 --- /dev/null +++ b/apps/desktop/src/components/BatchMatchingSummaryCard.tsx @@ -0,0 +1,207 @@ +/** + * 一键匹配汇总卡片组件 + * 遵循前端开发规范的组件设计原则 + */ + +import React from 'react'; +import { + CheckCircle, + XCircle, + Target, + Clock, + Film, + Users, + TrendingUp, + TrendingDown, + BarChart3, +} from 'lucide-react'; +import { + BatchMatchingResult, + formatDuration, + calculateSuccessRate, +} from '../types/batchMatching'; + +interface BatchMatchingSummaryCardProps { + result: BatchMatchingResult; + className?: string; +} + +export const BatchMatchingSummaryCard: React.FC = ({ + result, + className = '', +}) => { + const successRate = calculateSuccessRate(result.successful_matches, result.total_bindings); + + const getOverallStatusColor = () => { + if (result.failed_matches === 0 && result.successful_matches > 0) { + return 'border-green-200 bg-green-50'; + } else if (result.successful_matches > 0 && result.failed_matches > 0) { + return 'border-yellow-200 bg-yellow-50'; + } else { + return 'border-red-200 bg-red-50'; + } + }; + + const getOverallStatusIcon = () => { + if (result.failed_matches === 0 && result.successful_matches > 0) { + return ; + } else if (result.successful_matches > 0 && result.failed_matches > 0) { + return ; + } else { + return ; + } + }; + + const getOverallStatusText = () => { + if (result.failed_matches === 0 && result.successful_matches > 0) { + return '全部成功'; + } else if (result.successful_matches > 0 && result.failed_matches > 0) { + return '部分成功'; + } else { + return '全部失败'; + } + }; + + return ( +
+ {/* 头部状态 */} +
+
+ {getOverallStatusIcon()} +
+

+ {getOverallStatusText()} +

+

+ 一键匹配结果汇总 +

+
+
+
+
{successRate}%
+
成功率
+
+
+ + {/* 核心统计 */} +
+
+
+ +
+
{result.successful_matches}
+
成功
+
+ +
+
+ +
+
{result.failed_matches}
+
失败
+
+ +
+
+ +
+
{result.total_bindings}
+
总数
+
+ +
+
+ +
+
+ {formatDuration(result.total_duration_ms)} +
+
耗时
+
+
+ + {/* 详细统计 */} +
+

匹配详情

+
+
+ + 匹配片段: + {result.summary.total_segments_matched} +
+ +
+ + 使用素材: + {result.summary.total_materials_used} +
+ +
+ + 使用模特: + {result.summary.total_models_used} +
+ +
+ + 平均成功率: + {(result.summary.average_success_rate * 100).toFixed(1)}% +
+ + {result.summary.best_matching_template && ( +
+ + 最佳模板: + + {result.summary.best_matching_template} + +
+ )} + + {result.summary.worst_matching_template && ( +
+ + 最差模板: + + {result.summary.worst_matching_template} + +
+ )} +
+
+ + {/* 建议和提示 */} + {result.failed_matches > 0 && ( +
+
+
+ +
+

优化建议

+

+ {result.failed_matches} 个模板匹配失败。建议检查模板配置、素材分类情况或增加更多符合条件的素材。 +

+
+
+
+
+ )} + + {result.successful_matches === result.total_bindings && result.total_bindings > 0 && ( +
+
+
+ +
+

匹配完美!

+

+ 所有模板都成功匹配,您的项目素材配置非常完善。 +

+
+
+
+
+ )} +
+ ); +}; diff --git a/apps/desktop/src/pages/ProjectDetails.tsx b/apps/desktop/src/pages/ProjectDetails.tsx index c44a1a6..914ab98 100644 --- a/apps/desktop/src/pages/ProjectDetails.tsx +++ b/apps/desktop/src/pages/ProjectDetails.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { ArrowLeft, FolderOpen, Upload, FileVideo, FileAudio, FileImage, HardDrive, Brain, Loader2, Link, Layers, Calendar, MapPin, Users, CheckCircle, Filter } from 'lucide-react'; +import { ArrowLeft, FolderOpen, Upload, FileVideo, FileAudio, FileImage, HardDrive, Brain, Loader2, Link, Layers, Calendar, MapPin, Users, CheckCircle, Filter, Shuffle } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; import { useProjectStore } from '../store/projectStore'; import { useMaterialStore } from '../store/materialStore'; @@ -29,6 +29,10 @@ import { import { MaterialMatchingService } from '../services/materialMatchingService'; import { MaterialMatchingResult, MaterialMatchingRequest } from '../types/materialMatching'; import { MaterialSegmentView } from '../components/MaterialSegmentView'; +import { BatchMatchingService } from '../services/batchMatchingService'; +import { BatchMatchingRequest, BatchMatchingResult, BatchMatchingProgress, BatchMatchingProgressStatus } from '../types/batchMatching'; +import { BatchMatchingResultDialog } from '../components/BatchMatchingResultDialog'; +import { BatchMatchingProgressDialog } from '../components/BatchMatchingProgressDialog'; import { formatDistanceToNow } from 'date-fns'; import { zhCN } from 'date-fns/locale'; import { MaterialSegmentStats } from '../components/MaterialSegmentStats'; @@ -129,6 +133,13 @@ export const ProjectDetails: React.FC = () => { const [segmentStats, setSegmentStats] = useState(null); const [currentMatchingBinding, setCurrentMatchingBinding] = useState(null); + // 一键匹配状态 + const [showBatchMatchingResultDialog, setShowBatchMatchingResultDialog] = useState(false); + const [batchMatchingResult, setBatchMatchingResult] = useState(null); + const [batchMatchingLoading, setBatchMatchingLoading] = useState(false); + const [showBatchMatchingProgressDialog, setShowBatchMatchingProgressDialog] = useState(false); + const [batchMatchingProgress, setBatchMatchingProgress] = useState(null); + // 素材筛选状态 const [materialClassificationFilter, setMaterialClassificationFilter] = useState('全部'); const [materialModelFilter, setMaterialModelFilter] = useState('全部'); @@ -515,6 +526,83 @@ export const ProjectDetails: React.FC = () => { setMatchingLoading(false); }; + // 一键匹配处理函数 + const handleBatchMatching = async () => { + if (!project) return; + + try { + setBatchMatchingLoading(true); + setShowBatchMatchingProgressDialog(true); + + // 设置进度回调 + BatchMatchingService.setProgressCallback((progress: BatchMatchingProgress) => { + setBatchMatchingProgress(progress); + + // 当匹配完成时,显示结果对话框 + if (progress.status === BatchMatchingProgressStatus.Completed) { + setTimeout(() => { + setShowBatchMatchingProgressDialog(false); + setShowBatchMatchingResultDialog(true); + }, 1000); // 延迟1秒显示结果 + } else if (progress.status === BatchMatchingProgressStatus.Failed) { + setTimeout(() => { + setShowBatchMatchingProgressDialog(false); + }, 2000); // 延迟2秒关闭进度对话框 + } + }); + + const request: BatchMatchingRequest = { + project_id: project.id, + overwrite_existing: false, + result_name_prefix: '一键匹配', + }; + + const result = await BatchMatchingService.executeBatchMatching(request); + setBatchMatchingResult(result); + + // 显示结果提示 + const overallStatus = BatchMatchingService.getOverallStatus(result); + if (overallStatus === 'success') { + addNotification('一键匹配成功', `成功匹配 ${result.successful_matches} 个模板,耗时 ${(result.total_duration_ms / 1000).toFixed(1)} 秒`); + } else if (overallStatus === 'partial') { + addNotification('一键匹配部分成功', `成功匹配 ${result.successful_matches} 个模板,失败 ${result.failed_matches} 个`); + } else { + addNotification('一键匹配失败', `所有模板匹配均失败,请检查项目配置`); + } + + // 如果当前在匹配记录选项卡,刷新数据 + if (activeTab === 'matching-results') { + window.location.reload(); + } + } catch (error) { + console.error('一键匹配失败:', error); + addNotification('一键匹配失败', `执行一键匹配时发生错误: ${error}`); + setShowBatchMatchingProgressDialog(false); + } finally { + setBatchMatchingLoading(false); + BatchMatchingService.clearProgressCallback(); + } + }; + + const handleCloseBatchMatchingDialog = () => { + setShowBatchMatchingResultDialog(false); + setBatchMatchingResult(null); + }; + + const handleCloseBatchMatchingProgressDialog = () => { + setShowBatchMatchingProgressDialog(false); + setBatchMatchingProgress(null); + }; + + const handleCancelBatchMatching = () => { + // 取消匹配逻辑(如果需要的话) + BatchMatchingService.clearProgressCallback(); + setShowBatchMatchingProgressDialog(false); + setBatchMatchingLoading(false); + setBatchMatchingProgress(null); + addNotification('一键匹配已取消', '用户取消了一键匹配操作'); + }; + // 素材编辑处理函数 const handleEditMaterial = (material: Material) => { setEditingMaterial(material); @@ -1248,6 +1336,20 @@ export const ProjectDetails: React.FC = () => { 管理项目与模板的绑定关系,设置主要模板和备用模板。

+
+ +
{bindingError && ( @@ -1420,6 +1522,23 @@ export const ProjectDetails: React.FC = () => { onApplyResult={handleApplyMatchingResult} onRetryMatching={handleRetryMatching} /> + + {/* 一键匹配结果对话框 */} + + + {/* 一键匹配进度对话框 */} + ); }; diff --git a/apps/desktop/src/services/batchMatchingService.ts b/apps/desktop/src/services/batchMatchingService.ts new file mode 100644 index 0000000..eb007e6 --- /dev/null +++ b/apps/desktop/src/services/batchMatchingService.ts @@ -0,0 +1,218 @@ +/** + * 一键匹配服务 + * 遵循前端开发规范的服务层设计原则 + */ + +import { invoke } from '@tauri-apps/api/core'; +import { + BatchMatchingRequest, + BatchMatchingResult, + BatchMatchingProgress, + BatchMatchingProgressStatus, +} from '../types/batchMatching'; + +export class BatchMatchingService { + // 进度回调函数类型 + static progressCallback: ((progress: BatchMatchingProgress) => void) | null = null; + + /** + * 设置进度回调函数 + */ + static setProgressCallback(callback: (progress: BatchMatchingProgress) => void) { + this.progressCallback = callback; + } + + /** + * 清除进度回调函数 + */ + static clearProgressCallback() { + this.progressCallback = null; + } + + /** + * 执行一键匹配 + * 遍历项目的所有活跃模板绑定并逐一执行匹配 + */ + static async executeBatchMatching(request: BatchMatchingRequest): Promise { + const startTime = Date.now(); + + try { + console.log('BatchMatchingService: 开始执行一键匹配', request); + + // 初始化进度 + if (this.progressCallback) { + this.progressCallback({ + status: BatchMatchingProgressStatus.InProgress, + current_binding_index: 0, + total_bindings: 0, // 将在后端返回实际数量 + completed_bindings: 0, + failed_bindings: 0, + elapsed_time_ms: 0, + }); + } + + const result = await invoke('batch_match_all_templates', { + request, + }); + + // 完成进度 + if (this.progressCallback) { + this.progressCallback({ + status: BatchMatchingProgressStatus.Completed, + current_binding_index: result.total_bindings, + total_bindings: result.total_bindings, + completed_bindings: result.successful_matches, + failed_bindings: result.failed_matches, + elapsed_time_ms: Date.now() - startTime, + }); + } + + console.log('BatchMatchingService: 一键匹配完成', result); + return result; + } catch (error) { + // 失败进度 + if (this.progressCallback) { + this.progressCallback({ + status: BatchMatchingProgressStatus.Failed, + current_binding_index: 0, + total_bindings: 0, + completed_bindings: 0, + failed_bindings: 0, + elapsed_time_ms: Date.now() - startTime, + }); + } + + console.error('BatchMatchingService: 一键匹配失败', error); + throw new Error(`一键匹配失败: ${error}`); + } + } + + /** + * 验证项目是否可以执行一键匹配 + */ + static async validateProjectForBatchMatching(_projectId: string): Promise<{ + canMatch: boolean; + activeBindingsCount: number; + issues: string[]; + }> { + try { + // 这里可以添加预检查逻辑,比如检查活跃绑定数量、素材数量等 + // 目前简化实现,直接返回可以匹配 + return { + canMatch: true, + activeBindingsCount: 0, // 实际应该从后端获取 + issues: [], + }; + } catch (error) { + console.error('BatchMatchingService: 验证项目匹配条件失败', error); + return { + canMatch: false, + activeBindingsCount: 0, + issues: [`验证失败: ${error}`], + }; + } + } + + /** + * 格式化匹配结果摘要 + */ + static formatResultSummary(result: BatchMatchingResult): string { + const { + total_bindings, + successful_matches, + failed_matches, + skipped_bindings, + summary, + } = result; + + const successRate = total_bindings > 0 + ? Math.round((successful_matches / total_bindings) * 100) + : 0; + + return `匹配完成!总计 ${total_bindings} 个模板绑定,成功 ${successful_matches} 个,失败 ${failed_matches} 个,跳过 ${skipped_bindings} 个。` + + `\n成功率: ${successRate}%` + + `\n匹配片段: ${summary.total_segments_matched} 个` + + `\n使用素材: ${summary.total_materials_used} 个` + + `\n使用模特: ${summary.total_models_used} 个` + + (summary.best_matching_template ? `\n最佳匹配模板: ${summary.best_matching_template}` : '') + + (summary.worst_matching_template ? `\n最差匹配模板: ${summary.worst_matching_template}` : ''); + } + + /** + * 检查匹配结果是否成功 + */ + static isMatchingSuccessful(result: BatchMatchingResult): boolean { + return result.successful_matches > 0 && result.failed_matches === 0; + } + + /** + * 检查匹配结果是否部分成功 + */ + static isMatchingPartiallySuccessful(result: BatchMatchingResult): boolean { + return result.successful_matches > 0 && result.failed_matches > 0; + } + + /** + * 获取匹配结果的整体状态 + */ + static getOverallStatus(result: BatchMatchingResult): 'success' | 'partial' | 'failed' { + if (this.isMatchingSuccessful(result)) { + return 'success'; + } else if (this.isMatchingPartiallySuccessful(result)) { + return 'partial'; + } else { + return 'failed'; + } + } + + /** + * 生成匹配结果报告 + */ + static generateReport(result: BatchMatchingResult): { + title: string; + summary: string; + details: Array<{ + templateName: string; + status: string; + duration: string; + error?: string; + }>; + } { + const overallStatus = this.getOverallStatus(result); + const title = overallStatus === 'success' + ? '一键匹配成功完成' + : overallStatus === 'partial' + ? '一键匹配部分成功' + : '一键匹配失败'; + + const summary = this.formatResultSummary(result); + + const details = result.matching_results.map(item => ({ + templateName: item.template_name, + status: item.status, + duration: this.formatDuration(item.duration_ms), + error: item.error_message, + })); + + return { + title, + summary, + details, + }; + } + + /** + * 格式化时长显示 + */ + private static formatDuration(durationMs: number): string { + if (durationMs < 1000) { + return `${durationMs}ms`; + } else if (durationMs < 60000) { + return `${(durationMs / 1000).toFixed(1)}s`; + } else { + const minutes = Math.floor(durationMs / 60000); + const seconds = Math.floor((durationMs % 60000) / 1000); + return `${minutes}m ${seconds}s`; + } + } +} diff --git a/apps/desktop/src/types/batchMatching.ts b/apps/desktop/src/types/batchMatching.ts new file mode 100644 index 0000000..bce6a30 --- /dev/null +++ b/apps/desktop/src/types/batchMatching.ts @@ -0,0 +1,139 @@ +/** + * 一键匹配相关的类型定义 + * 遵循前端开发规范的类型设计原则 + */ + +// 一键匹配请求 +export interface BatchMatchingRequest { + project_id: string; + overwrite_existing: boolean; + result_name_prefix?: string; // 结果名称前缀,默认为"一键匹配" +} + +// 一键匹配结果 +export interface BatchMatchingResult { + project_id: string; + total_bindings: number; + successful_matches: number; + failed_matches: number; + skipped_bindings: number; + matching_results: BatchMatchingItemResult[]; + total_duration_ms: number; + summary: BatchMatchingSummary; +} + +// 单个绑定的匹配结果 +export interface BatchMatchingItemResult { + binding_id: string; + template_id: string; + template_name: string; + binding_name?: string; + status: BatchMatchingItemStatus; + matching_result?: any; // MaterialMatchingResult + saved_result_id?: string; + error_message?: string; + duration_ms: number; +} + +// 单个绑定匹配状态 +export enum BatchMatchingItemStatus { + Success = 'Success', + Failed = 'Failed', + Skipped = 'Skipped', +} + +// 一键匹配汇总信息 +export interface BatchMatchingSummary { + total_segments_matched: number; + total_materials_used: number; + total_models_used: number; + average_success_rate: number; + best_matching_template?: string; + worst_matching_template?: string; +} + +// 一键匹配进度状态 +export enum BatchMatchingProgressStatus { + NotStarted = 'NotStarted', + InProgress = 'InProgress', + Completed = 'Completed', + Failed = 'Failed', + Cancelled = 'Cancelled', +} + +// 一键匹配进度信息 +export interface BatchMatchingProgress { + status: BatchMatchingProgressStatus; + current_binding_index: number; + total_bindings: number; + current_template_name?: string; + completed_bindings: number; + failed_bindings: number; + elapsed_time_ms: number; + estimated_remaining_ms?: number; +} + +// 获取状态显示文本的辅助函数 +export const getBatchMatchingStatusDisplay = (status: BatchMatchingItemStatus): string => { + switch (status) { + case BatchMatchingItemStatus.Success: + return '成功'; + case BatchMatchingItemStatus.Failed: + return '失败'; + case BatchMatchingItemStatus.Skipped: + return '跳过'; + default: + return '未知'; + } +}; + +// 获取状态颜色的辅助函数 +export const getBatchMatchingStatusColor = (status: BatchMatchingItemStatus): string => { + switch (status) { + case BatchMatchingItemStatus.Success: + return 'bg-green-100 text-green-800'; + case BatchMatchingItemStatus.Failed: + return 'bg-red-100 text-red-800'; + case BatchMatchingItemStatus.Skipped: + return 'bg-gray-100 text-gray-800'; + default: + return 'bg-gray-100 text-gray-800'; + } +}; + +// 获取进度状态显示文本的辅助函数 +export const getBatchMatchingProgressStatusDisplay = (status: BatchMatchingProgressStatus): string => { + switch (status) { + case BatchMatchingProgressStatus.NotStarted: + return '未开始'; + case BatchMatchingProgressStatus.InProgress: + return '进行中'; + case BatchMatchingProgressStatus.Completed: + return '已完成'; + case BatchMatchingProgressStatus.Failed: + return '失败'; + case BatchMatchingProgressStatus.Cancelled: + return '已取消'; + default: + return '未知'; + } +}; + +// 格式化时长显示 +export const formatDuration = (durationMs: number): string => { + if (durationMs < 1000) { + return `${durationMs}ms`; + } else if (durationMs < 60000) { + return `${(durationMs / 1000).toFixed(1)}s`; + } else { + const minutes = Math.floor(durationMs / 60000); + const seconds = Math.floor((durationMs % 60000) / 1000); + return `${minutes}m ${seconds}s`; + } +}; + +// 计算成功率百分比 +export const calculateSuccessRate = (successful: number, total: number): number => { + if (total === 0) return 0; + return Math.round((successful / total) * 100); +};