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()}
+
+
+ 一键匹配结果汇总
+
+
+
+
+
+
+ {/* 核心统计 */}
+
+
+
+
+
+
{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);
+};