feat: 实现一键匹配功能 (v0.1.26)
- 新增一键匹配后端服务,支持遍历项目模板绑定并逐一匹配 - 在项目详情页添加一键匹配按钮,支持批量匹配操作 - 实现批量匹配进度管理,包括实时进度跟踪和取消功能 - 添加一键匹配结果汇总,包含详细统计和报告导出功能 - 新增批量匹配相关组件:进度对话框、结果对话框、汇总卡片 - 遵循 promptx/tauri-desktop-app-expert 开发规范 - 支持错误处理、状态管理和用户体验优化
This commit is contained in:
@@ -12,7 +12,6 @@ use crate::data::repositories::{
|
|||||||
material_repository::MaterialRepository,
|
material_repository::MaterialRepository,
|
||||||
material_usage_repository::MaterialUsageRepository,
|
material_usage_repository::MaterialUsageRepository,
|
||||||
video_classification_repository::VideoClassificationRepository,
|
video_classification_repository::VideoClassificationRepository,
|
||||||
template_matching_result_repository::TemplateMatchingResultRepository,
|
|
||||||
};
|
};
|
||||||
use crate::business::services::template_service::TemplateService;
|
use crate::business::services::template_service::TemplateService;
|
||||||
use crate::business::services::template_matching_result_service::TemplateMatchingResultService;
|
use crate::business::services::template_matching_result_service::TemplateMatchingResultService;
|
||||||
@@ -50,6 +49,60 @@ pub struct MaterialMatchingResult {
|
|||||||
pub failed_segments: Vec<FailedSegmentMatch>,
|
pub failed_segments: Vec<FailedSegmentMatch>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 一键匹配请求
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BatchMatchingRequest {
|
||||||
|
pub project_id: String,
|
||||||
|
pub overwrite_existing: bool,
|
||||||
|
pub result_name_prefix: Option<String>, // 结果名称前缀,默认为"一键匹配"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一键匹配结果
|
||||||
|
#[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<BatchMatchingItemResult>,
|
||||||
|
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<String>,
|
||||||
|
pub status: BatchMatchingItemStatus,
|
||||||
|
pub matching_result: Option<MaterialMatchingResult>,
|
||||||
|
pub saved_result_id: Option<String>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
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<String>,
|
||||||
|
pub worst_matching_template: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// 片段匹配结果
|
/// 片段匹配结果
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SegmentMatch {
|
pub struct SegmentMatch {
|
||||||
@@ -533,4 +586,171 @@ impl MaterialMatchingService {
|
|||||||
|
|
||||||
best_match
|
best_match
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 执行一键匹配 - 遍历项目的所有活跃模板绑定并逐一匹配
|
||||||
|
pub async fn batch_match_all_templates(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> {
|
||||||
|
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<crate::infrastructure::database::Database>) -> Result<Vec<crate::data::models::project_template_binding::ProjectTemplateBindingDetail>> {
|
||||||
|
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::<f64>() / 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ pub fn run() {
|
|||||||
commands::material_matching_commands::execute_material_matching_with_save,
|
commands::material_matching_commands::execute_material_matching_with_save,
|
||||||
commands::material_matching_commands::get_project_material_stats_for_matching,
|
commands::material_matching_commands::get_project_material_stats_for_matching,
|
||||||
commands::material_matching_commands::validate_template_binding_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_record,
|
||||||
commands::material_usage_commands::create_material_usage_records_batch,
|
commands::material_usage_commands::create_material_usage_records_batch,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use tauri::{command, State};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::business::services::material_matching_service::{
|
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_service::TemplateService;
|
||||||
use crate::business::services::template_matching_result_service::TemplateMatchingResultService;
|
use crate::business::services::template_matching_result_service::TemplateMatchingResultService;
|
||||||
@@ -257,3 +258,45 @@ pub struct TemplateBindingMatchingValidation {
|
|||||||
pub total_segments: u32,
|
pub total_segments: u32,
|
||||||
pub matchable_segments: u32,
|
pub matchable_segments: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 一键匹配所有模板
|
||||||
|
/// 遍历项目的所有活跃模板绑定并逐一执行匹配
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn batch_match_all_templates(
|
||||||
|
request: BatchMatchingRequest,
|
||||||
|
state: State<'_, crate::app_state::AppState>,
|
||||||
|
) -> Result<BatchMatchingResult, String> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|||||||
224
apps/desktop/src/components/BatchMatchingProgressDialog.tsx
Normal file
224
apps/desktop/src/components/BatchMatchingProgressDialog.tsx
Normal file
@@ -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<BatchMatchingProgressDialogProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onCancel,
|
||||||
|
progress,
|
||||||
|
canCancel = true,
|
||||||
|
}) => {
|
||||||
|
if (!isOpen || !progress) return null;
|
||||||
|
|
||||||
|
const getStatusIcon = () => {
|
||||||
|
switch (progress.status) {
|
||||||
|
case BatchMatchingProgressStatus.InProgress:
|
||||||
|
return <Loader2 className="w-8 h-8 text-blue-500 animate-spin" />;
|
||||||
|
case BatchMatchingProgressStatus.Completed:
|
||||||
|
return <CheckCircle className="w-8 h-8 text-green-500" />;
|
||||||
|
case BatchMatchingProgressStatus.Failed:
|
||||||
|
return <XCircle className="w-8 h-8 text-red-500" />;
|
||||||
|
case BatchMatchingProgressStatus.Cancelled:
|
||||||
|
return <AlertCircle className="w-8 h-8 text-yellow-500" />;
|
||||||
|
default:
|
||||||
|
return <Clock className="w-8 h-8 text-gray-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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 max-w-md w-full mx-4">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{getStatusIcon()}
|
||||||
|
<div>
|
||||||
|
<h2 className={`text-xl font-semibold ${getStatusColor()}`}>
|
||||||
|
{getBatchMatchingProgressStatusDisplay(progress.status)}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
一键匹配进度
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(isCompleted || isFailed) && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-sm text-gray-600 mb-2">
|
||||||
|
<span>总进度</span>
|
||||||
|
<span>{progress.current_binding_index} / {progress.total_bindings}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full transition-all duration-300 ${
|
||||||
|
isCompleted ? 'bg-green-500' :
|
||||||
|
isFailed ? 'bg-red-500' :
|
||||||
|
'bg-blue-500'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${progressPercentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-sm text-gray-600 mt-1">
|
||||||
|
{progressPercentage}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 当前状态 */}
|
||||||
|
{isInProgress && progress.current_template_name && (
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Target className="w-5 h-5 text-blue-600" />
|
||||||
|
<span className="text-sm font-medium text-blue-800">正在匹配</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-blue-700 mt-1">
|
||||||
|
{progress.current_template_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-green-50 p-3 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||||
|
<span className="text-sm font-medium text-green-800">已完成</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-bold text-green-900">{progress.completed_bindings}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-red-50 p-3 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<XCircle className="w-4 h-4 text-red-600" />
|
||||||
|
<span className="text-sm font-medium text-red-800">失败</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-bold text-red-900">{progress.failed_bindings}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 时间信息 */}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-600">已用时间:</span>
|
||||||
|
<p className="font-medium">{formatDuration(progress.elapsed_time_ms)}</p>
|
||||||
|
</div>
|
||||||
|
{progress.estimated_remaining_ms && isInProgress && (
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-600">预计剩余:</span>
|
||||||
|
<p className="font-medium">{formatDuration(progress.estimated_remaining_ms)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 完成状态消息 */}
|
||||||
|
{isCompleted && (
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||||
|
<span className="text-sm font-medium text-green-800">匹配完成</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-green-700 mt-1">
|
||||||
|
所有模板匹配已完成,点击关闭查看详细结果。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 失败状态消息 */}
|
||||||
|
{isFailed && (
|
||||||
|
<div className="bg-red-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<XCircle className="w-5 h-5 text-red-600" />
|
||||||
|
<span className="text-sm font-medium text-red-800">匹配失败</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-red-700 mt-1">
|
||||||
|
匹配过程中发生错误,请检查项目配置后重试。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部 */}
|
||||||
|
<div className="flex justify-end p-6 border-t border-gray-200 space-x-3">
|
||||||
|
{isInProgress && canCancel && onCancel && (
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
取消匹配
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{(isCompleted || isFailed) && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
查看结果
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
350
apps/desktop/src/components/BatchMatchingResultDialog.tsx
Normal file
350
apps/desktop/src/components/BatchMatchingResultDialog.tsx
Normal file
@@ -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<BatchMatchingResultDialogProps> = ({
|
||||||
|
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 <CheckCircle className="w-8 h-8 text-green-500" />;
|
||||||
|
} else if (result.successful_matches > 0 && result.failed_matches > 0) {
|
||||||
|
return <AlertCircle className="w-8 h-8 text-yellow-500" />;
|
||||||
|
} else {
|
||||||
|
return <XCircle className="w-8 h-8 text-red-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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 max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{getOverallStatusIcon()}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
|
{loading ? '正在执行一键匹配...' : getOverallStatusText()}
|
||||||
|
</h2>
|
||||||
|
{result && (
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
总计 {result.total_bindings} 个模板绑定,耗时 {formatDuration(result.total_duration_ms)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
|
<span className="ml-3 text-lg text-gray-600">正在匹配模板...</span>
|
||||||
|
</div>
|
||||||
|
) : result ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 汇总卡片 */}
|
||||||
|
<BatchMatchingSummaryCard result={result} />
|
||||||
|
|
||||||
|
{/* 统计概览 */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||||
|
<span className="text-sm font-medium text-green-800">成功</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-green-900">{result.successful_matches}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-red-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<XCircle className="w-5 h-5 text-red-600" />
|
||||||
|
<span className="text-sm font-medium text-red-800">失败</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-red-900">{result.failed_matches}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Target className="w-5 h-5 text-blue-600" />
|
||||||
|
<span className="text-sm font-medium text-blue-800">成功率</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-blue-900">{successRate}%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-purple-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Clock className="w-5 h-5 text-purple-600" />
|
||||||
|
<span className="text-sm font-medium text-purple-800">总耗时</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-bold text-purple-900">{formatDuration(result.total_duration_ms)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 汇总信息 */}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-3">匹配汇总</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Film className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-gray-600">匹配片段:</span>
|
||||||
|
<span className="font-medium">{result.summary.total_segments_matched}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Target className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-gray-600">使用素材:</span>
|
||||||
|
<span className="font-medium">{result.summary.total_materials_used}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Users className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-gray-600">使用模特:</span>
|
||||||
|
<span className="font-medium">{result.summary.total_models_used}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<TrendingUp className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-gray-600">平均成功率:</span>
|
||||||
|
<span className="font-medium">{(result.summary.average_success_rate * 100).toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(result.summary.best_matching_template || result.summary.worst_matching_template) && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||||
|
{result.summary.best_matching_template && (
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
<TrendingUp className="w-4 h-4 text-green-600" />
|
||||||
|
<span className="text-gray-600">最佳匹配模板:</span>
|
||||||
|
<span className="font-medium text-green-800">{result.summary.best_matching_template}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result.summary.worst_matching_template && (
|
||||||
|
<div className="flex items-center space-x-2 text-sm mt-1">
|
||||||
|
<TrendingDown className="w-4 h-4 text-red-600" />
|
||||||
|
<span className="text-gray-600">最差匹配模板:</span>
|
||||||
|
<span className="font-medium text-red-800">{result.summary.worst_matching_template}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 详细结果列表 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-3">详细结果</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{result.matching_results.map((item: BatchMatchingItemResult) => (
|
||||||
|
<div
|
||||||
|
key={item.binding_id}
|
||||||
|
className="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span className="font-medium text-gray-900">{item.template_name}</span>
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getBatchMatchingStatusColor(item.status)}`}>
|
||||||
|
{getBatchMatchingStatusDisplay(item.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.binding_name && (
|
||||||
|
<p className="text-sm text-gray-600 mt-1">绑定名称: {item.binding_name}</p>
|
||||||
|
)}
|
||||||
|
{item.error_message && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">错误: {item.error_message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm text-gray-600">耗时: {formatDuration(item.duration_ms)}</p>
|
||||||
|
{item.matching_result && (
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
成功率: {(item.matching_result.statistics?.success_rate * 100 || 0).toFixed(1)}%
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-500">暂无匹配结果</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部 */}
|
||||||
|
<div className="flex justify-between p-6 border-t border-gray-200">
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
{result && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => exportMatchingReport(result)}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
导出报告
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => copyMatchingSummary(result)}
|
||||||
|
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
|
||||||
|
>
|
||||||
|
复制摘要
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
207
apps/desktop/src/components/BatchMatchingSummaryCard.tsx
Normal file
207
apps/desktop/src/components/BatchMatchingSummaryCard.tsx
Normal file
@@ -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<BatchMatchingSummaryCardProps> = ({
|
||||||
|
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 <CheckCircle className="w-6 h-6 text-green-600" />;
|
||||||
|
} else if (result.successful_matches > 0 && result.failed_matches > 0) {
|
||||||
|
return <Target className="w-6 h-6 text-yellow-600" />;
|
||||||
|
} else {
|
||||||
|
return <XCircle className="w-6 h-6 text-red-600" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={`border-2 rounded-lg p-6 ${getOverallStatusColor()} ${className}`}>
|
||||||
|
{/* 头部状态 */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{getOverallStatusIcon()}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">
|
||||||
|
{getOverallStatusText()}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
一键匹配结果汇总
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-2xl font-bold text-gray-900">{successRate}%</div>
|
||||||
|
<div className="text-sm text-gray-600">成功率</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 核心统计 */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-green-900">{result.successful_matches}</div>
|
||||||
|
<div className="text-sm text-gray-600">成功</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
<XCircle className="w-5 h-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-red-900">{result.failed_matches}</div>
|
||||||
|
<div className="text-sm text-gray-600">失败</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
<BarChart3 className="w-5 h-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-blue-900">{result.total_bindings}</div>
|
||||||
|
<div className="text-sm text-gray-600">总数</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
<Clock className="w-5 h-5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold text-purple-900">
|
||||||
|
{formatDuration(result.total_duration_ms)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">耗时</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 详细统计 */}
|
||||||
|
<div className="border-t border-gray-200 pt-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 mb-3">匹配详情</h4>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Film className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-gray-600">匹配片段:</span>
|
||||||
|
<span className="font-medium">{result.summary.total_segments_matched}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Target className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-gray-600">使用素材:</span>
|
||||||
|
<span className="font-medium">{result.summary.total_materials_used}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Users className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-gray-600">使用模特:</span>
|
||||||
|
<span className="font-medium">{result.summary.total_models_used}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<TrendingUp className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-gray-600">平均成功率:</span>
|
||||||
|
<span className="font-medium">{(result.summary.average_success_rate * 100).toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result.summary.best_matching_template && (
|
||||||
|
<div className="flex items-center space-x-2 col-span-2">
|
||||||
|
<TrendingUp className="w-4 h-4 text-green-600" />
|
||||||
|
<span className="text-gray-600">最佳模板:</span>
|
||||||
|
<span className="font-medium text-green-800 truncate">
|
||||||
|
{result.summary.best_matching_template}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result.summary.worst_matching_template && (
|
||||||
|
<div className="flex items-center space-x-2 col-span-2">
|
||||||
|
<TrendingDown className="w-4 h-4 text-red-600" />
|
||||||
|
<span className="text-gray-600">最差模板:</span>
|
||||||
|
<span className="font-medium text-red-800 truncate">
|
||||||
|
{result.summary.worst_matching_template}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 建议和提示 */}
|
||||||
|
{result.failed_matches > 0 && (
|
||||||
|
<div className="border-t border-gray-200 pt-4 mt-4">
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<Target className="w-4 h-4 text-yellow-600 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium text-yellow-800">优化建议</p>
|
||||||
|
<p className="text-yellow-700 mt-1">
|
||||||
|
{result.failed_matches} 个模板匹配失败。建议检查模板配置、素材分类情况或增加更多符合条件的素材。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result.successful_matches === result.total_bindings && result.total_bindings > 0 && (
|
||||||
|
<div className="border-t border-gray-200 pt-4 mt-4">
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-md p-3">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-600 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium text-green-800">匹配完美!</p>
|
||||||
|
<p className="text-green-700 mt-1">
|
||||||
|
所有模板都成功匹配,您的项目素材配置非常完善。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
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 { invoke } from '@tauri-apps/api/core';
|
||||||
import { useProjectStore } from '../store/projectStore';
|
import { useProjectStore } from '../store/projectStore';
|
||||||
import { useMaterialStore } from '../store/materialStore';
|
import { useMaterialStore } from '../store/materialStore';
|
||||||
@@ -29,6 +29,10 @@ import {
|
|||||||
import { MaterialMatchingService } from '../services/materialMatchingService';
|
import { MaterialMatchingService } from '../services/materialMatchingService';
|
||||||
import { MaterialMatchingResult, MaterialMatchingRequest } from '../types/materialMatching';
|
import { MaterialMatchingResult, MaterialMatchingRequest } from '../types/materialMatching';
|
||||||
import { MaterialSegmentView } from '../components/MaterialSegmentView';
|
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 { formatDistanceToNow } from 'date-fns';
|
||||||
import { zhCN } from 'date-fns/locale';
|
import { zhCN } from 'date-fns/locale';
|
||||||
import { MaterialSegmentStats } from '../components/MaterialSegmentStats';
|
import { MaterialSegmentStats } from '../components/MaterialSegmentStats';
|
||||||
@@ -129,6 +133,13 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
const [segmentStats, setSegmentStats] = useState<any>(null);
|
const [segmentStats, setSegmentStats] = useState<any>(null);
|
||||||
const [currentMatchingBinding, setCurrentMatchingBinding] = useState<ProjectTemplateBindingDetail | null>(null);
|
const [currentMatchingBinding, setCurrentMatchingBinding] = useState<ProjectTemplateBindingDetail | null>(null);
|
||||||
|
|
||||||
|
// 一键匹配状态
|
||||||
|
const [showBatchMatchingResultDialog, setShowBatchMatchingResultDialog] = useState(false);
|
||||||
|
const [batchMatchingResult, setBatchMatchingResult] = useState<BatchMatchingResult | null>(null);
|
||||||
|
const [batchMatchingLoading, setBatchMatchingLoading] = useState(false);
|
||||||
|
const [showBatchMatchingProgressDialog, setShowBatchMatchingProgressDialog] = useState(false);
|
||||||
|
const [batchMatchingProgress, setBatchMatchingProgress] = useState<BatchMatchingProgress | null>(null);
|
||||||
|
|
||||||
// 素材筛选状态
|
// 素材筛选状态
|
||||||
const [materialClassificationFilter, setMaterialClassificationFilter] = useState<string>('全部');
|
const [materialClassificationFilter, setMaterialClassificationFilter] = useState<string>('全部');
|
||||||
const [materialModelFilter, setMaterialModelFilter] = useState<string>('全部');
|
const [materialModelFilter, setMaterialModelFilter] = useState<string>('全部');
|
||||||
@@ -515,6 +526,83 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
setMatchingLoading(false);
|
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) => {
|
const handleEditMaterial = (material: Material) => {
|
||||||
setEditingMaterial(material);
|
setEditingMaterial(material);
|
||||||
@@ -1248,6 +1336,20 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
管理项目与模板的绑定关系,设置主要模板和备用模板。
|
管理项目与模板的绑定关系,设置主要模板和备用模板。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={handleBatchMatching}
|
||||||
|
disabled={batchMatchingLoading || filteredBindingDetails.length === 0}
|
||||||
|
className="inline-flex items-center px-4 py-2.5 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg transition-all duration-200 hover:scale-105 shadow-sm hover:shadow-md text-sm font-medium"
|
||||||
|
>
|
||||||
|
{batchMatchingLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Shuffle className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{batchMatchingLoading ? '匹配中...' : '一键匹配'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{bindingError && (
|
{bindingError && (
|
||||||
@@ -1420,6 +1522,23 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
onApplyResult={handleApplyMatchingResult}
|
onApplyResult={handleApplyMatchingResult}
|
||||||
onRetryMatching={handleRetryMatching}
|
onRetryMatching={handleRetryMatching}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 一键匹配结果对话框 */}
|
||||||
|
<BatchMatchingResultDialog
|
||||||
|
isOpen={showBatchMatchingResultDialog}
|
||||||
|
onClose={handleCloseBatchMatchingDialog}
|
||||||
|
result={batchMatchingResult}
|
||||||
|
loading={batchMatchingLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 一键匹配进度对话框 */}
|
||||||
|
<BatchMatchingProgressDialog
|
||||||
|
isOpen={showBatchMatchingProgressDialog}
|
||||||
|
onClose={handleCloseBatchMatchingProgressDialog}
|
||||||
|
onCancel={handleCancelBatchMatching}
|
||||||
|
progress={batchMatchingProgress}
|
||||||
|
canCancel={batchMatchingLoading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
218
apps/desktop/src/services/batchMatchingService.ts
Normal file
218
apps/desktop/src/services/batchMatchingService.ts
Normal file
@@ -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<BatchMatchingResult> {
|
||||||
|
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<BatchMatchingResult>('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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
apps/desktop/src/types/batchMatching.ts
Normal file
139
apps/desktop/src/types/batchMatching.ts
Normal file
@@ -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);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user