feat: 实现一键匹配实时进度通讯

修复问题:
- 一键匹配进度条没有逐步递增,只在开始和结束时更新

实现内容:
1. 后端进度事件发送:
   - 在事件总线中添加BatchMatchingProgress事件类型
   - 在批量匹配服务中集成Tauri事件发送
   - 在每个模板匹配开始时发送实时进度事件

2. 前端进度事件监听:
   - 修改BatchMatchingService支持事件监听
   - 添加batch_matching_progress事件监听器
   - 实时更新进度条状态

3. 事件通讯机制:
   - 使用Tauri的emit系统发送事件到前端
   - 前端通过listen监听实时进度更新
   - 确保进度条能够逐步递增显示

技术细节:
- 后端:使用app_handle.emit()发送进度事件
- 前端:使用listen()监听batch_matching_progress事件
- 进度计算:基于当前轮数、绑定索引和总绑定数

现在一键匹配过程中进度条会实时更新,用户可以看到匹配的实际进展。
This commit is contained in:
imeepos 2025-07-21 20:07:27 +08:00
parent 70e8669ace
commit c3c72ce8bd
7 changed files with 96 additions and 17 deletions

View File

@ -16,6 +16,8 @@ use crate::data::repositories::{
use crate::business::services::template_service::TemplateService;
use crate::business::services::template_matching_result_service::TemplateMatchingResultService;
use crate::infrastructure::filename_utils::FilenameUtils;
use crate::infrastructure::event_bus::EventBusManager;
use tauri::Emitter;
use anyhow::{Result, anyhow};
use serde::{Serialize, Deserialize};
use std::collections::{HashMap, HashSet};
@ -28,6 +30,7 @@ pub struct MaterialMatchingService {
template_service: Arc<TemplateService>,
video_classification_repo: Arc<VideoClassificationRepository>,
matching_result_service: Option<Arc<TemplateMatchingResultService>>,
event_bus: Arc<EventBusManager>,
}
/// 素材匹配请求
@ -160,6 +163,7 @@ impl MaterialMatchingService {
template_service,
video_classification_repo,
matching_result_service: None,
event_bus: Arc::new(EventBusManager::new()),
}
}
@ -177,6 +181,7 @@ impl MaterialMatchingService {
template_service,
video_classification_repo,
matching_result_service: Some(matching_result_service),
event_bus: Arc::new(EventBusManager::new()),
}
}
@ -785,11 +790,17 @@ impl MaterialMatchingService {
/// 执行一键匹配 - 遍历项目的所有活跃模板绑定并逐一匹配
pub async fn batch_match_all_templates(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> {
// 调用优化的循环匹配方法
self.batch_match_all_templates_optimized(request, database).await
self.batch_match_all_templates_optimized(request, database, None).await
}
/// 执行一键匹配(带事件发送)
pub async fn batch_match_all_templates_with_events(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>, app_handle: Option<tauri::AppHandle>) -> Result<BatchMatchingResult> {
// 调用优化的循环匹配方法
self.batch_match_all_templates_optimized(request, database, app_handle).await
}
/// 优化的一键匹配 - 循环匹配模板直到失败(无法完整匹配模板 -- 素材不够用)
pub async fn batch_match_all_templates_optimized(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> {
pub async fn batch_match_all_templates_optimized(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>, app_handle: Option<tauri::AppHandle>) -> Result<BatchMatchingResult> {
let start_time = std::time::Instant::now();
// 获取项目的所有活跃模板绑定
@ -884,9 +895,23 @@ impl MaterialMatchingService {
}
// 逐一尝试匹配每个模板绑定
for binding_detail in &active_bindings {
for (binding_index, binding_detail) in active_bindings.iter().enumerate() {
let binding_start_time = std::time::Instant::now();
// 发送进度事件
if let Some(ref handle) = app_handle {
let current_binding_index = (total_rounds - 1) as usize * active_bindings.len() + binding_index + 1;
let _ = handle.emit("batch_matching_progress", serde_json::json!({
"project_id": request.project_id,
"current_binding_index": current_binding_index,
"total_bindings": active_bindings.len() * 100, // 估算总数
"current_template_name": binding_detail.template_name,
"completed_bindings": successful_matches,
"failed_bindings": failed_matches,
"elapsed_time_ms": start_time.elapsed().as_millis() as u64,
}));
}
let matching_request = MaterialMatchingRequest {
project_id: request.project_id.clone(),
template_id: binding_detail.binding.template_id.clone(),

View File

@ -382,17 +382,12 @@ impl MaterialSegment {
/// 检查片段是否满足最小时长要求
pub fn meets_duration_requirement(&self, required_duration: f64) -> bool {
let meets = self.duration >= required_duration;
println!(" 📏 时长要求检查: 素材{:.3}s >= 要求{:.3}s = {}",
self.duration, required_duration, meets);
meets
self.duration >= required_duration
}
/// 计算与目标时长的匹配度越接近越好返回0.0-1.0
pub fn duration_match_score(&self, target_duration: f64) -> f64 {
if self.duration < target_duration {
println!(" 📊 匹配评分: 素材{:.3}s < 目标{:.3}s时长不足 = 0.0",
self.duration, target_duration);
return 0.0; // 时长不足,不匹配
}
@ -405,9 +400,6 @@ impl MaterialSegment {
} else {
0.3 // 超出50%以上,低匹配度
};
println!(" 📊 匹配评分: 素材{:.3}s vs 目标{:.3}s超出比例{:.1}% = {:.3}",
self.duration, target_duration, excess_ratio * 100.0, score);
score
}
}

View File

@ -73,6 +73,16 @@ pub enum DataEvent {
stage: String, // "metadata", "scene_detection", "video_splitting"
progress_percentage: f64,
},
/// 批量匹配进度事件
BatchMatchingProgress {
project_id: String,
current_binding_index: u32,
total_bindings: u32,
current_template_name: Option<String>,
completed_bindings: u32,
failed_bindings: u32,
elapsed_time_ms: u64,
},
}
/// UI事件
@ -293,6 +303,28 @@ impl EventBusManager {
progress_percentage,
})).await
}
/// 发布批量匹配进度事件
pub async fn publish_batch_matching_progress(
&self,
project_id: String,
current_binding_index: u32,
total_bindings: u32,
current_template_name: Option<String>,
completed_bindings: u32,
failed_bindings: u32,
elapsed_time_ms: u64,
) -> Result<(), String> {
self.event_bus.publish(Event::Data(DataEvent::BatchMatchingProgress {
project_id,
current_binding_index,
total_bindings,
current_template_name,
completed_bindings,
failed_bindings,
elapsed_time_ms,
})).await
}
}
impl Default for EventBusManager {

View File

@ -710,7 +710,7 @@ impl TolerantJsonParser {
}
// 如果值部分是一个被引号包围的字符串
if (value_part.starts_with('"') && value_part.len() > 1) {
if value_part.starts_with('"') && value_part.len() > 1 {
// 寻找匹配的结束引号,考虑转义字符
let mut end_pos = None;
let mut chars = value_part[1..].char_indices();

View File

@ -265,6 +265,7 @@ pub struct TemplateBindingMatchingValidation {
pub async fn batch_match_all_templates(
request: BatchMatchingRequest,
state: State<'_, crate::app_state::AppState>,
app_handle: tauri::AppHandle,
) -> Result<BatchMatchingResult, String> {
let database = state.get_database();
@ -295,8 +296,8 @@ pub async fn batch_match_all_templates(
matching_result_service,
);
// 执行一键匹配
matching_service.batch_match_all_templates(request, database)
// 执行一键匹配(带事件发送)
matching_service.batch_match_all_templates_with_events(request, database, Some(app_handle))
.await
.map_err(|e| e.to_string())
}

View File

@ -549,7 +549,7 @@ export const ProjectDetails: React.FC = () => {
setShowBatchMatchingProgressDialog(true);
// 设置进度回调
BatchMatchingService.setProgressCallback((progress: BatchMatchingProgress) => {
await BatchMatchingService.setProgressCallback((progress: BatchMatchingProgress) => {
setBatchMatchingProgress(progress);
// 当匹配完成时,显示结果对话框

View File

@ -4,6 +4,7 @@
*/
import { invoke } from '@tauri-apps/api/core';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import {
BatchMatchingRequest,
BatchMatchingResult,
@ -14,12 +15,34 @@ import {
export class BatchMatchingService {
// 进度回调函数类型
static progressCallback: ((progress: BatchMatchingProgress) => void) | null = null;
// 事件监听器
static progressEventUnlisten: UnlistenFn | null = null;
/**
*
*/
static setProgressCallback(callback: (progress: BatchMatchingProgress) => void) {
static async setProgressCallback(callback: (progress: BatchMatchingProgress) => void) {
this.progressCallback = callback;
// 设置事件监听器
try {
this.progressEventUnlisten = await listen('batch_matching_progress', (event: any) => {
const progressData = event.payload;
if (this.progressCallback) {
this.progressCallback({
status: BatchMatchingProgressStatus.InProgress,
current_binding_index: progressData.current_binding_index,
total_bindings: progressData.total_bindings,
current_template_name: progressData.current_template_name,
completed_bindings: progressData.completed_bindings,
failed_bindings: progressData.failed_bindings,
elapsed_time_ms: progressData.elapsed_time_ms,
});
}
});
} catch (error) {
console.error('设置批量匹配进度事件监听器失败:', error);
}
}
/**
@ -27,6 +50,12 @@ export class BatchMatchingService {
*/
static clearProgressCallback() {
this.progressCallback = null;
// 清除事件监听器
if (this.progressEventUnlisten) {
this.progressEventUnlisten();
this.progressEventUnlisten = null;
}
}
/**