feat: 实现一键匹配功能 (v0.1.26)

- 新增一键匹配后端服务,支持遍历项目模板绑定并逐一匹配
- 在项目详情页添加一键匹配按钮,支持批量匹配操作
- 实现批量匹配进度管理,包括实时进度跟踪和取消功能
- 添加一键匹配结果汇总,包含详细统计和报告导出功能
- 新增批量匹配相关组件:进度对话框、结果对话框、汇总卡片
- 遵循 promptx/tauri-desktop-app-expert 开发规范
- 支持错误处理、状态管理和用户体验优化
This commit is contained in:
imeepos
2025-07-16 21:52:48 +08:00
parent d3ab2aa284
commit e3037916c0
9 changed files with 1524 additions and 3 deletions

View File

@@ -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<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)]
pub struct SegmentMatch {
@@ -533,4 +586,171 @@ impl MaterialMatchingService {
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),
}
}
}

View File

@@ -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,

View File

@@ -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<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())
}

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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<any>(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 [materialModelFilter, setMaterialModelFilter] = useState<string>('全部');
@@ -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 = () => {
</p>
</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>
{bindingError && (
@@ -1420,6 +1522,23 @@ export const ProjectDetails: React.FC = () => {
onApplyResult={handleApplyMatchingResult}
onRetryMatching={handleRetryMatching}
/>
{/* 一键匹配结果对话框 */}
<BatchMatchingResultDialog
isOpen={showBatchMatchingResultDialog}
onClose={handleCloseBatchMatchingDialog}
result={batchMatchingResult}
loading={batchMatchingLoading}
/>
{/* 一键匹配进度对话框 */}
<BatchMatchingProgressDialog
isOpen={showBatchMatchingProgressDialog}
onClose={handleCloseBatchMatchingProgressDialog}
onCancel={handleCancelBatchMatching}
progress={batchMatchingProgress}
canCancel={batchMatchingLoading}
/>
</div>
);
};

View 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`;
}
}
}

View 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);
};