feat: 实现模板匹配结果保存和管理功能
- 新增模板匹配结果数据模型和数据库表结构 - 实现匹配结果Repository层,支持CRUD操作和查询 - 实现匹配结果Service层,提供业务逻辑和统计功能 - 新增Tauri命令接口,支持前端调用 - 实现前端TypeScript类型定义 - 更新MaterialMatchingService,支持自动保存匹配结果 - 新增前端管理界面组件: - TemplateMatchingResultManager: 主管理界面 - TemplateMatchingResultCard: 结果卡片组件 - TemplateMatchingResultDetailModal: 详情模态框 - TemplateMatchingResultStatsPanel: 统计面板 - 编写完整的单元测试 - 新增API文档 功能特性: - 保存匹配结果到数据库,包含成功和失败片段详情 - 支持匹配结果的查询、过滤、排序和分页 - 提供匹配统计信息和质量评分 - 支持软删除和批量操作 - 完整的前端管理界面,支持查看、编辑、删除操作
This commit is contained in:
@@ -11,8 +11,10 @@ use crate::data::models::{
|
|||||||
use crate::data::repositories::{
|
use crate::data::repositories::{
|
||||||
material_repository::MaterialRepository,
|
material_repository::MaterialRepository,
|
||||||
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 anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -23,6 +25,7 @@ pub struct MaterialMatchingService {
|
|||||||
material_repo: Arc<MaterialRepository>,
|
material_repo: Arc<MaterialRepository>,
|
||||||
template_service: Arc<TemplateService>,
|
template_service: Arc<TemplateService>,
|
||||||
video_classification_repo: Arc<VideoClassificationRepository>,
|
video_classification_repo: Arc<VideoClassificationRepository>,
|
||||||
|
matching_result_service: Option<Arc<TemplateMatchingResultService>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 素材匹配请求
|
/// 素材匹配请求
|
||||||
@@ -89,6 +92,22 @@ impl MaterialMatchingService {
|
|||||||
material_repo,
|
material_repo,
|
||||||
template_service,
|
template_service,
|
||||||
video_classification_repo,
|
video_classification_repo,
|
||||||
|
matching_result_service: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建新的素材匹配服务实例(带结果保存功能)
|
||||||
|
pub fn new_with_result_service(
|
||||||
|
material_repo: Arc<MaterialRepository>,
|
||||||
|
template_service: Arc<TemplateService>,
|
||||||
|
video_classification_repo: Arc<VideoClassificationRepository>,
|
||||||
|
matching_result_service: Arc<TemplateMatchingResultService>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
material_repo,
|
||||||
|
template_service,
|
||||||
|
video_classification_repo,
|
||||||
|
matching_result_service: Some(matching_result_service),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +212,41 @@ impl MaterialMatchingService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 执行素材匹配并自动保存结果到数据库
|
||||||
|
pub async fn match_materials_and_save(
|
||||||
|
&self,
|
||||||
|
request: MaterialMatchingRequest,
|
||||||
|
result_name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
) -> Result<(MaterialMatchingResult, Option<crate::data::models::template_matching_result::TemplateMatchingResult>)> {
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
|
// 执行匹配
|
||||||
|
let matching_result = self.match_materials(request).await?;
|
||||||
|
|
||||||
|
let matching_duration_ms = start_time.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
// 如果配置了结果保存服务,则自动保存结果
|
||||||
|
if let Some(result_service) = &self.matching_result_service {
|
||||||
|
match result_service.save_matching_result(
|
||||||
|
&matching_result,
|
||||||
|
result_name,
|
||||||
|
description,
|
||||||
|
matching_duration_ms,
|
||||||
|
).await {
|
||||||
|
Ok(saved_result) => {
|
||||||
|
return Ok((matching_result, Some(saved_result)));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// 保存失败时记录错误但不影响匹配结果的返回
|
||||||
|
eprintln!("保存匹配结果失败: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((matching_result, None))
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取已分类的素材片段
|
/// 获取已分类的素材片段
|
||||||
async fn get_classified_segments(
|
async fn get_classified_segments(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub mod performance_monitor;
|
|||||||
pub mod enhanced_template_import_service;
|
pub mod enhanced_template_import_service;
|
||||||
pub mod project_template_binding_service;
|
pub mod project_template_binding_service;
|
||||||
pub mod material_matching_service;
|
pub mod material_matching_service;
|
||||||
|
pub mod template_matching_result_service;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
use crate::data::models::template_matching_result::{
|
||||||
|
TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult,
|
||||||
|
CreateTemplateMatchingResultRequest, TemplateMatchingResultQueryOptions,
|
||||||
|
MatchingResultStatus,
|
||||||
|
};
|
||||||
|
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||||
|
use crate::business::services::material_matching_service::{
|
||||||
|
MaterialMatchingResult as ServiceMatchingResult, SegmentMatch, FailedSegmentMatch,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 模板匹配结果服务
|
||||||
|
/// 遵循 Tauri 开发规范的业务逻辑设计原则
|
||||||
|
pub struct TemplateMatchingResultService {
|
||||||
|
repository: Arc<TemplateMatchingResultRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateMatchingResultService {
|
||||||
|
/// 创建新的模板匹配结果服务实例
|
||||||
|
pub fn new(repository: Arc<TemplateMatchingResultRepository>) -> Self {
|
||||||
|
Self { repository }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存匹配结果到数据库
|
||||||
|
/// 这是一个完整的事务操作,保存主结果和所有相关的片段信息
|
||||||
|
pub async fn save_matching_result(
|
||||||
|
&self,
|
||||||
|
service_result: &ServiceMatchingResult,
|
||||||
|
result_name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
matching_duration_ms: u64,
|
||||||
|
) -> Result<TemplateMatchingResult> {
|
||||||
|
// 创建主匹配结果记录
|
||||||
|
let create_request = CreateTemplateMatchingResultRequest {
|
||||||
|
project_id: service_result.project_id.clone(),
|
||||||
|
template_id: service_result.template_id.clone(),
|
||||||
|
binding_id: service_result.binding_id.clone(),
|
||||||
|
result_name,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut matching_result = self.repository.create(create_request)?;
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
let total_segments = service_result.matches.len() + service_result.failed_segments.len();
|
||||||
|
let matched_segments = service_result.matches.len();
|
||||||
|
let failed_segments = service_result.failed_segments.len();
|
||||||
|
|
||||||
|
// 计算使用的素材和模特数量
|
||||||
|
let mut used_material_ids = std::collections::HashSet::new();
|
||||||
|
let mut used_model_names = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
for segment_match in &service_result.matches {
|
||||||
|
used_material_ids.insert(segment_match.material_segment.material_id.clone());
|
||||||
|
if let Some(model_name) = &segment_match.model_name {
|
||||||
|
used_model_names.insert(model_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matching_result.update_statistics(
|
||||||
|
total_segments as u32,
|
||||||
|
matched_segments as u32,
|
||||||
|
failed_segments as u32,
|
||||||
|
used_material_ids.len() as u32,
|
||||||
|
used_model_names.len() as u32,
|
||||||
|
matching_duration_ms,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新主记录
|
||||||
|
self.repository.update(&matching_result)?;
|
||||||
|
|
||||||
|
// 保存成功匹配的片段
|
||||||
|
let segment_results: Vec<MatchingSegmentResult> = service_result.matches
|
||||||
|
.iter()
|
||||||
|
.map(|segment_match| {
|
||||||
|
let mut segment_result = MatchingSegmentResult::new(
|
||||||
|
uuid::Uuid::new_v4().to_string(),
|
||||||
|
matching_result.id.clone(),
|
||||||
|
segment_match.track_segment_id.clone(),
|
||||||
|
segment_match.track_segment_name.clone(),
|
||||||
|
segment_match.material_segment_id.clone(),
|
||||||
|
segment_match.material_segment.material_id.clone(),
|
||||||
|
segment_match.material_name.clone(),
|
||||||
|
segment_match.match_score,
|
||||||
|
segment_match.match_reason.clone(),
|
||||||
|
(segment_match.material_segment.duration * 1_000_000.0) as u64,
|
||||||
|
(segment_match.material_segment.start_time * 1_000_000.0) as u64,
|
||||||
|
(segment_match.material_segment.end_time * 1_000_000.0) as u64,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置模特信息
|
||||||
|
if let Some(model_name) = &segment_match.model_name {
|
||||||
|
// 这里可以通过模特名称查找模特ID,暂时使用名称作为ID
|
||||||
|
segment_result.set_model_info(model_name.clone(), model_name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
segment_result
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !segment_results.is_empty() {
|
||||||
|
self.repository.create_segment_results(&segment_results)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存失败的片段
|
||||||
|
let failed_segment_results: Vec<MatchingFailedSegmentResult> = service_result.failed_segments
|
||||||
|
.iter()
|
||||||
|
.map(|failed_segment| {
|
||||||
|
let mut failed_result = MatchingFailedSegmentResult::new(
|
||||||
|
uuid::Uuid::new_v4().to_string(),
|
||||||
|
matching_result.id.clone(),
|
||||||
|
failed_segment.track_segment_id.clone(),
|
||||||
|
failed_segment.track_segment_name.clone(),
|
||||||
|
failed_segment.matching_rule.display_name(),
|
||||||
|
failed_segment.failure_reason.clone(),
|
||||||
|
0, // 这里需要从模板中获取片段时长
|
||||||
|
0, // 这里需要从模板中获取开始时间
|
||||||
|
0, // 这里需要从模板中获取结束时间
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置匹配规则数据
|
||||||
|
if let Ok(rule_json) = serde_json::to_string(&failed_segment.matching_rule) {
|
||||||
|
failed_result.set_matching_rule_data(rule_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
failed_result
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !failed_segment_results.is_empty() {
|
||||||
|
self.repository.create_failed_segment_results(&failed_segment_results)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(matching_result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取匹配结果详情(包含所有片段信息)
|
||||||
|
pub async fn get_matching_result_detail(&self, result_id: &str) -> Result<Option<TemplateMatchingResultDetail>> {
|
||||||
|
let matching_result = match self.repository.get_by_id(result_id)? {
|
||||||
|
Some(result) => result,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let segment_results = self.repository.get_segment_results_by_matching_result_id(result_id)?;
|
||||||
|
let failed_segment_results = self.repository.get_failed_segment_results_by_matching_result_id(result_id)?;
|
||||||
|
|
||||||
|
Ok(Some(TemplateMatchingResultDetail {
|
||||||
|
matching_result,
|
||||||
|
segment_results,
|
||||||
|
failed_segment_results,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取项目的匹配结果列表
|
||||||
|
pub async fn get_project_matching_results(&self, project_id: &str) -> Result<Vec<TemplateMatchingResult>> {
|
||||||
|
Ok(self.repository.get_by_project_id(project_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取模板的匹配结果列表
|
||||||
|
pub async fn get_template_matching_results(&self, template_id: &str) -> Result<Vec<TemplateMatchingResult>> {
|
||||||
|
Ok(self.repository.get_by_template_id(template_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取绑定的匹配结果列表
|
||||||
|
pub async fn get_binding_matching_results(&self, binding_id: &str) -> Result<Vec<TemplateMatchingResult>> {
|
||||||
|
Ok(self.repository.get_by_binding_id(binding_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询匹配结果列表
|
||||||
|
pub async fn list_matching_results(&self, options: TemplateMatchingResultQueryOptions) -> Result<Vec<TemplateMatchingResult>> {
|
||||||
|
Ok(self.repository.list(options)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除匹配结果
|
||||||
|
pub async fn delete_matching_result(&self, result_id: &str) -> Result<bool> {
|
||||||
|
Ok(self.repository.delete(result_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 软删除匹配结果
|
||||||
|
pub async fn soft_delete_matching_result(&self, result_id: &str) -> Result<bool> {
|
||||||
|
Ok(self.repository.soft_delete(result_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新匹配结果名称和描述
|
||||||
|
pub async fn update_matching_result_info(
|
||||||
|
&self,
|
||||||
|
result_id: &str,
|
||||||
|
result_name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
) -> Result<Option<TemplateMatchingResult>> {
|
||||||
|
let mut matching_result = match self.repository.get_by_id(result_id)? {
|
||||||
|
Some(result) => result,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut updated = false;
|
||||||
|
|
||||||
|
if let Some(name) = result_name {
|
||||||
|
matching_result.result_name = name;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(desc) = description {
|
||||||
|
matching_result.set_description(desc);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated {
|
||||||
|
matching_result.updated_at = Utc::now();
|
||||||
|
self.repository.update(&matching_result)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(matching_result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置匹配结果质量评分
|
||||||
|
pub async fn set_quality_score(&self, result_id: &str, quality_score: f64) -> Result<Option<TemplateMatchingResult>> {
|
||||||
|
let mut matching_result = match self.repository.get_by_id(result_id)? {
|
||||||
|
Some(result) => result,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
matching_result.set_quality_score(quality_score);
|
||||||
|
self.repository.update(&matching_result)?;
|
||||||
|
|
||||||
|
Ok(Some(matching_result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取匹配结果统计信息
|
||||||
|
pub async fn get_matching_statistics(&self, project_id: Option<&str>) -> Result<MatchingStatistics> {
|
||||||
|
let options = TemplateMatchingResultQueryOptions {
|
||||||
|
project_id: project_id.map(|id| id.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let results = self.repository.list(options)?;
|
||||||
|
|
||||||
|
let total_results = results.len();
|
||||||
|
let successful_results = results.iter().filter(|r| r.is_successful()).count();
|
||||||
|
let total_segments: u32 = results.iter().map(|r| r.total_segments).sum();
|
||||||
|
let matched_segments: u32 = results.iter().map(|r| r.matched_segments).sum();
|
||||||
|
let total_materials: u32 = results.iter().map(|r| r.used_materials).sum();
|
||||||
|
let total_models: u32 = results.iter().map(|r| r.used_models).sum();
|
||||||
|
|
||||||
|
let average_success_rate = if total_results > 0 {
|
||||||
|
results.iter().map(|r| r.success_rate).sum::<f64>() / total_results as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(MatchingStatistics {
|
||||||
|
total_results,
|
||||||
|
successful_results,
|
||||||
|
total_segments,
|
||||||
|
matched_segments,
|
||||||
|
total_materials,
|
||||||
|
total_models,
|
||||||
|
average_success_rate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模板匹配结果详情
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct TemplateMatchingResultDetail {
|
||||||
|
pub matching_result: TemplateMatchingResult,
|
||||||
|
pub segment_results: Vec<MatchingSegmentResult>,
|
||||||
|
pub failed_segment_results: Vec<MatchingFailedSegmentResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 匹配统计信息
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct MatchingStatistics {
|
||||||
|
pub total_results: usize,
|
||||||
|
pub successful_results: usize,
|
||||||
|
pub total_segments: u32,
|
||||||
|
pub matched_segments: u32,
|
||||||
|
pub total_materials: u32,
|
||||||
|
pub total_models: u32,
|
||||||
|
pub average_success_rate: f64,
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod draft_parser_tests;
|
pub mod draft_parser_tests;
|
||||||
pub mod cloud_upload_service_tests;
|
pub mod cloud_upload_service_tests;
|
||||||
|
pub mod template_matching_result_service_tests;
|
||||||
|
|
||||||
// 测试工具函数
|
// 测试工具函数
|
||||||
pub mod test_utils {
|
pub mod test_utils {
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::super::super::template_matching_result_service::{
|
||||||
|
TemplateMatchingResultService, TemplateMatchingResultDetail, MatchingStatistics,
|
||||||
|
};
|
||||||
|
use crate::data::models::template_matching_result::{
|
||||||
|
TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult,
|
||||||
|
CreateTemplateMatchingResultRequest, TemplateMatchingResultQueryOptions,
|
||||||
|
MatchingResultStatus,
|
||||||
|
};
|
||||||
|
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||||
|
use crate::business::services::material_matching_service::{
|
||||||
|
MaterialMatchingResult as ServiceMatchingResult, SegmentMatch, FailedSegmentMatch,
|
||||||
|
MatchingStatistics as ServiceMatchingStatistics,
|
||||||
|
};
|
||||||
|
use crate::data::models::template::SegmentMatchingRule;
|
||||||
|
use crate::data::models::material::MaterialSegment;
|
||||||
|
use crate::infrastructure::database::Database;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
/// 创建测试数据库
|
||||||
|
async fn create_test_database() -> Arc<Database> {
|
||||||
|
let database = Database::new().expect("Failed to create database");
|
||||||
|
Arc::new(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建测试用的素材匹配结果
|
||||||
|
fn create_test_service_matching_result() -> ServiceMatchingResult {
|
||||||
|
let material_segment = MaterialSegment {
|
||||||
|
id: "segment-1".to_string(),
|
||||||
|
material_id: "material-1".to_string(),
|
||||||
|
segment_index: 0,
|
||||||
|
start_time: 0.0,
|
||||||
|
end_time: 10.0,
|
||||||
|
duration: 10.0,
|
||||||
|
file_path: "/test/video.mp4".to_string(),
|
||||||
|
file_size: 1024,
|
||||||
|
thumbnail_path: None,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let segment_match = SegmentMatch {
|
||||||
|
track_segment_id: "track-segment-1".to_string(),
|
||||||
|
track_segment_name: "测试轨道片段".to_string(),
|
||||||
|
material_segment_id: "segment-1".to_string(),
|
||||||
|
material_segment,
|
||||||
|
material_name: "测试素材".to_string(),
|
||||||
|
model_name: Some("测试模特".to_string()),
|
||||||
|
match_score: 0.95,
|
||||||
|
match_reason: "AI分类匹配".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let failed_segment = FailedSegmentMatch {
|
||||||
|
track_segment_id: "track-segment-2".to_string(),
|
||||||
|
track_segment_name: "失败轨道片段".to_string(),
|
||||||
|
matching_rule: SegmentMatchingRule::AiClassification {
|
||||||
|
category_id: "category-1".to_string(),
|
||||||
|
category_name: "全身".to_string(),
|
||||||
|
},
|
||||||
|
failure_reason: "没有找到匹配的素材".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let statistics = ServiceMatchingStatistics {
|
||||||
|
total_segments: 2,
|
||||||
|
matched_segments: 1,
|
||||||
|
failed_segments: 1,
|
||||||
|
success_rate: 50.0,
|
||||||
|
used_materials: 1,
|
||||||
|
used_models: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
ServiceMatchingResult {
|
||||||
|
binding_id: "binding-1".to_string(),
|
||||||
|
template_id: "template-1".to_string(),
|
||||||
|
project_id: "project-1".to_string(),
|
||||||
|
matches: vec![segment_match],
|
||||||
|
statistics,
|
||||||
|
failed_segments: vec![failed_segment],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_save_matching_result() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
|
||||||
|
// 先创建必要的依赖数据
|
||||||
|
let conn = database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
// 创建项目
|
||||||
|
let unique_path = format!("/test/path/{}", uuid::Uuid::new_v4());
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
["project-1", "测试项目", &unique_path, "2024-01-01T00:00:00Z", "2024-01-01T00:00:00Z"],
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// 创建模板
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO templates (id, name, description, canvas_width, canvas_height, canvas_ratio, duration, fps, import_status, source_file_path, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||||
|
["template-1", "测试模板", "测试模板描述", "1920", "1080", "16:9", "10000000", "30.0", "Completed", "/test/template.json", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00Z"],
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// 创建绑定
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO project_template_bindings (id, project_id, template_id, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
["binding-1", "project-1", "template-1", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00Z"],
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
let service_result = create_test_service_matching_result();
|
||||||
|
let result_name = "测试匹配结果".to_string();
|
||||||
|
let description = Some("这是一个测试匹配结果".to_string());
|
||||||
|
let matching_duration_ms = 1500;
|
||||||
|
|
||||||
|
let result = service.save_matching_result(
|
||||||
|
&service_result,
|
||||||
|
result_name.clone(),
|
||||||
|
description.clone(),
|
||||||
|
matching_duration_ms,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
if let Err(e) = &result {
|
||||||
|
println!("Error saving matching result: {}", e);
|
||||||
|
}
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let saved_result = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(saved_result.result_name, result_name);
|
||||||
|
assert_eq!(saved_result.description, description);
|
||||||
|
assert_eq!(saved_result.project_id, service_result.project_id);
|
||||||
|
assert_eq!(saved_result.template_id, service_result.template_id);
|
||||||
|
assert_eq!(saved_result.binding_id, service_result.binding_id);
|
||||||
|
assert_eq!(saved_result.total_segments, 2);
|
||||||
|
assert_eq!(saved_result.matched_segments, 1);
|
||||||
|
assert_eq!(saved_result.failed_segments, 1);
|
||||||
|
assert_eq!(saved_result.success_rate, 50.0);
|
||||||
|
assert_eq!(saved_result.used_materials, 1);
|
||||||
|
assert_eq!(saved_result.used_models, 1);
|
||||||
|
assert_eq!(saved_result.matching_duration_ms, matching_duration_ms);
|
||||||
|
assert_eq!(saved_result.status, MatchingResultStatus::PartialSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_matching_result_detail() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
// 先保存一个匹配结果
|
||||||
|
let service_result = create_test_service_matching_result();
|
||||||
|
let saved_result = service.save_matching_result(
|
||||||
|
&service_result,
|
||||||
|
"测试匹配结果".to_string(),
|
||||||
|
Some("测试描述".to_string()),
|
||||||
|
1000,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
// 获取详情
|
||||||
|
let detail_result = service.get_matching_result_detail(&saved_result.id).await;
|
||||||
|
assert!(detail_result.is_ok());
|
||||||
|
|
||||||
|
let detail = detail_result.unwrap();
|
||||||
|
assert!(detail.is_some());
|
||||||
|
|
||||||
|
let detail = detail.unwrap();
|
||||||
|
assert_eq!(detail.matching_result.id, saved_result.id);
|
||||||
|
assert_eq!(detail.segment_results.len(), 1);
|
||||||
|
assert_eq!(detail.failed_segment_results.len(), 1);
|
||||||
|
|
||||||
|
// 验证成功片段
|
||||||
|
let segment_result = &detail.segment_results[0];
|
||||||
|
assert_eq!(segment_result.track_segment_name, "测试轨道片段");
|
||||||
|
assert_eq!(segment_result.material_name, "测试素材");
|
||||||
|
assert_eq!(segment_result.model_name, Some("测试模特".to_string()));
|
||||||
|
assert_eq!(segment_result.match_score, 0.95);
|
||||||
|
|
||||||
|
// 验证失败片段
|
||||||
|
let failed_segment = &detail.failed_segment_results[0];
|
||||||
|
assert_eq!(failed_segment.track_segment_name, "失败轨道片段");
|
||||||
|
assert_eq!(failed_segment.failure_reason, "没有找到匹配的素材");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_project_matching_results() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
let project_id = "test-project-1";
|
||||||
|
|
||||||
|
// 保存两个匹配结果
|
||||||
|
let mut service_result1 = create_test_service_matching_result();
|
||||||
|
service_result1.project_id = project_id.to_string();
|
||||||
|
|
||||||
|
let mut service_result2 = create_test_service_matching_result();
|
||||||
|
service_result2.project_id = project_id.to_string();
|
||||||
|
service_result2.template_id = "template-2".to_string();
|
||||||
|
|
||||||
|
service.save_matching_result(&service_result1, "结果1".to_string(), None, 1000).await.unwrap();
|
||||||
|
service.save_matching_result(&service_result2, "结果2".to_string(), None, 1500).await.unwrap();
|
||||||
|
|
||||||
|
// 获取项目的匹配结果
|
||||||
|
let results = service.get_project_matching_results(project_id).await;
|
||||||
|
assert!(results.is_ok());
|
||||||
|
|
||||||
|
let results = results.unwrap();
|
||||||
|
assert_eq!(results.len(), 2);
|
||||||
|
assert!(results.iter().all(|r| r.project_id == project_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_matching_result() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
// 保存一个匹配结果
|
||||||
|
let service_result = create_test_service_matching_result();
|
||||||
|
let saved_result = service.save_matching_result(
|
||||||
|
&service_result,
|
||||||
|
"待删除结果".to_string(),
|
||||||
|
None,
|
||||||
|
1000,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
// 删除结果
|
||||||
|
let delete_result = service.delete_matching_result(&saved_result.id).await;
|
||||||
|
assert!(delete_result.is_ok());
|
||||||
|
assert!(delete_result.unwrap());
|
||||||
|
|
||||||
|
// 验证已删除
|
||||||
|
let detail_result = service.get_matching_result_detail(&saved_result.id).await;
|
||||||
|
assert!(detail_result.is_ok());
|
||||||
|
assert!(detail_result.unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_soft_delete_matching_result() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
// 保存一个匹配结果
|
||||||
|
let service_result = create_test_service_matching_result();
|
||||||
|
let saved_result = service.save_matching_result(
|
||||||
|
&service_result,
|
||||||
|
"待软删除结果".to_string(),
|
||||||
|
None,
|
||||||
|
1000,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
// 软删除结果
|
||||||
|
let delete_result = service.soft_delete_matching_result(&saved_result.id).await;
|
||||||
|
assert!(delete_result.is_ok());
|
||||||
|
assert!(delete_result.unwrap());
|
||||||
|
|
||||||
|
// 验证在列表中不可见(软删除)
|
||||||
|
let list_result = service.list_matching_results(TemplateMatchingResultQueryOptions::default()).await;
|
||||||
|
assert!(list_result.is_ok());
|
||||||
|
let results = list_result.unwrap();
|
||||||
|
assert!(!results.iter().any(|r| r.id == saved_result.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_matching_result_info() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
// 保存一个匹配结果
|
||||||
|
let service_result = create_test_service_matching_result();
|
||||||
|
let saved_result = service.save_matching_result(
|
||||||
|
&service_result,
|
||||||
|
"原始名称".to_string(),
|
||||||
|
Some("原始描述".to_string()),
|
||||||
|
1000,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
// 更新信息
|
||||||
|
let new_name = "更新后的名称".to_string();
|
||||||
|
let new_description = "更新后的描述".to_string();
|
||||||
|
|
||||||
|
let update_result = service.update_matching_result_info(
|
||||||
|
&saved_result.id,
|
||||||
|
Some(new_name.clone()),
|
||||||
|
Some(new_description.clone()),
|
||||||
|
).await;
|
||||||
|
|
||||||
|
assert!(update_result.is_ok());
|
||||||
|
let updated_result = update_result.unwrap();
|
||||||
|
assert!(updated_result.is_some());
|
||||||
|
|
||||||
|
let updated_result = updated_result.unwrap();
|
||||||
|
assert_eq!(updated_result.result_name, new_name);
|
||||||
|
assert_eq!(updated_result.description, Some(new_description));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_set_quality_score() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
// 保存一个匹配结果
|
||||||
|
let service_result = create_test_service_matching_result();
|
||||||
|
let saved_result = service.save_matching_result(
|
||||||
|
&service_result,
|
||||||
|
"质量评分测试".to_string(),
|
||||||
|
None,
|
||||||
|
1000,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
// 设置质量评分
|
||||||
|
let quality_score = 4.5;
|
||||||
|
let score_result = service.set_quality_score(&saved_result.id, quality_score).await;
|
||||||
|
|
||||||
|
assert!(score_result.is_ok());
|
||||||
|
let updated_result = score_result.unwrap();
|
||||||
|
assert!(updated_result.is_some());
|
||||||
|
|
||||||
|
let updated_result = updated_result.unwrap();
|
||||||
|
assert_eq!(updated_result.quality_score, Some(quality_score));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_matching_statistics() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
let project_id = "stats-test-project";
|
||||||
|
|
||||||
|
// 保存多个匹配结果
|
||||||
|
for i in 0..3 {
|
||||||
|
let mut service_result = create_test_service_matching_result();
|
||||||
|
service_result.project_id = project_id.to_string();
|
||||||
|
service_result.template_id = format!("template-{}", i);
|
||||||
|
|
||||||
|
service.save_matching_result(
|
||||||
|
&service_result,
|
||||||
|
format!("结果{}", i),
|
||||||
|
None,
|
||||||
|
1000 + i * 100,
|
||||||
|
).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计信息
|
||||||
|
let stats_result = service.get_matching_statistics(Some(project_id)).await;
|
||||||
|
assert!(stats_result.is_ok());
|
||||||
|
|
||||||
|
let stats = stats_result.unwrap();
|
||||||
|
assert_eq!(stats.total_results, 3);
|
||||||
|
assert_eq!(stats.successful_results, 3); // 所有结果都是部分成功
|
||||||
|
assert_eq!(stats.total_segments, 6); // 每个结果2个片段
|
||||||
|
assert_eq!(stats.matched_segments, 3); // 每个结果1个匹配片段
|
||||||
|
assert_eq!(stats.total_materials, 3); // 每个结果1个素材
|
||||||
|
assert_eq!(stats.total_models, 3); // 每个结果1个模特
|
||||||
|
assert_eq!(stats.average_success_rate, 50.0); // 平均成功率50%
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_matching_results_with_filters() {
|
||||||
|
let database = create_test_database().await;
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
// 保存不同状态的匹配结果
|
||||||
|
let mut service_result1 = create_test_service_matching_result();
|
||||||
|
service_result1.statistics.success_rate = 100.0; // 成功
|
||||||
|
service_result1.statistics.matched_segments = 2;
|
||||||
|
service_result1.statistics.failed_segments = 0;
|
||||||
|
|
||||||
|
let service_result2 = create_test_service_matching_result(); // 部分成功
|
||||||
|
|
||||||
|
service.save_matching_result(&service_result1, "成功结果".to_string(), None, 1000).await.unwrap();
|
||||||
|
service.save_matching_result(&service_result2, "部分成功结果".to_string(), None, 1500).await.unwrap();
|
||||||
|
|
||||||
|
// 测试状态过滤
|
||||||
|
let options = TemplateMatchingResultQueryOptions {
|
||||||
|
status: Some(MatchingResultStatus::Success),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let results = service.list_matching_results(options).await.unwrap();
|
||||||
|
assert_eq!(results.len(), 1);
|
||||||
|
assert_eq!(results[0].status, MatchingResultStatus::Success);
|
||||||
|
|
||||||
|
// 测试搜索关键词
|
||||||
|
let options = TemplateMatchingResultQueryOptions {
|
||||||
|
search_keyword: Some("成功".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let results = service.list_matching_results(options).await.unwrap();
|
||||||
|
assert_eq!(results.len(), 2); // 两个结果都包含"成功"
|
||||||
|
|
||||||
|
// 测试分页
|
||||||
|
let options = TemplateMatchingResultQueryOptions {
|
||||||
|
limit: Some(1),
|
||||||
|
offset: Some(0),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let results = service.list_matching_results(options).await.unwrap();
|
||||||
|
assert_eq!(results.len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,3 +6,4 @@ pub mod ai_classification;
|
|||||||
pub mod video_classification;
|
pub mod video_classification;
|
||||||
pub mod template;
|
pub mod template;
|
||||||
pub mod project_template_binding;
|
pub mod project_template_binding;
|
||||||
|
pub mod template_matching_result;
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
/// 模板匹配结果实体模型
|
||||||
|
/// 遵循 Tauri 开发规范的数据模型设计原则
|
||||||
|
/// 保存一次完整的模板匹配结果,包含所有匹配的片段信息
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TemplateMatchingResult {
|
||||||
|
pub id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub template_id: String,
|
||||||
|
pub binding_id: String,
|
||||||
|
pub result_name: String, // 用户自定义的结果名称
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub total_segments: u32,
|
||||||
|
pub matched_segments: u32,
|
||||||
|
pub failed_segments: u32,
|
||||||
|
pub success_rate: f64,
|
||||||
|
pub used_materials: u32,
|
||||||
|
pub used_models: u32,
|
||||||
|
pub matching_duration_ms: u64, // 匹配耗时(毫秒)
|
||||||
|
pub quality_score: Option<f64>, // 匹配质量评分
|
||||||
|
pub status: MatchingResultStatus,
|
||||||
|
pub metadata: Option<String>, // JSON格式的额外元数据
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub is_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 匹配结果状态
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum MatchingResultStatus {
|
||||||
|
/// 匹配成功
|
||||||
|
Success,
|
||||||
|
/// 部分匹配成功
|
||||||
|
PartialSuccess,
|
||||||
|
/// 匹配失败
|
||||||
|
Failed,
|
||||||
|
/// 匹配被取消
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MatchingResultStatus {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 匹配片段结果实体模型
|
||||||
|
/// 保存每个成功匹配的片段详细信息
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MatchingSegmentResult {
|
||||||
|
pub id: String,
|
||||||
|
pub matching_result_id: String, // 关联的匹配结果ID
|
||||||
|
pub track_segment_id: String, // 模板轨道片段ID
|
||||||
|
pub track_segment_name: String,
|
||||||
|
pub material_segment_id: String, // 匹配到的素材片段ID
|
||||||
|
pub material_id: String, // 素材ID
|
||||||
|
pub material_name: String,
|
||||||
|
pub model_id: Option<String>, // 模特ID
|
||||||
|
pub model_name: Option<String>,
|
||||||
|
pub match_score: f64, // 匹配评分
|
||||||
|
pub match_reason: String, // 匹配原因
|
||||||
|
pub segment_duration: u64, // 片段时长(微秒)
|
||||||
|
pub start_time: u64, // 在模板中的开始时间(微秒)
|
||||||
|
pub end_time: u64, // 在模板中的结束时间(微秒)
|
||||||
|
pub properties: Option<String>, // JSON格式的片段属性
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 匹配失败片段结果实体模型
|
||||||
|
/// 保存匹配失败的片段信息,用于分析和改进
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MatchingFailedSegmentResult {
|
||||||
|
pub id: String,
|
||||||
|
pub matching_result_id: String, // 关联的匹配结果ID
|
||||||
|
pub track_segment_id: String, // 模板轨道片段ID
|
||||||
|
pub track_segment_name: String,
|
||||||
|
pub matching_rule_type: String, // 匹配规则类型
|
||||||
|
pub matching_rule_data: Option<String>, // 匹配规则数据(JSON格式)
|
||||||
|
pub failure_reason: String, // 失败原因
|
||||||
|
pub failure_details: Option<String>, // 失败详情(JSON格式)
|
||||||
|
pub segment_duration: u64, // 片段时长(微秒)
|
||||||
|
pub start_time: u64, // 在模板中的开始时间(微秒)
|
||||||
|
pub end_time: u64, // 在模板中的结束时间(微秒)
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateMatchingResult {
|
||||||
|
/// 创建新的模板匹配结果实例
|
||||||
|
pub fn new(
|
||||||
|
id: String,
|
||||||
|
project_id: String,
|
||||||
|
template_id: String,
|
||||||
|
binding_id: String,
|
||||||
|
result_name: String,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
project_id,
|
||||||
|
template_id,
|
||||||
|
binding_id,
|
||||||
|
result_name,
|
||||||
|
description: None,
|
||||||
|
total_segments: 0,
|
||||||
|
matched_segments: 0,
|
||||||
|
failed_segments: 0,
|
||||||
|
success_rate: 0.0,
|
||||||
|
used_materials: 0,
|
||||||
|
used_models: 0,
|
||||||
|
matching_duration_ms: 0,
|
||||||
|
quality_score: None,
|
||||||
|
status: MatchingResultStatus::default(),
|
||||||
|
metadata: None,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
is_active: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新匹配统计信息
|
||||||
|
pub fn update_statistics(
|
||||||
|
&mut self,
|
||||||
|
total_segments: u32,
|
||||||
|
matched_segments: u32,
|
||||||
|
failed_segments: u32,
|
||||||
|
used_materials: u32,
|
||||||
|
used_models: u32,
|
||||||
|
matching_duration_ms: u64,
|
||||||
|
) {
|
||||||
|
self.total_segments = total_segments;
|
||||||
|
self.matched_segments = matched_segments;
|
||||||
|
self.failed_segments = failed_segments;
|
||||||
|
self.success_rate = if total_segments > 0 {
|
||||||
|
(matched_segments as f64) / (total_segments as f64) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
self.used_materials = used_materials;
|
||||||
|
self.used_models = used_models;
|
||||||
|
self.matching_duration_ms = matching_duration_ms;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
|
||||||
|
// 根据成功率设置状态
|
||||||
|
self.status = if self.success_rate >= 100.0 {
|
||||||
|
MatchingResultStatus::Success
|
||||||
|
} else if self.success_rate > 0.0 {
|
||||||
|
MatchingResultStatus::PartialSuccess
|
||||||
|
} else {
|
||||||
|
MatchingResultStatus::Failed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置质量评分
|
||||||
|
pub fn set_quality_score(&mut self, score: f64) {
|
||||||
|
self.quality_score = Some(score);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置描述
|
||||||
|
pub fn set_description(&mut self, description: String) {
|
||||||
|
self.description = Some(description);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置元数据
|
||||||
|
pub fn set_metadata(&mut self, metadata: String) {
|
||||||
|
self.metadata = Some(metadata);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否为成功的匹配结果
|
||||||
|
pub fn is_successful(&self) -> bool {
|
||||||
|
matches!(self.status, MatchingResultStatus::Success | MatchingResultStatus::PartialSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取状态显示名称
|
||||||
|
pub fn status_display_name(&self) -> String {
|
||||||
|
match self.status {
|
||||||
|
MatchingResultStatus::Success => "匹配成功".to_string(),
|
||||||
|
MatchingResultStatus::PartialSuccess => "部分成功".to_string(),
|
||||||
|
MatchingResultStatus::Failed => "匹配失败".to_string(),
|
||||||
|
MatchingResultStatus::Cancelled => "已取消".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatchingSegmentResult {
|
||||||
|
/// 创建新的匹配片段结果实例
|
||||||
|
pub fn new(
|
||||||
|
id: String,
|
||||||
|
matching_result_id: String,
|
||||||
|
track_segment_id: String,
|
||||||
|
track_segment_name: String,
|
||||||
|
material_segment_id: String,
|
||||||
|
material_id: String,
|
||||||
|
material_name: String,
|
||||||
|
match_score: f64,
|
||||||
|
match_reason: String,
|
||||||
|
segment_duration: u64,
|
||||||
|
start_time: u64,
|
||||||
|
end_time: u64,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
matching_result_id,
|
||||||
|
track_segment_id,
|
||||||
|
track_segment_name,
|
||||||
|
material_segment_id,
|
||||||
|
material_id,
|
||||||
|
material_name,
|
||||||
|
model_id: None,
|
||||||
|
model_name: None,
|
||||||
|
match_score,
|
||||||
|
match_reason,
|
||||||
|
segment_duration,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
properties: None,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置模特信息
|
||||||
|
pub fn set_model_info(&mut self, model_id: String, model_name: String) {
|
||||||
|
self.model_id = Some(model_id);
|
||||||
|
self.model_name = Some(model_name);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置片段属性
|
||||||
|
pub fn set_properties(&mut self, properties: String) {
|
||||||
|
self.properties = Some(properties);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatchingFailedSegmentResult {
|
||||||
|
/// 创建新的匹配失败片段结果实例
|
||||||
|
pub fn new(
|
||||||
|
id: String,
|
||||||
|
matching_result_id: String,
|
||||||
|
track_segment_id: String,
|
||||||
|
track_segment_name: String,
|
||||||
|
matching_rule_type: String,
|
||||||
|
failure_reason: String,
|
||||||
|
segment_duration: u64,
|
||||||
|
start_time: u64,
|
||||||
|
end_time: u64,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
matching_result_id,
|
||||||
|
track_segment_id,
|
||||||
|
track_segment_name,
|
||||||
|
matching_rule_type,
|
||||||
|
matching_rule_data: None,
|
||||||
|
failure_reason,
|
||||||
|
failure_details: None,
|
||||||
|
segment_duration,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置匹配规则数据
|
||||||
|
pub fn set_matching_rule_data(&mut self, rule_data: String) {
|
||||||
|
self.matching_rule_data = Some(rule_data);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置失败详情
|
||||||
|
pub fn set_failure_details(&mut self, details: String) {
|
||||||
|
self.failure_details = Some(details);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建模板匹配结果请求
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateTemplateMatchingResultRequest {
|
||||||
|
pub project_id: String,
|
||||||
|
pub template_id: String,
|
||||||
|
pub binding_id: String,
|
||||||
|
pub result_name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模板匹配结果查询选项
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct TemplateMatchingResultQueryOptions {
|
||||||
|
pub project_id: Option<String>,
|
||||||
|
pub template_id: Option<String>,
|
||||||
|
pub binding_id: Option<String>,
|
||||||
|
pub status: Option<MatchingResultStatus>,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
pub search_keyword: Option<String>,
|
||||||
|
pub sort_by: Option<String>, // "created_at", "success_rate", "result_name"
|
||||||
|
pub sort_order: Option<String>, // "asc", "desc"
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ pub mod model_repository;
|
|||||||
pub mod ai_classification_repository;
|
pub mod ai_classification_repository;
|
||||||
pub mod video_classification_repository;
|
pub mod video_classification_repository;
|
||||||
pub mod project_template_binding_repository;
|
pub mod project_template_binding_repository;
|
||||||
|
pub mod template_matching_result_repository;
|
||||||
|
|||||||
@@ -0,0 +1,558 @@
|
|||||||
|
use rusqlite::{Result, Row};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use crate::infrastructure::database::Database;
|
||||||
|
|
||||||
|
use crate::data::models::template_matching_result::{
|
||||||
|
TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult,
|
||||||
|
MatchingResultStatus, CreateTemplateMatchingResultRequest,
|
||||||
|
TemplateMatchingResultQueryOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 模板匹配结果数据库操作仓储
|
||||||
|
/// 遵循 Tauri 开发规范的数据访问层设计原则
|
||||||
|
pub struct TemplateMatchingResultRepository {
|
||||||
|
database: Arc<Database>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateMatchingResultRepository {
|
||||||
|
/// 创建新的模板匹配结果仓储实例
|
||||||
|
pub fn new(database: Arc<Database>) -> Self {
|
||||||
|
Self { database }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建模板匹配结果
|
||||||
|
pub fn create(&self, request: CreateTemplateMatchingResultRequest) -> Result<TemplateMatchingResult> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let result = TemplateMatchingResult::new(
|
||||||
|
uuid::Uuid::new_v4().to_string(),
|
||||||
|
request.project_id,
|
||||||
|
request.template_id,
|
||||||
|
request.binding_id,
|
||||||
|
request.result_name,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut final_result = result;
|
||||||
|
if let Some(desc) = request.description {
|
||||||
|
final_result.set_description(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO template_matching_results (
|
||||||
|
id, project_id, template_id, binding_id, result_name, description,
|
||||||
|
total_segments, matched_segments, failed_segments, success_rate,
|
||||||
|
used_materials, used_models, matching_duration_ms, quality_score,
|
||||||
|
status, metadata, created_at, updated_at, is_active
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)",
|
||||||
|
rusqlite::params![
|
||||||
|
&final_result.id,
|
||||||
|
&final_result.project_id,
|
||||||
|
&final_result.template_id,
|
||||||
|
&final_result.binding_id,
|
||||||
|
&final_result.result_name,
|
||||||
|
&final_result.description,
|
||||||
|
&final_result.total_segments.to_string(),
|
||||||
|
&final_result.matched_segments.to_string(),
|
||||||
|
&final_result.failed_segments.to_string(),
|
||||||
|
&final_result.success_rate.to_string(),
|
||||||
|
&final_result.used_materials.to_string(),
|
||||||
|
&final_result.used_models.to_string(),
|
||||||
|
&final_result.matching_duration_ms.to_string(),
|
||||||
|
&final_result.quality_score.map(|s| s.to_string()),
|
||||||
|
&serde_json::to_string(&final_result.status).unwrap(),
|
||||||
|
&final_result.metadata,
|
||||||
|
&final_result.created_at.to_rfc3339(),
|
||||||
|
&final_result.updated_at.to_rfc3339(),
|
||||||
|
&(final_result.is_active as i32).to_string(),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(final_result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据ID获取模板匹配结果
|
||||||
|
pub fn get_by_id(&self, id: &str) -> Result<Option<TemplateMatchingResult>> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT id, project_id, template_id, binding_id, result_name, description,
|
||||||
|
total_segments, matched_segments, failed_segments, success_rate,
|
||||||
|
used_materials, used_models, matching_duration_ms, quality_score,
|
||||||
|
status, metadata, created_at, updated_at, is_active
|
||||||
|
FROM template_matching_results WHERE id = ?1"
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let result_iter = stmt.query_map([id], |row| {
|
||||||
|
self.row_to_matching_result(row)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for result in result_iter {
|
||||||
|
return Ok(Some(result?));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新模板匹配结果
|
||||||
|
pub fn update(&self, result: &TemplateMatchingResult) -> Result<()> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE template_matching_results SET
|
||||||
|
result_name = ?1, description = ?2, total_segments = ?3,
|
||||||
|
matched_segments = ?4, failed_segments = ?5, success_rate = ?6,
|
||||||
|
used_materials = ?7, used_models = ?8, matching_duration_ms = ?9,
|
||||||
|
quality_score = ?10, status = ?11, metadata = ?12, updated_at = ?13
|
||||||
|
WHERE id = ?14",
|
||||||
|
rusqlite::params![
|
||||||
|
&result.result_name,
|
||||||
|
&result.description,
|
||||||
|
&result.total_segments.to_string(),
|
||||||
|
&result.matched_segments.to_string(),
|
||||||
|
&result.failed_segments.to_string(),
|
||||||
|
&result.success_rate.to_string(),
|
||||||
|
&result.used_materials.to_string(),
|
||||||
|
&result.used_models.to_string(),
|
||||||
|
&result.matching_duration_ms.to_string(),
|
||||||
|
&result.quality_score.map(|s| s.to_string()),
|
||||||
|
&serde_json::to_string(&result.status).unwrap(),
|
||||||
|
&result.metadata,
|
||||||
|
&result.updated_at.to_rfc3339(),
|
||||||
|
&result.id,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除模板匹配结果
|
||||||
|
pub fn delete(&self, id: &str) -> Result<bool> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let rows_affected = conn.execute(
|
||||||
|
"DELETE FROM template_matching_results WHERE id = ?1",
|
||||||
|
[id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(rows_affected > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 软删除模板匹配结果
|
||||||
|
pub fn soft_delete(&self, id: &str) -> Result<bool> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let rows_affected = conn.execute(
|
||||||
|
"UPDATE template_matching_results SET is_active = 0, updated_at = ?1 WHERE id = ?2",
|
||||||
|
[&Utc::now().to_rfc3339(), id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(rows_affected > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询模板匹配结果列表
|
||||||
|
pub fn list(&self, options: TemplateMatchingResultQueryOptions) -> Result<Vec<TemplateMatchingResult>> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let mut query = "SELECT id, project_id, template_id, binding_id, result_name, description,
|
||||||
|
total_segments, matched_segments, failed_segments, success_rate,
|
||||||
|
used_materials, used_models, matching_duration_ms, quality_score,
|
||||||
|
status, metadata, created_at, updated_at, is_active
|
||||||
|
FROM template_matching_results WHERE is_active = 1".to_string();
|
||||||
|
let mut params: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
// 添加查询条件
|
||||||
|
if let Some(project_id) = &options.project_id {
|
||||||
|
query.push_str(" AND project_id = ?");
|
||||||
|
params.push(project_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(template_id) = &options.template_id {
|
||||||
|
query.push_str(" AND template_id = ?");
|
||||||
|
params.push(template_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(binding_id) = &options.binding_id {
|
||||||
|
query.push_str(" AND binding_id = ?");
|
||||||
|
params.push(binding_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(status) = &options.status {
|
||||||
|
query.push_str(" AND status = ?");
|
||||||
|
params.push(serde_json::to_string(status).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(keyword) = &options.search_keyword {
|
||||||
|
query.push_str(" AND (result_name LIKE ? OR description LIKE ?)");
|
||||||
|
let search_pattern = format!("%{}%", keyword);
|
||||||
|
params.push(search_pattern.clone());
|
||||||
|
params.push(search_pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加排序
|
||||||
|
let sort_by = options.sort_by.as_deref().unwrap_or("created_at");
|
||||||
|
let sort_order = options.sort_order.as_deref().unwrap_or("desc");
|
||||||
|
query.push_str(&format!(" ORDER BY {} {}", sort_by, sort_order));
|
||||||
|
|
||||||
|
// 添加分页
|
||||||
|
if let Some(limit) = options.limit {
|
||||||
|
query.push_str(" LIMIT ?");
|
||||||
|
params.push(limit.to_string());
|
||||||
|
|
||||||
|
if let Some(offset) = options.offset {
|
||||||
|
query.push_str(" OFFSET ?");
|
||||||
|
params.push(offset.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(&query)?;
|
||||||
|
let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
|
||||||
|
|
||||||
|
let result_iter = stmt.query_map(param_refs.as_slice(), |row| {
|
||||||
|
self.row_to_matching_result(row)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for result in result_iter {
|
||||||
|
results.push(result?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据项目ID获取匹配结果列表
|
||||||
|
pub fn get_by_project_id(&self, project_id: &str) -> Result<Vec<TemplateMatchingResult>> {
|
||||||
|
let options = TemplateMatchingResultQueryOptions {
|
||||||
|
project_id: Some(project_id.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
self.list(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据模板ID获取匹配结果列表
|
||||||
|
pub fn get_by_template_id(&self, template_id: &str) -> Result<Vec<TemplateMatchingResult>> {
|
||||||
|
let options = TemplateMatchingResultQueryOptions {
|
||||||
|
template_id: Some(template_id.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
self.list(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据绑定ID获取匹配结果列表
|
||||||
|
pub fn get_by_binding_id(&self, binding_id: &str) -> Result<Vec<TemplateMatchingResult>> {
|
||||||
|
let options = TemplateMatchingResultQueryOptions {
|
||||||
|
binding_id: Some(binding_id.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
self.list(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 匹配片段结果管理 =====
|
||||||
|
|
||||||
|
/// 创建匹配片段结果
|
||||||
|
pub fn create_segment_result(&self, segment_result: &MatchingSegmentResult) -> Result<()> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO matching_segment_results (
|
||||||
|
id, matching_result_id, track_segment_id, track_segment_name,
|
||||||
|
material_segment_id, material_id, material_name, model_id, model_name,
|
||||||
|
match_score, match_reason, segment_duration, start_time, end_time,
|
||||||
|
properties, created_at, updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
|
||||||
|
rusqlite::params![
|
||||||
|
&segment_result.id,
|
||||||
|
&segment_result.matching_result_id,
|
||||||
|
&segment_result.track_segment_id,
|
||||||
|
&segment_result.track_segment_name,
|
||||||
|
&segment_result.material_segment_id,
|
||||||
|
&segment_result.material_id,
|
||||||
|
&segment_result.material_name,
|
||||||
|
&segment_result.model_id,
|
||||||
|
&segment_result.model_name,
|
||||||
|
&segment_result.match_score.to_string(),
|
||||||
|
&segment_result.match_reason,
|
||||||
|
&segment_result.segment_duration.to_string(),
|
||||||
|
&segment_result.start_time.to_string(),
|
||||||
|
&segment_result.end_time.to_string(),
|
||||||
|
&segment_result.properties,
|
||||||
|
&segment_result.created_at.to_rfc3339(),
|
||||||
|
&segment_result.updated_at.to_rfc3339(),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量创建匹配片段结果
|
||||||
|
pub fn create_segment_results(&self, segment_results: &[MatchingSegmentResult]) -> Result<()> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let tx = conn.unchecked_transaction()?;
|
||||||
|
|
||||||
|
for segment_result in segment_results {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO matching_segment_results (
|
||||||
|
id, matching_result_id, track_segment_id, track_segment_name,
|
||||||
|
material_segment_id, material_id, material_name, model_id, model_name,
|
||||||
|
match_score, match_reason, segment_duration, start_time, end_time,
|
||||||
|
properties, created_at, updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
|
||||||
|
rusqlite::params![
|
||||||
|
&segment_result.id,
|
||||||
|
&segment_result.matching_result_id,
|
||||||
|
&segment_result.track_segment_id,
|
||||||
|
&segment_result.track_segment_name,
|
||||||
|
&segment_result.material_segment_id,
|
||||||
|
&segment_result.material_id,
|
||||||
|
&segment_result.material_name,
|
||||||
|
&segment_result.model_id,
|
||||||
|
&segment_result.model_name,
|
||||||
|
&segment_result.match_score.to_string(),
|
||||||
|
&segment_result.match_reason,
|
||||||
|
&segment_result.segment_duration.to_string(),
|
||||||
|
&segment_result.start_time.to_string(),
|
||||||
|
&segment_result.end_time.to_string(),
|
||||||
|
&segment_result.properties,
|
||||||
|
&segment_result.created_at.to_rfc3339(),
|
||||||
|
&segment_result.updated_at.to_rfc3339(),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取匹配结果的所有成功片段
|
||||||
|
pub fn get_segment_results_by_matching_result_id(&self, matching_result_id: &str) -> Result<Vec<MatchingSegmentResult>> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT id, matching_result_id, track_segment_id, track_segment_name,
|
||||||
|
material_segment_id, material_id, material_name, model_id, model_name,
|
||||||
|
match_score, match_reason, segment_duration, start_time, end_time,
|
||||||
|
properties, created_at, updated_at
|
||||||
|
FROM matching_segment_results WHERE matching_result_id = ?1
|
||||||
|
ORDER BY start_time ASC"
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let segment_iter = stmt.query_map([matching_result_id], |row| {
|
||||||
|
self.row_to_segment_result(row)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut segments = Vec::new();
|
||||||
|
for segment in segment_iter {
|
||||||
|
segments.push(segment?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 匹配失败片段结果管理 =====
|
||||||
|
|
||||||
|
/// 创建匹配失败片段结果
|
||||||
|
pub fn create_failed_segment_result(&self, failed_segment: &MatchingFailedSegmentResult) -> Result<()> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO matching_failed_segment_results (
|
||||||
|
id, matching_result_id, track_segment_id, track_segment_name,
|
||||||
|
matching_rule_type, matching_rule_data, failure_reason, failure_details,
|
||||||
|
segment_duration, start_time, end_time, created_at, updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||||
|
rusqlite::params![
|
||||||
|
&failed_segment.id,
|
||||||
|
&failed_segment.matching_result_id,
|
||||||
|
&failed_segment.track_segment_id,
|
||||||
|
&failed_segment.track_segment_name,
|
||||||
|
&failed_segment.matching_rule_type,
|
||||||
|
&failed_segment.matching_rule_data,
|
||||||
|
&failed_segment.failure_reason,
|
||||||
|
&failed_segment.failure_details,
|
||||||
|
&failed_segment.segment_duration.to_string(),
|
||||||
|
&failed_segment.start_time.to_string(),
|
||||||
|
&failed_segment.end_time.to_string(),
|
||||||
|
&failed_segment.created_at.to_rfc3339(),
|
||||||
|
&failed_segment.updated_at.to_rfc3339(),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量创建匹配失败片段结果
|
||||||
|
pub fn create_failed_segment_results(&self, failed_segments: &[MatchingFailedSegmentResult]) -> Result<()> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let tx = conn.unchecked_transaction()?;
|
||||||
|
|
||||||
|
for failed_segment in failed_segments {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO matching_failed_segment_results (
|
||||||
|
id, matching_result_id, track_segment_id, track_segment_name,
|
||||||
|
matching_rule_type, matching_rule_data, failure_reason, failure_details,
|
||||||
|
segment_duration, start_time, end_time, created_at, updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||||
|
rusqlite::params![
|
||||||
|
&failed_segment.id,
|
||||||
|
&failed_segment.matching_result_id,
|
||||||
|
&failed_segment.track_segment_id,
|
||||||
|
&failed_segment.track_segment_name,
|
||||||
|
&failed_segment.matching_rule_type,
|
||||||
|
&failed_segment.matching_rule_data,
|
||||||
|
&failed_segment.failure_reason,
|
||||||
|
&failed_segment.failure_details,
|
||||||
|
&failed_segment.segment_duration.to_string(),
|
||||||
|
&failed_segment.start_time.to_string(),
|
||||||
|
&failed_segment.end_time.to_string(),
|
||||||
|
&failed_segment.created_at.to_rfc3339(),
|
||||||
|
&failed_segment.updated_at.to_rfc3339(),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取匹配结果的所有失败片段
|
||||||
|
pub fn get_failed_segment_results_by_matching_result_id(&self, matching_result_id: &str) -> Result<Vec<MatchingFailedSegmentResult>> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT id, matching_result_id, track_segment_id, track_segment_name,
|
||||||
|
matching_rule_type, matching_rule_data, failure_reason, failure_details,
|
||||||
|
segment_duration, start_time, end_time, created_at, updated_at
|
||||||
|
FROM matching_failed_segment_results WHERE matching_result_id = ?1
|
||||||
|
ORDER BY start_time ASC"
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let failed_segment_iter = stmt.query_map([matching_result_id], |row| {
|
||||||
|
self.row_to_failed_segment_result(row)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut failed_segments = Vec::new();
|
||||||
|
for failed_segment in failed_segment_iter {
|
||||||
|
failed_segments.push(failed_segment?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(failed_segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将数据库行转换为模板匹配结果实体
|
||||||
|
fn row_to_matching_result(&self, row: &Row) -> Result<TemplateMatchingResult> {
|
||||||
|
let status_str: String = row.get("status")?;
|
||||||
|
let status: MatchingResultStatus = serde_json::from_str(&status_str)
|
||||||
|
.unwrap_or(MatchingResultStatus::Success);
|
||||||
|
|
||||||
|
let quality_score_str: Option<String> = row.get("quality_score")?;
|
||||||
|
let quality_score = quality_score_str.and_then(|s| s.parse::<f64>().ok());
|
||||||
|
|
||||||
|
let created_at_str: String = row.get("created_at")?;
|
||||||
|
let updated_at_str: String = row.get("updated_at")?;
|
||||||
|
let created_at = DateTime::parse_from_rfc3339(&created_at_str)
|
||||||
|
.unwrap_or_else(|_| Utc::now().into())
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)
|
||||||
|
.unwrap_or_else(|_| Utc::now().into())
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
|
||||||
|
let is_active_str: String = row.get("is_active")?;
|
||||||
|
let is_active = is_active_str.parse::<i32>().unwrap_or(1) != 0;
|
||||||
|
|
||||||
|
Ok(TemplateMatchingResult {
|
||||||
|
id: row.get("id")?,
|
||||||
|
project_id: row.get("project_id")?,
|
||||||
|
template_id: row.get("template_id")?,
|
||||||
|
binding_id: row.get("binding_id")?,
|
||||||
|
result_name: row.get("result_name")?,
|
||||||
|
description: row.get("description")?,
|
||||||
|
total_segments: row.get::<_, String>("total_segments")?.parse().unwrap_or(0),
|
||||||
|
matched_segments: row.get::<_, String>("matched_segments")?.parse().unwrap_or(0),
|
||||||
|
failed_segments: row.get::<_, String>("failed_segments")?.parse().unwrap_or(0),
|
||||||
|
success_rate: row.get::<_, String>("success_rate")?.parse().unwrap_or(0.0),
|
||||||
|
used_materials: row.get::<_, String>("used_materials")?.parse().unwrap_or(0),
|
||||||
|
used_models: row.get::<_, String>("used_models")?.parse().unwrap_or(0),
|
||||||
|
matching_duration_ms: row.get::<_, String>("matching_duration_ms")?.parse().unwrap_or(0),
|
||||||
|
quality_score,
|
||||||
|
status,
|
||||||
|
metadata: row.get("metadata")?,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
is_active,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将数据库行转换为匹配片段结果实体
|
||||||
|
fn row_to_segment_result(&self, row: &Row) -> Result<MatchingSegmentResult> {
|
||||||
|
let created_at_str: String = row.get("created_at")?;
|
||||||
|
let updated_at_str: String = row.get("updated_at")?;
|
||||||
|
let created_at = DateTime::parse_from_rfc3339(&created_at_str)
|
||||||
|
.unwrap_or_else(|_| Utc::now().into())
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)
|
||||||
|
.unwrap_or_else(|_| Utc::now().into())
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
|
||||||
|
Ok(MatchingSegmentResult {
|
||||||
|
id: row.get("id")?,
|
||||||
|
matching_result_id: row.get("matching_result_id")?,
|
||||||
|
track_segment_id: row.get("track_segment_id")?,
|
||||||
|
track_segment_name: row.get("track_segment_name")?,
|
||||||
|
material_segment_id: row.get("material_segment_id")?,
|
||||||
|
material_id: row.get("material_id")?,
|
||||||
|
material_name: row.get("material_name")?,
|
||||||
|
model_id: row.get("model_id")?,
|
||||||
|
model_name: row.get("model_name")?,
|
||||||
|
match_score: row.get::<_, String>("match_score")?.parse().unwrap_or(0.0),
|
||||||
|
match_reason: row.get("match_reason")?,
|
||||||
|
segment_duration: row.get::<_, String>("segment_duration")?.parse().unwrap_or(0),
|
||||||
|
start_time: row.get::<_, String>("start_time")?.parse().unwrap_or(0),
|
||||||
|
end_time: row.get::<_, String>("end_time")?.parse().unwrap_or(0),
|
||||||
|
properties: row.get("properties")?,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将数据库行转换为匹配失败片段结果实体
|
||||||
|
fn row_to_failed_segment_result(&self, row: &Row) -> Result<MatchingFailedSegmentResult> {
|
||||||
|
let created_at_str: String = row.get("created_at")?;
|
||||||
|
let updated_at_str: String = row.get("updated_at")?;
|
||||||
|
let created_at = DateTime::parse_from_rfc3339(&created_at_str)
|
||||||
|
.unwrap_or_else(|_| Utc::now().into())
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)
|
||||||
|
.unwrap_or_else(|_| Utc::now().into())
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
|
||||||
|
Ok(MatchingFailedSegmentResult {
|
||||||
|
id: row.get("id")?,
|
||||||
|
matching_result_id: row.get("matching_result_id")?,
|
||||||
|
track_segment_id: row.get("track_segment_id")?,
|
||||||
|
track_segment_name: row.get("track_segment_name")?,
|
||||||
|
matching_rule_type: row.get("matching_rule_type")?,
|
||||||
|
matching_rule_data: row.get("matching_rule_data")?,
|
||||||
|
failure_reason: row.get("failure_reason")?,
|
||||||
|
failure_details: row.get("failure_details")?,
|
||||||
|
segment_duration: row.get::<_, String>("segment_duration")?.parse().unwrap_or(0),
|
||||||
|
start_time: row.get::<_, String>("start_time")?.parse().unwrap_or(0),
|
||||||
|
end_time: row.get::<_, String>("end_time")?.parse().unwrap_or(0),
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -671,7 +671,83 @@ impl Database {
|
|||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// 创建模板匹配结果表
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS template_matching_results (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
template_id TEXT NOT NULL,
|
||||||
|
binding_id TEXT NOT NULL,
|
||||||
|
result_name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
total_segments INTEGER NOT NULL DEFAULT 0,
|
||||||
|
matched_segments INTEGER NOT NULL DEFAULT 0,
|
||||||
|
failed_segments INTEGER NOT NULL DEFAULT 0,
|
||||||
|
success_rate REAL NOT NULL DEFAULT 0.0,
|
||||||
|
used_materials INTEGER NOT NULL DEFAULT 0,
|
||||||
|
used_models INTEGER NOT NULL DEFAULT 0,
|
||||||
|
matching_duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||||
|
quality_score REAL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'Success',
|
||||||
|
metadata TEXT,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
is_active INTEGER DEFAULT 1,
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (template_id) REFERENCES templates (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (binding_id) REFERENCES project_template_bindings (id) ON DELETE CASCADE
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 创建匹配片段结果表
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS matching_segment_results (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
matching_result_id TEXT NOT NULL,
|
||||||
|
track_segment_id TEXT NOT NULL,
|
||||||
|
track_segment_name TEXT NOT NULL,
|
||||||
|
material_segment_id TEXT NOT NULL,
|
||||||
|
material_id TEXT NOT NULL,
|
||||||
|
material_name TEXT NOT NULL,
|
||||||
|
model_id TEXT,
|
||||||
|
model_name TEXT,
|
||||||
|
match_score REAL NOT NULL,
|
||||||
|
match_reason TEXT NOT NULL,
|
||||||
|
segment_duration INTEGER NOT NULL,
|
||||||
|
start_time INTEGER NOT NULL,
|
||||||
|
end_time INTEGER NOT NULL,
|
||||||
|
properties TEXT,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
FOREIGN KEY (matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (material_segment_id) REFERENCES material_segments (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (model_id) REFERENCES models (id) ON DELETE SET NULL
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 创建匹配失败片段结果表
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS matching_failed_segment_results (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
matching_result_id TEXT NOT NULL,
|
||||||
|
track_segment_id TEXT NOT NULL,
|
||||||
|
track_segment_name TEXT NOT NULL,
|
||||||
|
matching_rule_type TEXT NOT NULL,
|
||||||
|
matching_rule_data TEXT,
|
||||||
|
failure_reason TEXT NOT NULL,
|
||||||
|
failure_details TEXT,
|
||||||
|
segment_duration INTEGER NOT NULL,
|
||||||
|
start_time INTEGER NOT NULL,
|
||||||
|
end_time INTEGER NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
FOREIGN KEY (matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
// 创建性能监控表
|
// 创建性能监控表
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -748,7 +824,53 @@ impl Database {
|
|||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// 创建模板匹配结果表索引
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_project_id ON template_matching_results (project_id)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_template_id ON template_matching_results (template_id)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_binding_id ON template_matching_results (binding_id)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_status ON template_matching_results (status)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_created_at ON template_matching_results (created_at)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 创建匹配片段结果表索引
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_matching_segment_results_matching_result_id ON matching_segment_results (matching_result_id)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_matching_segment_results_material_id ON matching_segment_results (material_id)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_matching_segment_results_model_id ON matching_segment_results (model_id)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 创建匹配失败片段结果表索引
|
||||||
|
conn.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_matching_failed_segment_results_matching_result_id ON matching_failed_segment_results (matching_result_id)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
// 添加新字段(如果不存在)- 数据库迁移
|
// 添加新字段(如果不存在)- 数据库迁移
|
||||||
let _ = conn.execute(
|
let _ = conn.execute(
|
||||||
|
|||||||
@@ -191,8 +191,24 @@ pub fn run() {
|
|||||||
commands::project_template_binding_commands::check_project_template_binding_exists,
|
commands::project_template_binding_commands::check_project_template_binding_exists,
|
||||||
// 素材匹配命令
|
// 素材匹配命令
|
||||||
commands::material_matching_commands::execute_material_matching,
|
commands::material_matching_commands::execute_material_matching,
|
||||||
|
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::template_matching_result_commands::save_matching_result,
|
||||||
|
commands::template_matching_result_commands::get_matching_result_detail,
|
||||||
|
commands::template_matching_result_commands::get_project_matching_results,
|
||||||
|
commands::template_matching_result_commands::get_template_matching_results,
|
||||||
|
commands::template_matching_result_commands::get_binding_matching_results,
|
||||||
|
commands::template_matching_result_commands::list_matching_results,
|
||||||
|
commands::template_matching_result_commands::delete_matching_result,
|
||||||
|
commands::template_matching_result_commands::soft_delete_matching_result,
|
||||||
|
commands::template_matching_result_commands::update_matching_result_info,
|
||||||
|
commands::template_matching_result_commands::set_matching_result_quality_score,
|
||||||
|
commands::template_matching_result_commands::get_matching_statistics,
|
||||||
|
commands::template_matching_result_commands::create_matching_result,
|
||||||
|
commands::template_matching_result_commands::get_matching_result_by_id,
|
||||||
|
commands::template_matching_result_commands::get_matching_result_status_options,
|
||||||
// MaterialSegment聚合视图命令
|
// MaterialSegment聚合视图命令
|
||||||
commands::material_segment_view_commands::get_project_segment_view,
|
commands::material_segment_view_commands::get_project_segment_view,
|
||||||
commands::material_segment_view_commands::get_project_segment_view_with_query,
|
commands::material_segment_view_commands::get_project_segment_view_with_query,
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ use crate::business::services::material_matching_service::{
|
|||||||
MaterialMatchingService, MaterialMatchingRequest, MaterialMatchingResult
|
MaterialMatchingService, MaterialMatchingRequest, MaterialMatchingResult
|
||||||
};
|
};
|
||||||
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::data::repositories::{
|
use crate::data::repositories::{
|
||||||
material_repository::MaterialRepository,
|
material_repository::MaterialRepository,
|
||||||
video_classification_repository::VideoClassificationRepository,
|
video_classification_repository::VideoClassificationRepository,
|
||||||
|
template_matching_result_repository::TemplateMatchingResultRepository,
|
||||||
};
|
};
|
||||||
|
use crate::data::models::template_matching_result::TemplateMatchingResult;
|
||||||
use crate::infrastructure::database::Database;
|
use crate::infrastructure::database::Database;
|
||||||
|
|
||||||
/// 执行素材匹配
|
/// 执行素材匹配
|
||||||
@@ -46,6 +49,43 @@ pub async fn execute_material_matching(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 执行素材匹配并自动保存结果
|
||||||
|
#[command]
|
||||||
|
pub async fn execute_material_matching_with_save(
|
||||||
|
request: MaterialMatchingRequest,
|
||||||
|
result_name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<(MaterialMatchingResult, Option<TemplateMatchingResult>), String> {
|
||||||
|
// 创建服务实例
|
||||||
|
let material_repo = Arc::new(
|
||||||
|
MaterialRepository::new(database.inner().clone())
|
||||||
|
.map_err(|e| format!("创建素材仓库失败: {}", e))?
|
||||||
|
);
|
||||||
|
|
||||||
|
let template_service = Arc::new(TemplateService::new(database.inner().clone()));
|
||||||
|
|
||||||
|
let video_classification_repo = Arc::new(
|
||||||
|
VideoClassificationRepository::new(database.inner().clone())
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建匹配结果服务
|
||||||
|
let matching_result_repo = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let matching_result_service = Arc::new(TemplateMatchingResultService::new(matching_result_repo));
|
||||||
|
|
||||||
|
let matching_service = MaterialMatchingService::new_with_result_service(
|
||||||
|
material_repo,
|
||||||
|
template_service,
|
||||||
|
video_classification_repo,
|
||||||
|
matching_result_service,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 执行匹配并保存结果
|
||||||
|
matching_service.match_materials_and_save(request, result_name, description)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取项目的可用素材统计信息
|
/// 获取项目的可用素材统计信息
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn get_project_material_stats_for_matching(
|
pub async fn get_project_material_stats_for_matching(
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ pub mod test_commands;
|
|||||||
pub mod debug_commands;
|
pub mod debug_commands;
|
||||||
pub mod project_template_binding_commands;
|
pub mod project_template_binding_commands;
|
||||||
pub mod material_matching_commands;
|
pub mod material_matching_commands;
|
||||||
|
pub mod template_matching_result_commands;
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* 模板匹配结果相关的 Tauri 命令
|
||||||
|
* 遵循 Tauri 开发规范的 API 设计原则
|
||||||
|
*/
|
||||||
|
|
||||||
|
use tauri::{command, State};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::business::services::template_matching_result_service::{
|
||||||
|
TemplateMatchingResultService, TemplateMatchingResultDetail, MatchingStatistics,
|
||||||
|
};
|
||||||
|
use crate::business::services::material_matching_service::MaterialMatchingResult as ServiceMatchingResult;
|
||||||
|
use crate::data::models::template_matching_result::{
|
||||||
|
TemplateMatchingResult, CreateTemplateMatchingResultRequest,
|
||||||
|
TemplateMatchingResultQueryOptions, MatchingResultStatus,
|
||||||
|
};
|
||||||
|
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||||
|
use crate::infrastructure::database::Database;
|
||||||
|
|
||||||
|
/// 保存匹配结果到数据库
|
||||||
|
#[command]
|
||||||
|
pub async fn save_matching_result(
|
||||||
|
service_result: ServiceMatchingResult,
|
||||||
|
result_name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
matching_duration_ms: u64,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<TemplateMatchingResult, String> {
|
||||||
|
// 创建服务实例
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
// 保存匹配结果
|
||||||
|
service.save_matching_result(&service_result, result_name, description, matching_duration_ms)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取匹配结果详情
|
||||||
|
#[command]
|
||||||
|
pub async fn get_matching_result_detail(
|
||||||
|
result_id: String,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<Option<TemplateMatchingResultDetail>, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.get_matching_result_detail(&result_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取项目的匹配结果列表
|
||||||
|
#[command]
|
||||||
|
pub async fn get_project_matching_results(
|
||||||
|
project_id: String,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<Vec<TemplateMatchingResult>, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.get_project_matching_results(&project_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取模板的匹配结果列表
|
||||||
|
#[command]
|
||||||
|
pub async fn get_template_matching_results(
|
||||||
|
template_id: String,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<Vec<TemplateMatchingResult>, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.get_template_matching_results(&template_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取绑定的匹配结果列表
|
||||||
|
#[command]
|
||||||
|
pub async fn get_binding_matching_results(
|
||||||
|
binding_id: String,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<Vec<TemplateMatchingResult>, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.get_binding_matching_results(&binding_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询匹配结果列表
|
||||||
|
#[command]
|
||||||
|
pub async fn list_matching_results(
|
||||||
|
options: TemplateMatchingResultQueryOptions,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<Vec<TemplateMatchingResult>, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.list_matching_results(options)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除匹配结果
|
||||||
|
#[command]
|
||||||
|
pub async fn delete_matching_result(
|
||||||
|
result_id: String,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.delete_matching_result(&result_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 软删除匹配结果
|
||||||
|
#[command]
|
||||||
|
pub async fn soft_delete_matching_result(
|
||||||
|
result_id: String,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.soft_delete_matching_result(&result_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新匹配结果信息
|
||||||
|
#[command]
|
||||||
|
pub async fn update_matching_result_info(
|
||||||
|
result_id: String,
|
||||||
|
result_name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<Option<TemplateMatchingResult>, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.update_matching_result_info(&result_id, result_name, description)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置匹配结果质量评分
|
||||||
|
#[command]
|
||||||
|
pub async fn set_matching_result_quality_score(
|
||||||
|
result_id: String,
|
||||||
|
quality_score: f64,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<Option<TemplateMatchingResult>, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.set_quality_score(&result_id, quality_score)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取匹配结果统计信息
|
||||||
|
#[command]
|
||||||
|
pub async fn get_matching_statistics(
|
||||||
|
project_id: Option<String>,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<MatchingStatistics, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.get_matching_statistics(project_id.as_deref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建匹配结果(手动创建,不通过匹配服务)
|
||||||
|
#[command]
|
||||||
|
pub async fn create_matching_result(
|
||||||
|
request: CreateTemplateMatchingResultRequest,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<TemplateMatchingResult, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
|
||||||
|
repository.create(request)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据ID获取匹配结果
|
||||||
|
#[command]
|
||||||
|
pub async fn get_matching_result_by_id(
|
||||||
|
result_id: String,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<Option<TemplateMatchingResult>, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
|
||||||
|
repository.get_by_id(&result_id)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取匹配结果状态选项(用于前端下拉选择)
|
||||||
|
#[command]
|
||||||
|
pub async fn get_matching_result_status_options() -> Result<Vec<(String, String)>, String> {
|
||||||
|
Ok(vec![
|
||||||
|
("Success".to_string(), "匹配成功".to_string()),
|
||||||
|
("PartialSuccess".to_string(), "部分成功".to_string()),
|
||||||
|
("Failed".to_string(), "匹配失败".to_string()),
|
||||||
|
("Cancelled".to_string(), "已取消".to_string()),
|
||||||
|
])
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ interface DeleteConfirmDialogProps {
|
|||||||
/** 要删除的项目名称 */
|
/** 要删除的项目名称 */
|
||||||
itemName?: string;
|
itemName?: string;
|
||||||
/** 是否正在删除 */
|
/** 是否正在删除 */
|
||||||
deleting: boolean;
|
deleting?: boolean;
|
||||||
/** 确认删除回调 */
|
/** 确认删除回调 */
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
/** 取消回调 */
|
/** 取消回调 */
|
||||||
|
|||||||
198
apps/desktop/src/components/TemplateMatchingResultCard.tsx
Normal file
198
apps/desktop/src/components/TemplateMatchingResultCard.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { TemplateMatchingResult, MatchingResultStatus } from '../types/templateMatchingResult';
|
||||||
|
|
||||||
|
interface TemplateMatchingResultCardProps {
|
||||||
|
result: TemplateMatchingResult;
|
||||||
|
onViewDetail: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onEdit?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
|
||||||
|
result,
|
||||||
|
onViewDetail,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
|
}) => {
|
||||||
|
// 格式化时长显示
|
||||||
|
const formatDuration = (ms: number): string => {
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
return `${(ms / 60000).toFixed(1)}min`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化日期显示
|
||||||
|
const formatDate = (dateString: string): string => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态样式
|
||||||
|
const getStatusStyle = (status: MatchingResultStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case MatchingResultStatus.Success:
|
||||||
|
return 'bg-green-100 text-green-800 border-green-200';
|
||||||
|
case MatchingResultStatus.PartialSuccess:
|
||||||
|
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
|
||||||
|
case MatchingResultStatus.Failed:
|
||||||
|
return 'bg-red-100 text-red-800 border-red-200';
|
||||||
|
case MatchingResultStatus.Cancelled:
|
||||||
|
return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态显示文本
|
||||||
|
const getStatusText = (status: MatchingResultStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case MatchingResultStatus.Success:
|
||||||
|
return '匹配成功';
|
||||||
|
case MatchingResultStatus.PartialSuccess:
|
||||||
|
return '部分成功';
|
||||||
|
case MatchingResultStatus.Failed:
|
||||||
|
return '匹配失败';
|
||||||
|
case MatchingResultStatus.Cancelled:
|
||||||
|
return '已取消';
|
||||||
|
default:
|
||||||
|
return '未知状态';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取成功率颜色
|
||||||
|
const getSuccessRateColor = (rate: number) => {
|
||||||
|
if (rate >= 90) return 'text-green-600';
|
||||||
|
if (rate >= 70) return 'text-yellow-600';
|
||||||
|
if (rate >= 50) return 'text-orange-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="template-matching-result-card bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
||||||
|
{/* 卡片头部 */}
|
||||||
|
<div className="p-4 border-b border-gray-100">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
||||||
|
{result.result_name}
|
||||||
|
</h3>
|
||||||
|
{result.description && (
|
||||||
|
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
||||||
|
{result.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`ml-3 px-2 py-1 rounded-full text-xs font-medium border ${getStatusStyle(result.status)}`}>
|
||||||
|
{getStatusText(result.status)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
{/* 成功率 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<div className={`text-2xl font-bold ${getSuccessRateColor(result.success_rate)}`}>
|
||||||
|
{result.success_rate.toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">成功率</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 片段统计 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-gray-900">
|
||||||
|
{result.matched_segments}/{result.total_segments}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">匹配片段</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 详细统计 */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">失败片段:</span>
|
||||||
|
<span className="text-gray-900">{result.failed_segments}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">使用素材:</span>
|
||||||
|
<span className="text-gray-900">{result.used_materials}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">使用模特:</span>
|
||||||
|
<span className="text-gray-900">{result.used_models}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">匹配耗时:</span>
|
||||||
|
<span className="text-gray-900">{formatDuration(result.matching_duration_ms)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 质量评分 */}
|
||||||
|
{result.quality_score && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-500">质量评分:</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex text-yellow-400">
|
||||||
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
|
<svg
|
||||||
|
key={star}
|
||||||
|
className={`w-4 h-4 ${
|
||||||
|
star <= result.quality_score! ? 'fill-current' : 'text-gray-300'
|
||||||
|
}`}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||||
|
</svg>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="ml-2 text-sm text-gray-900">
|
||||||
|
{result.quality_score.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 时间信息 */}
|
||||||
|
<div className="px-4 py-3 bg-gray-50 border-t border-gray-100 text-xs text-gray-500">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>创建: {formatDate(result.created_at)}</span>
|
||||||
|
<span>更新: {formatDate(result.updated_at)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="px-4 py-3 border-t border-gray-100 flex justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={onViewDetail}
|
||||||
|
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</button>
|
||||||
|
{onEdit && (
|
||||||
|
<button
|
||||||
|
onClick={onEdit}
|
||||||
|
className="px-3 py-1.5 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={onDelete}
|
||||||
|
className="px-3 py-1.5 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import {
|
||||||
|
TemplateMatchingResultDetail,
|
||||||
|
MatchingResultStatus
|
||||||
|
} from '../types/templateMatchingResult';
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { LoadingSpinner } from './LoadingSpinner';
|
||||||
|
import { ErrorMessage } from './ErrorMessage';
|
||||||
|
import { TabNavigation } from './TabNavigation';
|
||||||
|
|
||||||
|
interface TemplateMatchingResultDetailModalProps {
|
||||||
|
resultId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onUpdate?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateMatchingResultDetailModal: React.FC<TemplateMatchingResultDetailModalProps> = ({
|
||||||
|
resultId,
|
||||||
|
onClose,
|
||||||
|
onUpdate,
|
||||||
|
}) => {
|
||||||
|
const [detail, setDetail] = useState<TemplateMatchingResultDetail | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState('overview');
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
result_name: '',
|
||||||
|
description: '',
|
||||||
|
quality_score: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载详情数据
|
||||||
|
const loadDetail = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const resultDetail = await invoke<TemplateMatchingResultDetail>('get_matching_result_detail', {
|
||||||
|
resultId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resultDetail) {
|
||||||
|
setDetail(resultDetail);
|
||||||
|
setEditForm({
|
||||||
|
result_name: resultDetail.matching_result.result_name,
|
||||||
|
description: resultDetail.matching_result.description || '',
|
||||||
|
quality_score: resultDetail.matching_result.quality_score?.toString() || '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setError('匹配结果不存在');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err as string);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存编辑
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!detail) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新基本信息
|
||||||
|
await invoke('update_matching_result_info', {
|
||||||
|
resultId: detail.matching_result.id,
|
||||||
|
resultName: editForm.result_name || undefined,
|
||||||
|
description: editForm.description || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新质量评分
|
||||||
|
if (editForm.quality_score) {
|
||||||
|
const score = parseFloat(editForm.quality_score);
|
||||||
|
if (!isNaN(score) && score >= 0 && score <= 5) {
|
||||||
|
await invoke('set_matching_result_quality_score', {
|
||||||
|
resultId: detail.matching_result.id,
|
||||||
|
qualityScore: score,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditMode(false);
|
||||||
|
await loadDetail();
|
||||||
|
onUpdate?.();
|
||||||
|
} catch (err) {
|
||||||
|
setError(`保存失败: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时长
|
||||||
|
const formatDuration = (ms: number): string => {
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
return `${(ms / 60000).toFixed(1)}min`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (microseconds: number): string => {
|
||||||
|
const seconds = microseconds / 1000000;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = (seconds % 60).toFixed(1);
|
||||||
|
return minutes > 0 ? `${minutes}:${remainingSeconds.padStart(4, '0')}` : `${remainingSeconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态样式
|
||||||
|
const getStatusStyle = (status: MatchingResultStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case MatchingResultStatus.Success:
|
||||||
|
return 'bg-green-100 text-green-800';
|
||||||
|
case MatchingResultStatus.PartialSuccess:
|
||||||
|
return 'bg-yellow-100 text-yellow-800';
|
||||||
|
case MatchingResultStatus.Failed:
|
||||||
|
return 'bg-red-100 text-red-800';
|
||||||
|
case MatchingResultStatus.Cancelled:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadDetail();
|
||||||
|
}, [resultId]);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'overview', label: '概览', count: undefined },
|
||||||
|
{ id: 'segments', label: '成功片段', count: detail?.segment_results.length },
|
||||||
|
{ id: 'failed', label: '失败片段', count: detail?.failed_segment_results.length },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen onClose={onClose} title="匹配结果详情">
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<LoadingSpinner text="加载详情..." />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !detail) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen onClose={onClose} title="匹配结果详情">
|
||||||
|
<div className="p-4">
|
||||||
|
<ErrorMessage message={error || '数据加载失败'} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { matching_result, segment_results, failed_segment_results } = detail;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen
|
||||||
|
onClose={onClose}
|
||||||
|
title="匹配结果详情"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="max-h-[80vh] overflow-y-auto">
|
||||||
|
{/* 头部信息 */}
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
{editMode ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
结果名称
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.result_name}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, result_name: e.target.value }))}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
描述
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={editForm.description}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
质量评分 (0-5)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="5"
|
||||||
|
step="0.1"
|
||||||
|
value={editForm.quality_score}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, quality_score: e.target.value }))}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditMode(false)}
|
||||||
|
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
|
{matching_result.result_name}
|
||||||
|
</h2>
|
||||||
|
{matching_result.description && (
|
||||||
|
<p className="text-gray-600 mt-1">{matching_result.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusStyle(matching_result.status)}`}>
|
||||||
|
{matching_result.status === MatchingResultStatus.Success ? '匹配成功' :
|
||||||
|
matching_result.status === MatchingResultStatus.PartialSuccess ? '部分成功' :
|
||||||
|
matching_result.status === MatchingResultStatus.Failed ? '匹配失败' : '已取消'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditMode(true)}
|
||||||
|
className="px-3 py-1 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||||
|
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
|
{matching_result.success_rate.toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">成功率</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-gray-900">
|
||||||
|
{matching_result.matched_segments}/{matching_result.total_segments}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">匹配片段</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-gray-900">
|
||||||
|
{matching_result.used_materials}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">使用素材</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-2xl font-bold text-gray-900">
|
||||||
|
{formatDuration(matching_result.matching_duration_ms)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">匹配耗时</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 质量评分 */}
|
||||||
|
{matching_result.quality_score && (
|
||||||
|
<div className="mt-4 p-3 bg-yellow-50 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-gray-700">质量评分:</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex text-yellow-400 mr-2">
|
||||||
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
|
<svg
|
||||||
|
key={star}
|
||||||
|
className={`w-5 h-5 ${
|
||||||
|
star <= matching_result.quality_score! ? 'fill-current' : 'text-gray-300'
|
||||||
|
}`}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||||
|
</svg>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{matching_result.quality_score.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标签页导航 */}
|
||||||
|
<TabNavigation
|
||||||
|
tabs={tabs}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 标签页内容 */}
|
||||||
|
<div className="p-6">
|
||||||
|
{activeTab === 'overview' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-3">基本信息</h3>
|
||||||
|
<dl className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">项目ID:</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{matching_result.project_id}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">模板ID:</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{matching_result.template_id}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">绑定ID:</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{matching_result.binding_id}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">创建时间:</dt>
|
||||||
|
<dd className="text-sm text-gray-900">
|
||||||
|
{new Date(matching_result.created_at).toLocaleString('zh-CN')}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">更新时间:</dt>
|
||||||
|
<dd className="text-sm text-gray-900">
|
||||||
|
{new Date(matching_result.updated_at).toLocaleString('zh-CN')}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-3">匹配统计</h3>
|
||||||
|
<dl className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">总片段数:</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{matching_result.total_segments}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">成功匹配:</dt>
|
||||||
|
<dd className="text-sm text-green-600">{matching_result.matched_segments}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">匹配失败:</dt>
|
||||||
|
<dd className="text-sm text-red-600">{matching_result.failed_segments}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">使用素材:</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{matching_result.used_materials}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-sm text-gray-500">使用模特:</dt>
|
||||||
|
<dd className="text-sm text-gray-900">{matching_result.used_models}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'segments' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
成功匹配的片段 ({segment_results.length})
|
||||||
|
</h3>
|
||||||
|
{segment_results.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-8">暂无成功匹配的片段</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{segment_results.map((segment) => (
|
||||||
|
<div key={segment.id} className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-900">{segment.track_segment_name}</h4>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
素材: {segment.material_name}
|
||||||
|
{segment.model_name && ` | 模特: ${segment.model_name}`}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
时长: {formatTime(segment.segment_duration)} |
|
||||||
|
时间: {formatTime(segment.start_time)} - {formatTime(segment.end_time)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
匹配原因: {segment.match_reason}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-lg font-bold text-green-600">
|
||||||
|
{(segment.match_score * 100).toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">匹配度</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'failed' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
匹配失败的片段 ({failed_segment_results.length})
|
||||||
|
</h3>
|
||||||
|
{failed_segment_results.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-8">暂无匹配失败的片段</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{failed_segment_results.map((segment) => (
|
||||||
|
<div key={segment.id} className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900">{segment.track_segment_name}</h4>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
匹配规则: {segment.matching_rule_type}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
时长: {formatTime(segment.segment_duration)} |
|
||||||
|
时间: {formatTime(segment.start_time)} - {formatTime(segment.end_time)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-red-600 mt-2 font-medium">
|
||||||
|
失败原因: {segment.failure_reason}
|
||||||
|
</p>
|
||||||
|
{segment.failure_details && (
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
详情: {segment.failure_details}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
332
apps/desktop/src/components/TemplateMatchingResultManager.tsx
Normal file
332
apps/desktop/src/components/TemplateMatchingResultManager.tsx
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import {
|
||||||
|
TemplateMatchingResult,
|
||||||
|
TemplateMatchingResultQueryOptions,
|
||||||
|
MatchingResultStatus,
|
||||||
|
MatchingStatistics
|
||||||
|
} from '../types/templateMatchingResult';
|
||||||
|
import { LoadingSpinner } from './LoadingSpinner';
|
||||||
|
import { ErrorMessage } from './ErrorMessage';
|
||||||
|
import { EmptyState } from './EmptyState';
|
||||||
|
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
|
||||||
|
import { CustomSelect } from './CustomSelect';
|
||||||
|
import { TemplateMatchingResultCard } from './TemplateMatchingResultCard';
|
||||||
|
import { TemplateMatchingResultDetailModal } from './TemplateMatchingResultDetailModal';
|
||||||
|
import { TemplateMatchingResultStatsPanel } from './TemplateMatchingResultStatsPanel';
|
||||||
|
|
||||||
|
interface TemplateMatchingResultManagerProps {
|
||||||
|
projectId?: string;
|
||||||
|
templateId?: string;
|
||||||
|
bindingId?: string;
|
||||||
|
showStats?: boolean;
|
||||||
|
onResultSelect?: (result: TemplateMatchingResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManagerProps> = ({
|
||||||
|
projectId,
|
||||||
|
templateId,
|
||||||
|
bindingId,
|
||||||
|
showStats = true,
|
||||||
|
onResultSelect,
|
||||||
|
}) => {
|
||||||
|
const [results, setResults] = useState<TemplateMatchingResult[]>([]);
|
||||||
|
const [statistics, setStatistics] = useState<MatchingStatistics | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedResult, setSelectedResult] = useState<TemplateMatchingResult | null>(null);
|
||||||
|
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||||
|
show: boolean;
|
||||||
|
result: TemplateMatchingResult | null;
|
||||||
|
}>({ show: false, result: null });
|
||||||
|
|
||||||
|
// 过滤和排序状态
|
||||||
|
const [filters, setFilters] = useState<{
|
||||||
|
status?: MatchingResultStatus;
|
||||||
|
searchKeyword?: string;
|
||||||
|
sortBy: string;
|
||||||
|
sortOrder: string;
|
||||||
|
}>({
|
||||||
|
sortBy: 'created_at',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载匹配结果列表
|
||||||
|
const loadResults = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const queryOptions: TemplateMatchingResultQueryOptions = {
|
||||||
|
project_id: projectId,
|
||||||
|
template_id: templateId,
|
||||||
|
binding_id: bindingId,
|
||||||
|
status: filters.status,
|
||||||
|
search_keyword: filters.searchKeyword,
|
||||||
|
sort_by: filters.sortBy,
|
||||||
|
sort_order: filters.sortOrder,
|
||||||
|
limit: pagination.pageSize,
|
||||||
|
offset: (pagination.page - 1) * pagination.pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultList = await invoke<TemplateMatchingResult[]>('list_matching_results', {
|
||||||
|
options: queryOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
setResults(resultList);
|
||||||
|
setPagination(prev => ({ ...prev, total: resultList.length }));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err as string);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载统计信息
|
||||||
|
const loadStatistics = async () => {
|
||||||
|
if (!showStats) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await invoke<MatchingStatistics>('get_matching_statistics', {
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
setStatistics(stats);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载统计信息失败:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除匹配结果
|
||||||
|
const handleDelete = async (result: TemplateMatchingResult) => {
|
||||||
|
try {
|
||||||
|
await invoke<boolean>('soft_delete_matching_result', {
|
||||||
|
resultId: result.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重新加载列表
|
||||||
|
await loadResults();
|
||||||
|
await loadStatistics();
|
||||||
|
|
||||||
|
setDeleteConfirm({ show: false, result: null });
|
||||||
|
} catch (err) {
|
||||||
|
setError(`删除失败: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleViewDetail = (result: TemplateMatchingResult) => {
|
||||||
|
setSelectedResult(result);
|
||||||
|
setShowDetailModal(true);
|
||||||
|
onResultSelect?.(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理过滤器变化
|
||||||
|
const handleFilterChange = (newFilters: Partial<typeof filters>) => {
|
||||||
|
setFilters(prev => ({ ...prev, ...newFilters }));
|
||||||
|
setPagination(prev => ({ ...prev, page: 1 })); // 重置到第一页
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理分页变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setPagination(prev => ({ ...prev, page }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
useEffect(() => {
|
||||||
|
loadResults();
|
||||||
|
loadStatistics();
|
||||||
|
}, [projectId, templateId, bindingId, filters, pagination.page]);
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: '', label: '全部状态' },
|
||||||
|
{ value: 'Success', label: '匹配成功' },
|
||||||
|
{ value: 'PartialSuccess', label: '部分成功' },
|
||||||
|
{ value: 'Failed', label: '匹配失败' },
|
||||||
|
{ value: 'Cancelled', label: '已取消' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 排序选项
|
||||||
|
const sortOptions = [
|
||||||
|
{ value: 'created_at', label: '创建时间' },
|
||||||
|
{ value: 'updated_at', label: '更新时间' },
|
||||||
|
{ value: 'success_rate', label: '成功率' },
|
||||||
|
{ value: 'result_name', label: '结果名称' },
|
||||||
|
{ value: 'total_segments', label: '片段数量' },
|
||||||
|
{ value: 'matching_duration_ms', label: '匹配耗时' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sortOrderOptions = [
|
||||||
|
{ value: 'desc', label: '降序' },
|
||||||
|
{ value: 'asc', label: '升序' },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading && results.length === 0) {
|
||||||
|
return <LoadingSpinner text="加载匹配结果..." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="template-matching-result-manager">
|
||||||
|
{/* 统计面板 */}
|
||||||
|
{showStats && statistics && (
|
||||||
|
<TemplateMatchingResultStatsPanel statistics={statistics} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 过滤器和搜索 */}
|
||||||
|
<div className="filters-section bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
搜索
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索结果名称或描述..."
|
||||||
|
value={filters.searchKeyword || ''}
|
||||||
|
onChange={(e) => handleFilterChange({ searchKeyword: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 状态过滤 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
状态
|
||||||
|
</label>
|
||||||
|
<CustomSelect
|
||||||
|
options={statusOptions}
|
||||||
|
value={filters.status || ''}
|
||||||
|
onChange={(value) => handleFilterChange({ status: value as MatchingResultStatus })}
|
||||||
|
placeholder="选择状态"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 排序字段 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
排序字段
|
||||||
|
</label>
|
||||||
|
<CustomSelect
|
||||||
|
options={sortOptions}
|
||||||
|
value={filters.sortBy}
|
||||||
|
onChange={(value) => handleFilterChange({ sortBy: value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 排序顺序 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
排序顺序
|
||||||
|
</label>
|
||||||
|
<CustomSelect
|
||||||
|
options={sortOrderOptions}
|
||||||
|
value={filters.sortOrder}
|
||||||
|
onChange={(value) => handleFilterChange({ sortOrder: value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex justify-between items-center mt-4">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
共 {pagination.total} 个匹配结果
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
loadResults();
|
||||||
|
loadStatistics();
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 错误信息 */}
|
||||||
|
{error && (
|
||||||
|
<ErrorMessage
|
||||||
|
message={error}
|
||||||
|
onDismiss={() => setError(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 结果列表 */}
|
||||||
|
{results.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
title="暂无匹配结果"
|
||||||
|
description="还没有任何模板匹配结果,请先执行模板匹配操作。"
|
||||||
|
icon="📊"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="results-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{results.map((result) => (
|
||||||
|
<TemplateMatchingResultCard
|
||||||
|
key={result.id}
|
||||||
|
result={result}
|
||||||
|
onViewDetail={() => handleViewDetail(result)}
|
||||||
|
onDelete={() => setDeleteConfirm({ show: true, result })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{pagination.total > pagination.pageSize && (
|
||||||
|
<div className="pagination-section mt-6 flex justify-center">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handlePageChange(pagination.page - 1)}
|
||||||
|
disabled={pagination.page <= 1}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<span className="px-3 py-2 text-sm text-gray-700">
|
||||||
|
第 {pagination.page} 页,共 {Math.ceil(pagination.total / pagination.pageSize)} 页
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePageChange(pagination.page + 1)}
|
||||||
|
disabled={pagination.page >= Math.ceil(pagination.total / pagination.pageSize)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 详情模态框 */}
|
||||||
|
{showDetailModal && selectedResult && (
|
||||||
|
<TemplateMatchingResultDetailModal
|
||||||
|
resultId={selectedResult.id}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDetailModal(false);
|
||||||
|
setSelectedResult(null);
|
||||||
|
}}
|
||||||
|
onUpdate={() => {
|
||||||
|
loadResults();
|
||||||
|
loadStatistics();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
isOpen={deleteConfirm.show}
|
||||||
|
title="删除匹配结果"
|
||||||
|
message={`确定要删除匹配结果 "${deleteConfirm.result?.result_name}" 吗?此操作不可撤销。`}
|
||||||
|
onConfirm={() => deleteConfirm.result && handleDelete(deleteConfirm.result)}
|
||||||
|
onCancel={() => setDeleteConfirm({ show: false, result: null })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
215
apps/desktop/src/components/TemplateMatchingResultStatsPanel.tsx
Normal file
215
apps/desktop/src/components/TemplateMatchingResultStatsPanel.tsx
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MatchingStatistics } from '../types/templateMatchingResult';
|
||||||
|
|
||||||
|
interface TemplateMatchingResultStatsPanelProps {
|
||||||
|
statistics: MatchingStatistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateMatchingResultStatsPanel: React.FC<TemplateMatchingResultStatsPanelProps> = ({
|
||||||
|
statistics,
|
||||||
|
}) => {
|
||||||
|
// 计算成功率百分比
|
||||||
|
const successPercentage = statistics.total_results > 0
|
||||||
|
? (statistics.successful_results / statistics.total_results) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// 计算片段匹配率
|
||||||
|
const segmentMatchPercentage = statistics.total_segments > 0
|
||||||
|
? (statistics.matched_segments / statistics.total_segments) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// 获取成功率颜色
|
||||||
|
const getSuccessRateColor = (rate: number) => {
|
||||||
|
if (rate >= 90) return 'text-green-600';
|
||||||
|
if (rate >= 70) return 'text-yellow-600';
|
||||||
|
if (rate >= 50) return 'text-orange-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统计卡片组件
|
||||||
|
const StatCard: React.FC<{
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
subtitle?: string;
|
||||||
|
color?: string;
|
||||||
|
icon?: string;
|
||||||
|
}> = ({ title, value, subtitle, color = 'text-gray-900', icon }) => (
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-500">{title}</p>
|
||||||
|
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-xs text-gray-400 mt-1">{subtitle}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{icon && (
|
||||||
|
<div className="text-2xl opacity-60">{icon}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="template-matching-result-stats-panel mb-6">
|
||||||
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||||
|
<span className="mr-2">📊</span>
|
||||||
|
匹配结果统计
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{/* 总结果数 */}
|
||||||
|
<StatCard
|
||||||
|
title="总匹配结果"
|
||||||
|
value={statistics.total_results}
|
||||||
|
subtitle="个匹配结果"
|
||||||
|
icon="📋"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 成功结果数 */}
|
||||||
|
<StatCard
|
||||||
|
title="成功结果"
|
||||||
|
value={statistics.successful_results}
|
||||||
|
subtitle={`${successPercentage.toFixed(1)}% 成功率`}
|
||||||
|
color={getSuccessRateColor(successPercentage)}
|
||||||
|
icon="✅"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 总片段数 */}
|
||||||
|
<StatCard
|
||||||
|
title="总片段数"
|
||||||
|
value={statistics.total_segments}
|
||||||
|
subtitle="个模板片段"
|
||||||
|
icon="🎬"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 匹配片段数 */}
|
||||||
|
<StatCard
|
||||||
|
title="匹配片段"
|
||||||
|
value={statistics.matched_segments}
|
||||||
|
subtitle={`${segmentMatchPercentage.toFixed(1)}% 匹配率`}
|
||||||
|
color={getSuccessRateColor(segmentMatchPercentage)}
|
||||||
|
icon="🎯"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 详细统计 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{/* 素材使用统计 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<span className="mr-2">🎥</span>
|
||||||
|
素材使用情况
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">使用素材总数:</span>
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{statistics.total_materials}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">平均每结果:</span>
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{statistics.total_results > 0
|
||||||
|
? (statistics.total_materials / statistics.total_results).toFixed(1)
|
||||||
|
: '0'
|
||||||
|
} 个
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模特使用统计 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<span className="mr-2">👤</span>
|
||||||
|
模特使用情况
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">使用模特总数:</span>
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{statistics.total_models}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">平均每结果:</span>
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{statistics.total_results > 0
|
||||||
|
? (statistics.total_models / statistics.total_results).toFixed(1)
|
||||||
|
: '0'
|
||||||
|
} 个
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 质量统计 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<span className="mr-2">⭐</span>
|
||||||
|
质量统计
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">平均成功率:</span>
|
||||||
|
<span className={`text-sm font-medium ${getSuccessRateColor(statistics.average_success_rate)}`}>
|
||||||
|
{statistics.average_success_rate.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">失败片段:</span>
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{statistics.total_segments - statistics.matched_segments}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条显示 */}
|
||||||
|
<div className="mt-4 bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 mb-3">整体匹配进度</h3>
|
||||||
|
|
||||||
|
{/* 结果成功率进度条 */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||||
|
<span>结果成功率</span>
|
||||||
|
<span>{successPercentage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full transition-all duration-300 ${
|
||||||
|
successPercentage >= 90 ? 'bg-green-500' :
|
||||||
|
successPercentage >= 70 ? 'bg-yellow-500' :
|
||||||
|
successPercentage >= 50 ? 'bg-orange-500' : 'bg-red-500'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${Math.min(successPercentage, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 片段匹配率进度条 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||||
|
<span>片段匹配率</span>
|
||||||
|
<span>{segmentMatchPercentage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full transition-all duration-300 ${
|
||||||
|
segmentMatchPercentage >= 90 ? 'bg-green-500' :
|
||||||
|
segmentMatchPercentage >= 70 ? 'bg-yellow-500' :
|
||||||
|
segmentMatchPercentage >= 50 ? 'bg-orange-500' : 'bg-red-500'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${Math.min(segmentMatchPercentage, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
298
apps/desktop/src/types/templateMatchingResult.ts
Normal file
298
apps/desktop/src/types/templateMatchingResult.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* 模板匹配结果相关的类型定义
|
||||||
|
* 遵循前端开发规范的类型设计原则
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// 匹配结果状态枚举
|
||||||
|
export enum MatchingResultStatus {
|
||||||
|
Success = 'Success',
|
||||||
|
PartialSuccess = 'PartialSuccess',
|
||||||
|
Failed = 'Failed',
|
||||||
|
Cancelled = 'Cancelled'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板匹配结果
|
||||||
|
export interface TemplateMatchingResult {
|
||||||
|
id: string;
|
||||||
|
project_id: string;
|
||||||
|
template_id: string;
|
||||||
|
binding_id: string;
|
||||||
|
result_name: string;
|
||||||
|
description?: string;
|
||||||
|
total_segments: number;
|
||||||
|
matched_segments: number;
|
||||||
|
failed_segments: number;
|
||||||
|
success_rate: number;
|
||||||
|
used_materials: number;
|
||||||
|
used_models: number;
|
||||||
|
matching_duration_ms: number;
|
||||||
|
quality_score?: number;
|
||||||
|
status: MatchingResultStatus;
|
||||||
|
metadata?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配片段结果
|
||||||
|
export interface MatchingSegmentResult {
|
||||||
|
id: string;
|
||||||
|
matching_result_id: string;
|
||||||
|
track_segment_id: string;
|
||||||
|
track_segment_name: string;
|
||||||
|
material_segment_id: string;
|
||||||
|
material_id: string;
|
||||||
|
material_name: string;
|
||||||
|
model_id?: string;
|
||||||
|
model_name?: string;
|
||||||
|
match_score: number;
|
||||||
|
match_reason: string;
|
||||||
|
segment_duration: number;
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
|
properties?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配失败片段结果
|
||||||
|
export interface MatchingFailedSegmentResult {
|
||||||
|
id: string;
|
||||||
|
matching_result_id: string;
|
||||||
|
track_segment_id: string;
|
||||||
|
track_segment_name: string;
|
||||||
|
matching_rule_type: string;
|
||||||
|
matching_rule_data?: string;
|
||||||
|
failure_reason: string;
|
||||||
|
failure_details?: string;
|
||||||
|
segment_duration: number;
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板匹配结果详情
|
||||||
|
export interface TemplateMatchingResultDetail {
|
||||||
|
matching_result: TemplateMatchingResult;
|
||||||
|
segment_results: MatchingSegmentResult[];
|
||||||
|
failed_segment_results: MatchingFailedSegmentResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建模板匹配结果请求
|
||||||
|
export interface CreateTemplateMatchingResultRequest {
|
||||||
|
project_id: string;
|
||||||
|
template_id: string;
|
||||||
|
binding_id: string;
|
||||||
|
result_name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板匹配结果查询选项
|
||||||
|
export interface TemplateMatchingResultQueryOptions {
|
||||||
|
project_id?: string;
|
||||||
|
template_id?: string;
|
||||||
|
binding_id?: string;
|
||||||
|
status?: MatchingResultStatus;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search_keyword?: string;
|
||||||
|
sort_by?: string; // "created_at", "success_rate", "result_name"
|
||||||
|
sort_order?: string; // "asc", "desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配统计信息
|
||||||
|
export interface MatchingStatistics {
|
||||||
|
total_results: number;
|
||||||
|
successful_results: number;
|
||||||
|
total_segments: number;
|
||||||
|
matched_segments: number;
|
||||||
|
total_materials: number;
|
||||||
|
total_models: number;
|
||||||
|
average_success_rate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存匹配结果请求
|
||||||
|
export interface SaveMatchingResultRequest {
|
||||||
|
service_result: any; // MaterialMatchingResult from service
|
||||||
|
result_name: string;
|
||||||
|
description?: string;
|
||||||
|
matching_duration_ms: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新匹配结果信息请求
|
||||||
|
export interface UpdateMatchingResultInfoRequest {
|
||||||
|
result_id: string;
|
||||||
|
result_name?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置质量评分请求
|
||||||
|
export interface SetQualityScoreRequest {
|
||||||
|
result_id: string;
|
||||||
|
quality_score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果状态选项
|
||||||
|
export interface MatchingResultStatusOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果列表项(用于列表显示)
|
||||||
|
export interface MatchingResultListItem {
|
||||||
|
id: string;
|
||||||
|
result_name: string;
|
||||||
|
project_id: string;
|
||||||
|
template_id: string;
|
||||||
|
status: MatchingResultStatus;
|
||||||
|
success_rate: number;
|
||||||
|
total_segments: number;
|
||||||
|
matched_segments: number;
|
||||||
|
failed_segments: number;
|
||||||
|
used_materials: number;
|
||||||
|
used_models: number;
|
||||||
|
matching_duration_ms: number;
|
||||||
|
quality_score?: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果卡片信息(用于卡片显示)
|
||||||
|
export interface MatchingResultCardInfo {
|
||||||
|
id: string;
|
||||||
|
result_name: string;
|
||||||
|
description?: string;
|
||||||
|
status: MatchingResultStatus;
|
||||||
|
status_display: string;
|
||||||
|
success_rate: number;
|
||||||
|
total_segments: number;
|
||||||
|
matched_segments: number;
|
||||||
|
failed_segments: number;
|
||||||
|
used_materials: number;
|
||||||
|
used_models: number;
|
||||||
|
matching_duration_display: string; // 格式化后的时长显示
|
||||||
|
quality_score?: number;
|
||||||
|
created_at_display: string; // 格式化后的创建时间显示
|
||||||
|
updated_at_display: string; // 格式化后的更新时间显示
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果过滤选项
|
||||||
|
export interface MatchingResultFilterOptions {
|
||||||
|
status?: MatchingResultStatus[];
|
||||||
|
success_rate_min?: number;
|
||||||
|
success_rate_max?: number;
|
||||||
|
date_range?: {
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
};
|
||||||
|
has_quality_score?: boolean;
|
||||||
|
min_segments?: number;
|
||||||
|
max_segments?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果排序选项
|
||||||
|
export interface MatchingResultSortOptions {
|
||||||
|
field: 'created_at' | 'updated_at' | 'success_rate' | 'result_name' | 'total_segments' | 'matching_duration_ms';
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果分页选项
|
||||||
|
export interface MatchingResultPaginationOptions {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
total?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果搜索选项
|
||||||
|
export interface MatchingResultSearchOptions {
|
||||||
|
keyword?: string;
|
||||||
|
filter?: MatchingResultFilterOptions;
|
||||||
|
sort?: MatchingResultSortOptions;
|
||||||
|
pagination?: MatchingResultPaginationOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果导出选项
|
||||||
|
export interface MatchingResultExportOptions {
|
||||||
|
format: 'json' | 'csv' | 'excel';
|
||||||
|
include_segments: boolean;
|
||||||
|
include_failed_segments: boolean;
|
||||||
|
date_range?: {
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果批量操作选项
|
||||||
|
export interface MatchingResultBatchOperationOptions {
|
||||||
|
operation: 'delete' | 'soft_delete' | 'export' | 'set_quality_score';
|
||||||
|
result_ids: string[];
|
||||||
|
parameters?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果分析报告
|
||||||
|
export interface MatchingResultAnalysisReport {
|
||||||
|
total_results: number;
|
||||||
|
status_distribution: Record<MatchingResultStatus, number>;
|
||||||
|
success_rate_distribution: {
|
||||||
|
excellent: number; // 90-100%
|
||||||
|
good: number; // 70-89%
|
||||||
|
fair: number; // 50-69%
|
||||||
|
poor: number; // 0-49%
|
||||||
|
};
|
||||||
|
duration_statistics: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
average: number;
|
||||||
|
median: number;
|
||||||
|
};
|
||||||
|
segment_statistics: {
|
||||||
|
total_segments: number;
|
||||||
|
total_matched: number;
|
||||||
|
total_failed: number;
|
||||||
|
average_segments_per_result: number;
|
||||||
|
};
|
||||||
|
material_usage: {
|
||||||
|
total_materials_used: number;
|
||||||
|
average_materials_per_result: number;
|
||||||
|
most_used_materials: Array<{
|
||||||
|
material_id: string;
|
||||||
|
material_name: string;
|
||||||
|
usage_count: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
model_usage: {
|
||||||
|
total_models_used: number;
|
||||||
|
average_models_per_result: number;
|
||||||
|
most_used_models: Array<{
|
||||||
|
model_id: string;
|
||||||
|
model_name: string;
|
||||||
|
usage_count: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
quality_score_distribution?: {
|
||||||
|
excellent: number; // 4.5-5.0
|
||||||
|
good: number; // 3.5-4.4
|
||||||
|
fair: number; // 2.5-3.4
|
||||||
|
poor: number; // 0-2.4
|
||||||
|
unrated: number;
|
||||||
|
};
|
||||||
|
trends: {
|
||||||
|
success_rate_trend: 'improving' | 'declining' | 'stable';
|
||||||
|
duration_trend: 'faster' | 'slower' | 'stable';
|
||||||
|
quality_trend?: 'improving' | 'declining' | 'stable';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配结果操作历史
|
||||||
|
export interface MatchingResultOperationHistory {
|
||||||
|
id: string;
|
||||||
|
result_id: string;
|
||||||
|
operation_type: 'create' | 'update' | 'delete' | 'quality_score_set';
|
||||||
|
operation_details: string;
|
||||||
|
performed_by?: string;
|
||||||
|
performed_at: string;
|
||||||
|
old_values?: Record<string, any>;
|
||||||
|
new_values?: Record<string, any>;
|
||||||
|
}
|
||||||
383
docs/api/template-matching-result-api.md
Normal file
383
docs/api/template-matching-result-api.md
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
# 模板匹配结果 API 文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
模板匹配结果 API 提供了完整的模板匹配结果管理功能,包括保存匹配结果、查询历史记录、管理匹配详情等操作。
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### TemplateMatchingResult
|
||||||
|
|
||||||
|
模板匹配结果主要实体。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TemplateMatchingResult {
|
||||||
|
id: string; // 匹配结果唯一标识
|
||||||
|
project_id: string; // 项目ID
|
||||||
|
template_id: string; // 模板ID
|
||||||
|
binding_id: string; // 绑定ID
|
||||||
|
result_name: string; // 结果名称
|
||||||
|
description?: string; // 结果描述
|
||||||
|
total_segments: number; // 总片段数
|
||||||
|
matched_segments: number; // 匹配成功片段数
|
||||||
|
failed_segments: number; // 匹配失败片段数
|
||||||
|
success_rate: number; // 成功率(百分比)
|
||||||
|
used_materials: number; // 使用的素材数量
|
||||||
|
used_models: number; // 使用的模特数量
|
||||||
|
matching_duration_ms: number; // 匹配耗时(毫秒)
|
||||||
|
quality_score?: number; // 质量评分(0-5)
|
||||||
|
status: MatchingResultStatus; // 匹配状态
|
||||||
|
metadata?: string; // 额外元数据(JSON格式)
|
||||||
|
created_at: string; // 创建时间
|
||||||
|
updated_at: string; // 更新时间
|
||||||
|
is_active: boolean; // 是否激活
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MatchingResultStatus
|
||||||
|
|
||||||
|
匹配结果状态枚举。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum MatchingResultStatus {
|
||||||
|
Success = 'Success', // 匹配成功
|
||||||
|
PartialSuccess = 'PartialSuccess', // 部分成功
|
||||||
|
Failed = 'Failed', // 匹配失败
|
||||||
|
Cancelled = 'Cancelled' // 已取消
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MatchingSegmentResult
|
||||||
|
|
||||||
|
成功匹配的片段结果。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MatchingSegmentResult {
|
||||||
|
id: string; // 片段结果ID
|
||||||
|
matching_result_id: string; // 关联的匹配结果ID
|
||||||
|
track_segment_id: string; // 模板轨道片段ID
|
||||||
|
track_segment_name: string; // 轨道片段名称
|
||||||
|
material_segment_id: string; // 匹配到的素材片段ID
|
||||||
|
material_id: string; // 素材ID
|
||||||
|
material_name: string; // 素材名称
|
||||||
|
model_id?: string; // 模特ID
|
||||||
|
model_name?: string; // 模特名称
|
||||||
|
match_score: number; // 匹配评分(0-1)
|
||||||
|
match_reason: string; // 匹配原因
|
||||||
|
segment_duration: number; // 片段时长(微秒)
|
||||||
|
start_time: number; // 开始时间(微秒)
|
||||||
|
end_time: number; // 结束时间(微秒)
|
||||||
|
properties?: string; // 片段属性(JSON格式)
|
||||||
|
created_at: string; // 创建时间
|
||||||
|
updated_at: string; // 更新时间
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MatchingFailedSegmentResult
|
||||||
|
|
||||||
|
匹配失败的片段结果。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MatchingFailedSegmentResult {
|
||||||
|
id: string; // 失败片段结果ID
|
||||||
|
matching_result_id: string; // 关联的匹配结果ID
|
||||||
|
track_segment_id: string; // 模板轨道片段ID
|
||||||
|
track_segment_name: string; // 轨道片段名称
|
||||||
|
matching_rule_type: string; // 匹配规则类型
|
||||||
|
matching_rule_data?: string; // 匹配规则数据(JSON格式)
|
||||||
|
failure_reason: string; // 失败原因
|
||||||
|
failure_details?: string; // 失败详情(JSON格式)
|
||||||
|
segment_duration: number; // 片段时长(微秒)
|
||||||
|
start_time: number; // 开始时间(微秒)
|
||||||
|
end_time: number; // 结束时间(微秒)
|
||||||
|
created_at: string; // 创建时间
|
||||||
|
updated_at: string; // 更新时间
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 1. 保存匹配结果
|
||||||
|
|
||||||
|
自动保存素材匹配结果到数据库。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
save_matching_result(
|
||||||
|
service_result: MaterialMatchingResult,
|
||||||
|
result_name: string,
|
||||||
|
description?: string,
|
||||||
|
matching_duration_ms: number
|
||||||
|
): Promise<TemplateMatchingResult>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `service_result`: 素材匹配服务返回的匹配结果
|
||||||
|
- `result_name`: 结果名称
|
||||||
|
- `description`: 可选的结果描述
|
||||||
|
- `matching_duration_ms`: 匹配耗时(毫秒)
|
||||||
|
|
||||||
|
**返回值:** 保存后的模板匹配结果
|
||||||
|
|
||||||
|
### 2. 获取匹配结果详情
|
||||||
|
|
||||||
|
获取包含所有片段信息的完整匹配结果详情。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
get_matching_result_detail(result_id: string): Promise<TemplateMatchingResultDetail | null>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `result_id`: 匹配结果ID
|
||||||
|
|
||||||
|
**返回值:** 匹配结果详情,包含成功和失败的片段信息
|
||||||
|
|
||||||
|
### 3. 获取项目匹配结果列表
|
||||||
|
|
||||||
|
获取指定项目的所有匹配结果。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
get_project_matching_results(project_id: string): Promise<TemplateMatchingResult[]>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `project_id`: 项目ID
|
||||||
|
|
||||||
|
**返回值:** 匹配结果列表
|
||||||
|
|
||||||
|
### 4. 获取模板匹配结果列表
|
||||||
|
|
||||||
|
获取指定模板的所有匹配结果。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
get_template_matching_results(template_id: string): Promise<TemplateMatchingResult[]>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `template_id`: 模板ID
|
||||||
|
|
||||||
|
**返回值:** 匹配结果列表
|
||||||
|
|
||||||
|
### 5. 获取绑定匹配结果列表
|
||||||
|
|
||||||
|
获取指定绑定的所有匹配结果。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
get_binding_matching_results(binding_id: string): Promise<TemplateMatchingResult[]>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `binding_id`: 绑定ID
|
||||||
|
|
||||||
|
**返回值:** 匹配结果列表
|
||||||
|
|
||||||
|
### 6. 查询匹配结果列表
|
||||||
|
|
||||||
|
根据查询条件获取匹配结果列表。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
list_matching_results(options: TemplateMatchingResultQueryOptions): Promise<TemplateMatchingResult[]>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `options`: 查询选项
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TemplateMatchingResultQueryOptions {
|
||||||
|
project_id?: string; // 项目ID过滤
|
||||||
|
template_id?: string; // 模板ID过滤
|
||||||
|
binding_id?: string; // 绑定ID过滤
|
||||||
|
status?: MatchingResultStatus; // 状态过滤
|
||||||
|
limit?: number; // 限制数量
|
||||||
|
offset?: number; // 偏移量
|
||||||
|
search_keyword?: string; // 搜索关键词
|
||||||
|
sort_by?: string; // 排序字段
|
||||||
|
sort_order?: string; // 排序顺序
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 删除匹配结果
|
||||||
|
|
||||||
|
永久删除匹配结果。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
delete_matching_result(result_id: string): Promise<boolean>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `result_id`: 匹配结果ID
|
||||||
|
|
||||||
|
**返回值:** 是否删除成功
|
||||||
|
|
||||||
|
### 8. 软删除匹配结果
|
||||||
|
|
||||||
|
软删除匹配结果(标记为不活跃)。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
soft_delete_matching_result(result_id: string): Promise<boolean>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `result_id`: 匹配结果ID
|
||||||
|
|
||||||
|
**返回值:** 是否删除成功
|
||||||
|
|
||||||
|
### 9. 更新匹配结果信息
|
||||||
|
|
||||||
|
更新匹配结果的名称和描述。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
update_matching_result_info(
|
||||||
|
result_id: string,
|
||||||
|
result_name?: string,
|
||||||
|
description?: string
|
||||||
|
): Promise<TemplateMatchingResult | null>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `result_id`: 匹配结果ID
|
||||||
|
- `result_name`: 新的结果名称(可选)
|
||||||
|
- `description`: 新的结果描述(可选)
|
||||||
|
|
||||||
|
**返回值:** 更新后的匹配结果
|
||||||
|
|
||||||
|
### 10. 设置质量评分
|
||||||
|
|
||||||
|
为匹配结果设置质量评分。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
set_matching_result_quality_score(
|
||||||
|
result_id: string,
|
||||||
|
quality_score: number
|
||||||
|
): Promise<TemplateMatchingResult | null>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `result_id`: 匹配结果ID
|
||||||
|
- `quality_score`: 质量评分(0-5)
|
||||||
|
|
||||||
|
**返回值:** 更新后的匹配结果
|
||||||
|
|
||||||
|
### 11. 获取匹配统计信息
|
||||||
|
|
||||||
|
获取匹配结果的统计信息。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
get_matching_statistics(project_id?: string): Promise<MatchingStatistics>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `project_id`: 可选的项目ID,如果提供则只统计该项目的数据
|
||||||
|
|
||||||
|
**返回值:** 匹配统计信息
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MatchingStatistics {
|
||||||
|
total_results: number; // 总结果数
|
||||||
|
successful_results: number; // 成功结果数
|
||||||
|
total_segments: number; // 总片段数
|
||||||
|
matched_segments: number; // 匹配片段数
|
||||||
|
total_materials: number; // 总素材数
|
||||||
|
total_models: number; // 总模特数
|
||||||
|
average_success_rate: number; // 平均成功率
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12. 执行匹配并自动保存
|
||||||
|
|
||||||
|
执行素材匹配并自动保存结果到数据库。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
execute_material_matching_with_save(
|
||||||
|
request: MaterialMatchingRequest,
|
||||||
|
result_name: string,
|
||||||
|
description?: string
|
||||||
|
): Promise<[MaterialMatchingResult, TemplateMatchingResult | null]>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `request`: 素材匹配请求
|
||||||
|
- `result_name`: 结果名称
|
||||||
|
- `description`: 可选的结果描述
|
||||||
|
|
||||||
|
**返回值:** 包含匹配结果和保存结果的元组
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
所有 API 接口都会返回 `Result<T, String>` 类型,其中错误信息为字符串格式。常见错误包括:
|
||||||
|
|
||||||
|
- `"模板匹配结果不存在"`: 指定的匹配结果ID不存在
|
||||||
|
- `"项目不存在"`: 指定的项目ID不存在
|
||||||
|
- `"模板不存在"`: 指定的模板ID不存在
|
||||||
|
- `"数据库操作失败"`: 数据库操作出现错误
|
||||||
|
- `"参数验证失败"`: 输入参数不符合要求
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 保存匹配结果
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
// 执行匹配并保存结果
|
||||||
|
const result = await invoke('execute_material_matching_with_save', {
|
||||||
|
request: {
|
||||||
|
project_id: 'project-1',
|
||||||
|
template_id: 'template-1',
|
||||||
|
binding_id: 'binding-1',
|
||||||
|
overwrite_existing: false,
|
||||||
|
},
|
||||||
|
resultName: '我的匹配结果',
|
||||||
|
description: '这是一个测试匹配结果',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('匹配结果:', result[0]);
|
||||||
|
console.log('保存结果:', result[1]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询匹配结果
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 获取项目的所有匹配结果
|
||||||
|
const results = await invoke('get_project_matching_results', {
|
||||||
|
projectId: 'project-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取匹配结果详情
|
||||||
|
const detail = await invoke('get_matching_result_detail', {
|
||||||
|
resultId: 'result-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 查询匹配结果(带过滤条件)
|
||||||
|
const filteredResults = await invoke('list_matching_results', {
|
||||||
|
options: {
|
||||||
|
project_id: 'project-1',
|
||||||
|
status: 'Success',
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
sort_by: 'created_at',
|
||||||
|
sort_order: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 管理匹配结果
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 更新匹配结果信息
|
||||||
|
await invoke('update_matching_result_info', {
|
||||||
|
resultId: 'result-1',
|
||||||
|
resultName: '更新后的名称',
|
||||||
|
description: '更新后的描述',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置质量评分
|
||||||
|
await invoke('set_matching_result_quality_score', {
|
||||||
|
resultId: 'result-1',
|
||||||
|
qualityScore: 4.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 软删除匹配结果
|
||||||
|
await invoke('soft_delete_matching_result', {
|
||||||
|
resultId: 'result-1',
|
||||||
|
});
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user