feat: 完成模板匹配功能优化
- 修复重复资源使用问题: * 修改MaterialMatchingService中的匹配算法,确保每个素材片段在一次匹配中只能被使用一次 * 添加get_classified_segments_with_exclusions方法支持额外排除片段 * 重构match_materials_with_used_segments方法正确处理全局使用状态 * 更新批量匹配逻辑使用新的匹配方法 - 添加批量删除匹配记录功能: * 在TemplateMatchingResultService中添加批量删除方法 * 在MaterialUsageRepository中添加批量删除使用记录的方法 * 删除匹配记录时自动重置相关资源的使用状态 * 添加相应的Tauri命令和API接口 - 添加匹配记录导出状态标识: * 在TemplateMatchingResult模型中添加is_exported和last_exported_at字段 * 更新数据库schema和仓库层支持新字段 * 在导出功能中自动更新导出状态 * 添加重置导出状态的功能 - 优化一键匹配命名逻辑: * 改进命名规则使用模板名称+序号格式 * 为每个模板维护独立的序号计数器 * 支持自定义前缀的命名方式 - 更新前端组件支持新功能: * 在TemplateMatchingResultManager中添加批量选择和批量删除功能 * 在TemplateMatchingResultCard中添加选择框和导出状态显示 * 添加全选/取消全选功能 * 优化UI显示导出状态标识 - 数据库迁移: * 添加is_exported和last_exported_at字段到template_matching_results表 * 保持向后兼容性
This commit is contained in:
@@ -321,11 +321,22 @@ impl MaterialMatchingService {
|
|||||||
materials: &[Material],
|
materials: &[Material],
|
||||||
classification_records: &HashMap<String, Vec<VideoClassificationRecord>>,
|
classification_records: &HashMap<String, Vec<VideoClassificationRecord>>,
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
|
) -> Result<Vec<(MaterialSegment, String)>> {
|
||||||
|
self.get_classified_segments_with_exclusions(materials, classification_records, project_id, &HashSet::new()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取已分类的素材片段(排除已使用的片段和额外排除的片段)
|
||||||
|
async fn get_classified_segments_with_exclusions(
|
||||||
|
&self,
|
||||||
|
materials: &[Material],
|
||||||
|
classification_records: &HashMap<String, Vec<VideoClassificationRecord>>,
|
||||||
|
project_id: &str,
|
||||||
|
additional_used_segments: &HashSet<String>,
|
||||||
) -> Result<Vec<(MaterialSegment, String)>> {
|
) -> Result<Vec<(MaterialSegment, String)>> {
|
||||||
let mut classified_segments = Vec::new();
|
let mut classified_segments = Vec::new();
|
||||||
|
|
||||||
// 获取项目中已使用的素材片段ID列表
|
// 获取项目中已使用的素材片段ID列表(从数据库)
|
||||||
let used_segment_ids = match self.material_usage_repo.get_usage_records_by_project(project_id) {
|
let mut used_segment_ids = match self.material_usage_repo.get_usage_records_by_project(project_id) {
|
||||||
Ok(usage_records) => {
|
Ok(usage_records) => {
|
||||||
usage_records.into_iter()
|
usage_records.into_iter()
|
||||||
.map(|record| record.material_segment_id)
|
.map(|record| record.material_segment_id)
|
||||||
@@ -338,6 +349,9 @@ impl MaterialMatchingService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 合并额外的已使用片段ID
|
||||||
|
used_segment_ids.extend(additional_used_segments.iter().cloned());
|
||||||
|
|
||||||
for material in materials {
|
for material in materials {
|
||||||
|
|
||||||
// 只处理有分类记录的素材
|
// 只处理有分类记录的素材
|
||||||
@@ -644,6 +658,9 @@ impl MaterialMatchingService {
|
|||||||
let mut termination_reason = String::new();
|
let mut termination_reason = String::new();
|
||||||
let mut materials_exhausted = false;
|
let mut materials_exhausted = false;
|
||||||
|
|
||||||
|
// 为每个模板维护独立的序号计数器
|
||||||
|
let mut template_counters: HashMap<String, u32> = HashMap::new();
|
||||||
|
|
||||||
// 获取项目中已使用的素材片段ID列表(从数据库)
|
// 获取项目中已使用的素材片段ID列表(从数据库)
|
||||||
let existing_used_segments = match self.material_usage_repo.get_usage_records_by_project(&request.project_id) {
|
let existing_used_segments = match self.material_usage_repo.get_usage_records_by_project(&request.project_id) {
|
||||||
Ok(usage_records) => {
|
Ok(usage_records) => {
|
||||||
@@ -705,18 +722,24 @@ impl MaterialMatchingService {
|
|||||||
overwrite_existing: request.overwrite_existing,
|
overwrite_existing: request.overwrite_existing,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result_name = format!(
|
// 改进的命名逻辑:模板名称 + 序号(每个模板独立计数)
|
||||||
"{}-{}-R{}",
|
let template_counter = template_counters.entry(binding_detail.template_name.clone()).or_insert(0);
|
||||||
request.result_name_prefix.as_deref().unwrap_or("一键匹配"),
|
*template_counter += 1;
|
||||||
binding_detail.template_name,
|
|
||||||
total_rounds
|
|
||||||
);
|
|
||||||
|
|
||||||
match self.match_materials_and_save(matching_request, result_name, None).await {
|
let result_name = if let Some(prefix) = &request.result_name_prefix {
|
||||||
Ok((matching_result, saved_result)) => {
|
format!("{}-{}-{:03}", prefix, binding_detail.template_name, *template_counter)
|
||||||
|
} else {
|
||||||
|
format!("{}-{:03}", binding_detail.template_name, *template_counter)
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.match_materials_with_used_segments(matching_request, result_name, &global_used_segment_ids).await {
|
||||||
|
Ok((matching_result, saved_result, newly_used_segments)) => {
|
||||||
round_successful_matches += 1;
|
round_successful_matches += 1;
|
||||||
successful_matches += 1;
|
successful_matches += 1;
|
||||||
|
|
||||||
|
// 更新全局已使用片段列表
|
||||||
|
global_used_segment_ids.extend(newly_used_segments);
|
||||||
|
|
||||||
matching_results.push(BatchMatchingItemResult {
|
matching_results.push(BatchMatchingItemResult {
|
||||||
binding_id: binding_detail.binding.id.clone(),
|
binding_id: binding_detail.binding.id.clone(),
|
||||||
template_id: binding_detail.binding.template_id.clone(),
|
template_id: binding_detail.binding.template_id.clone(),
|
||||||
@@ -885,83 +908,7 @@ impl MaterialMatchingService {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取已分类的素材片段(排除指定的已使用片段)
|
|
||||||
async fn get_classified_segments_with_exclusions(
|
|
||||||
&self,
|
|
||||||
materials: &[Material],
|
|
||||||
classification_records: &HashMap<String, Vec<VideoClassificationRecord>>,
|
|
||||||
project_id: &str,
|
|
||||||
additional_used_segments: &HashSet<String>,
|
|
||||||
) -> Result<Vec<(MaterialSegment, String)>> {
|
|
||||||
// 获取数据库中已使用的素材片段ID列表
|
|
||||||
let db_used_segment_ids = match self.material_usage_repo.get_usage_records_by_project(project_id) {
|
|
||||||
Ok(usage_records) => {
|
|
||||||
usage_records.into_iter()
|
|
||||||
.map(|record| record.material_segment_id)
|
|
||||||
.collect::<HashSet<String>>()
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("警告:获取素材使用记录失败: {},将继续进行匹配", e);
|
|
||||||
HashSet::new()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 合并所有已使用的片段ID
|
|
||||||
let mut all_used_segments = db_used_segment_ids;
|
|
||||||
all_used_segments.extend(additional_used_segments.iter().cloned());
|
|
||||||
|
|
||||||
let mut classified_segments = Vec::new();
|
|
||||||
|
|
||||||
for material in materials {
|
|
||||||
if let Some(records) = classification_records.get(&material.id) {
|
|
||||||
if records.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if material.segments.is_empty() {
|
|
||||||
// 处理虚拟片段
|
|
||||||
if let Some(duration) = material.get_duration() {
|
|
||||||
for record in records {
|
|
||||||
if all_used_segments.contains(&record.segment_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let virtual_segment = MaterialSegment {
|
|
||||||
id: record.segment_id.clone(),
|
|
||||||
material_id: material.id.clone(),
|
|
||||||
segment_index: 0,
|
|
||||||
start_time: 0.0,
|
|
||||||
end_time: duration,
|
|
||||||
duration,
|
|
||||||
file_path: material.original_path.clone(),
|
|
||||||
file_size: material.file_size,
|
|
||||||
thumbnail_path: material.thumbnail_path.clone(),
|
|
||||||
usage_count: 0,
|
|
||||||
is_used: false,
|
|
||||||
last_used_at: None,
|
|
||||||
created_at: chrono::Utc::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
classified_segments.push((virtual_segment, record.category.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 处理实际片段
|
|
||||||
for segment in &material.segments {
|
|
||||||
if all_used_segments.contains(&segment.id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(record) = records.iter().find(|r| r.segment_id == segment.id) {
|
|
||||||
classified_segments.push((segment.clone(), record.category.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(classified_segments)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 使用指定的已使用素材列表进行匹配
|
/// 使用指定的已使用素材列表进行匹配
|
||||||
async fn match_materials_with_used_segments(
|
async fn match_materials_with_used_segments(
|
||||||
@@ -970,16 +917,117 @@ impl MaterialMatchingService {
|
|||||||
result_name: String,
|
result_name: String,
|
||||||
used_segment_ids: &HashSet<String>,
|
used_segment_ids: &HashSet<String>,
|
||||||
) -> Result<(MaterialMatchingResult, Option<crate::data::models::template_matching_result::TemplateMatchingResult>, HashSet<String>)> {
|
) -> Result<(MaterialMatchingResult, Option<crate::data::models::template_matching_result::TemplateMatchingResult>, HashSet<String>)> {
|
||||||
// 这里需要实现一个修改版的匹配逻辑,考虑额外的已使用素材
|
// 获取模板信息
|
||||||
// 为了简化,暂时使用现有的匹配方法,但这需要进一步优化
|
let template = self.template_service.get_template_by_id(&request.template_id)
|
||||||
let (matching_result, saved_result) = self.match_materials_and_save(request, result_name, None).await?;
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("模板不存在: {}", request.template_id))?;
|
||||||
|
|
||||||
// 收集本次匹配使用的素材片段ID
|
// 获取项目的所有素材
|
||||||
let newly_used_segments: HashSet<String> = matching_result.matches.iter()
|
let project_materials = self.material_repo.get_by_project_id(&request.project_id)?;
|
||||||
.map(|m| m.material_segment_id.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok((matching_result, saved_result, newly_used_segments))
|
// 获取所有素材的分类记录
|
||||||
|
let mut classification_records = HashMap::new();
|
||||||
|
for material in &project_materials {
|
||||||
|
let records = self.video_classification_repo.get_by_material_id(&material.id).await?;
|
||||||
|
classification_records.insert(material.id.clone(), records);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可用的素材片段(排除已使用的片段)
|
||||||
|
let available_segments = self.get_classified_segments_with_exclusions(
|
||||||
|
&project_materials,
|
||||||
|
&classification_records,
|
||||||
|
&request.project_id,
|
||||||
|
used_segment_ids
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// 执行匹配算法
|
||||||
|
let mut matches = Vec::new();
|
||||||
|
let mut failed_segments = Vec::new();
|
||||||
|
let mut fixed_segments = Vec::new();
|
||||||
|
let mut local_used_segment_ids = HashSet::new();
|
||||||
|
let mut used_model_ids = HashSet::new();
|
||||||
|
|
||||||
|
// 获取所有需要匹配的轨道片段
|
||||||
|
let track_segments = self.get_template_track_segments(&template).await?;
|
||||||
|
|
||||||
|
for track_segment in &track_segments {
|
||||||
|
// 检查是否为固定素材
|
||||||
|
if track_segment.matching_rule.is_fixed_material() {
|
||||||
|
fixed_segments.push(track_segment.clone());
|
||||||
|
continue; // 固定素材跳过匹配,不计入失败
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配片段
|
||||||
|
match self.match_single_segment(
|
||||||
|
track_segment,
|
||||||
|
&available_segments,
|
||||||
|
&classification_records,
|
||||||
|
&project_materials,
|
||||||
|
&mut local_used_segment_ids,
|
||||||
|
).await {
|
||||||
|
Ok(segment_match) => {
|
||||||
|
// 收集使用的模特ID
|
||||||
|
if let Some(model_name) = &segment_match.model_name {
|
||||||
|
if !model_name.is_empty() {
|
||||||
|
used_model_ids.insert(model_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches.push(segment_match);
|
||||||
|
}
|
||||||
|
Err(error_msg) => {
|
||||||
|
failed_segments.push(FailedSegmentMatch {
|
||||||
|
track_segment_id: track_segment.id.clone(),
|
||||||
|
track_segment_name: track_segment.name.clone(),
|
||||||
|
matching_rule: track_segment.matching_rule.clone(),
|
||||||
|
failure_reason: error_msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算统计信息
|
||||||
|
let total_segments = track_segments.len() as u32;
|
||||||
|
let matched_segments = matches.len() as u32;
|
||||||
|
let failed_segments_count = failed_segments.len() as u32;
|
||||||
|
let fixed_segments_count = fixed_segments.len() as u32;
|
||||||
|
let success_rate = if total_segments > 0 {
|
||||||
|
(matched_segments + fixed_segments_count) as f64 / total_segments as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建匹配结果
|
||||||
|
let matching_result = MaterialMatchingResult {
|
||||||
|
binding_id: request.binding_id.clone(),
|
||||||
|
template_id: request.template_id.clone(),
|
||||||
|
project_id: request.project_id.clone(),
|
||||||
|
matches,
|
||||||
|
statistics: MatchingStatistics {
|
||||||
|
total_segments,
|
||||||
|
matched_segments,
|
||||||
|
failed_segments: failed_segments_count,
|
||||||
|
success_rate,
|
||||||
|
used_materials: local_used_segment_ids.len() as u32,
|
||||||
|
used_models: used_model_ids.len() as u32,
|
||||||
|
},
|
||||||
|
failed_segments,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存匹配结果到数据库
|
||||||
|
let saved_result = if let Some(result_service) = &self.matching_result_service {
|
||||||
|
let saved = result_service.save_matching_result(
|
||||||
|
&matching_result,
|
||||||
|
result_name,
|
||||||
|
None,
|
||||||
|
0, // 匹配耗时,这里简化为0
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Some(saved)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((matching_result, saved_result, local_used_segment_ids))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 计算批量匹配汇总信息
|
/// 计算批量匹配汇总信息
|
||||||
|
|||||||
@@ -200,6 +200,62 @@ impl TemplateMatchingResultService {
|
|||||||
Ok(self.repository.soft_delete(result_id)?)
|
Ok(self.repository.soft_delete(result_id)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 批量删除匹配结果
|
||||||
|
pub async fn batch_delete_matching_results(&self, result_ids: &[String]) -> Result<u32> {
|
||||||
|
let mut deleted_count = 0;
|
||||||
|
|
||||||
|
for result_id in result_ids {
|
||||||
|
if self.repository.delete(result_id)? {
|
||||||
|
deleted_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(deleted_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量软删除匹配结果
|
||||||
|
pub async fn batch_soft_delete_matching_results(&self, result_ids: &[String]) -> Result<u32> {
|
||||||
|
let mut deleted_count = 0;
|
||||||
|
|
||||||
|
for result_id in result_ids {
|
||||||
|
if self.repository.soft_delete(result_id)? {
|
||||||
|
deleted_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(deleted_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量删除匹配结果并重置资源使用状态
|
||||||
|
pub async fn batch_delete_matching_results_with_usage_reset(
|
||||||
|
&self,
|
||||||
|
result_ids: &[String],
|
||||||
|
material_usage_repo: Arc<crate::data::repositories::material_usage_repository::MaterialUsageRepository>
|
||||||
|
) -> Result<(u32, u32)> {
|
||||||
|
// 先删除使用记录并重置资源状态
|
||||||
|
let deleted_usage_records = material_usage_repo.delete_usage_records_by_matching_results(result_ids)?;
|
||||||
|
|
||||||
|
// 再删除匹配结果
|
||||||
|
let deleted_results = self.batch_delete_matching_results(result_ids).await?;
|
||||||
|
|
||||||
|
Ok((deleted_results, deleted_usage_records))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量软删除匹配结果并重置资源使用状态
|
||||||
|
pub async fn batch_soft_delete_matching_results_with_usage_reset(
|
||||||
|
&self,
|
||||||
|
result_ids: &[String],
|
||||||
|
material_usage_repo: Arc<crate::data::repositories::material_usage_repository::MaterialUsageRepository>
|
||||||
|
) -> Result<(u32, u32)> {
|
||||||
|
// 先删除使用记录并重置资源状态
|
||||||
|
let deleted_usage_records = material_usage_repo.delete_usage_records_by_matching_results(result_ids)?;
|
||||||
|
|
||||||
|
// 再软删除匹配结果
|
||||||
|
let deleted_results = self.batch_soft_delete_matching_results(result_ids).await?;
|
||||||
|
|
||||||
|
Ok((deleted_results, deleted_usage_records))
|
||||||
|
}
|
||||||
|
|
||||||
/// 更新匹配结果名称和描述
|
/// 更新匹配结果名称和描述
|
||||||
pub async fn update_matching_result_info(
|
pub async fn update_matching_result_info(
|
||||||
&self,
|
&self,
|
||||||
@@ -349,6 +405,10 @@ impl TemplateMatchingResultService {
|
|||||||
std::fs::write(&output_file_path, json_content)
|
std::fs::write(&output_file_path, json_content)
|
||||||
.map_err(|e| anyhow!("写入文件失败: {}", e))?;
|
.map_err(|e| anyhow!("写入文件失败: {}", e))?;
|
||||||
|
|
||||||
|
// 更新导出状态
|
||||||
|
self.repository.increment_export_count(result_id)?;
|
||||||
|
|
||||||
|
println!("✅ 导出成功: {}", output_file_path);
|
||||||
Ok(output_file_path)
|
Ok(output_file_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,6 +456,9 @@ impl TemplateMatchingResultService {
|
|||||||
std::fs::write(&output_file_path, json_content)
|
std::fs::write(&output_file_path, json_content)
|
||||||
.map_err(|e| anyhow!("写入文件失败 {}: {}", output_file_path, e))?;
|
.map_err(|e| anyhow!("写入文件失败 {}: {}", output_file_path, e))?;
|
||||||
|
|
||||||
|
// 更新导出状态
|
||||||
|
self.repository.increment_export_count(result_id)?;
|
||||||
|
|
||||||
println!("✅ 导出成功: {}", output_file_path);
|
println!("✅ 导出成功: {}", output_file_path);
|
||||||
Ok(output_file_path)
|
Ok(output_file_path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ pub struct TemplateMatchingResult {
|
|||||||
pub status: MatchingResultStatus,
|
pub status: MatchingResultStatus,
|
||||||
pub metadata: Option<String>, // JSON格式的额外元数据
|
pub metadata: Option<String>, // JSON格式的额外元数据
|
||||||
pub export_count: u32, // 导出次数
|
pub export_count: u32, // 导出次数
|
||||||
|
pub is_exported: bool, // 是否已导出
|
||||||
|
pub last_exported_at: Option<DateTime<Utc>>, // 最后导出时间
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
@@ -138,6 +140,8 @@ impl TemplateMatchingResult {
|
|||||||
status: MatchingResultStatus::default(),
|
status: MatchingResultStatus::default(),
|
||||||
metadata: None,
|
metadata: None,
|
||||||
export_count: 0,
|
export_count: 0,
|
||||||
|
is_exported: false,
|
||||||
|
last_exported_at: None,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
@@ -209,6 +213,21 @@ impl TemplateMatchingResult {
|
|||||||
MatchingResultStatus::Cancelled => "已取消".to_string(),
|
MatchingResultStatus::Cancelled => "已取消".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 标记为已导出
|
||||||
|
pub fn mark_as_exported(&mut self) {
|
||||||
|
self.is_exported = true;
|
||||||
|
self.export_count += 1;
|
||||||
|
self.last_exported_at = Some(Utc::now());
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置导出状态
|
||||||
|
pub fn reset_export_status(&mut self) {
|
||||||
|
self.is_exported = false;
|
||||||
|
self.last_exported_at = None;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MatchingSegmentResult {
|
impl MatchingSegmentResult {
|
||||||
|
|||||||
@@ -202,6 +202,52 @@ impl MaterialUsageRepository {
|
|||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 批量删除使用记录(按模板匹配结果ID)
|
||||||
|
pub fn delete_usage_records_by_matching_results(&self, matching_result_ids: &[String]) -> Result<u32> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
let tx = conn.unchecked_transaction()?;
|
||||||
|
|
||||||
|
let mut total_deleted = 0;
|
||||||
|
let mut affected_segment_ids = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
for matching_result_id in matching_result_ids {
|
||||||
|
// 获取要删除的记录的片段ID
|
||||||
|
let mut stmt = tx.prepare(
|
||||||
|
"SELECT material_segment_id FROM material_usage_records WHERE template_matching_result_id = ?1"
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let segment_ids: Result<Vec<String>, _> = stmt.query_map([matching_result_id], |row| {
|
||||||
|
Ok(row.get::<_, String>(0)?)
|
||||||
|
})?.collect();
|
||||||
|
|
||||||
|
let segment_ids = segment_ids?;
|
||||||
|
for segment_id in segment_ids {
|
||||||
|
affected_segment_ids.insert(segment_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除使用记录
|
||||||
|
let deleted = tx.execute(
|
||||||
|
"DELETE FROM material_usage_records WHERE template_matching_result_id = ?1",
|
||||||
|
[matching_result_id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
total_deleted += deleted as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置所有受影响片段的使用状态
|
||||||
|
for segment_id in &affected_segment_ids {
|
||||||
|
self.update_segment_usage_status_in_tx(&tx, segment_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
Ok(total_deleted)
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取素材使用统计信息
|
/// 获取素材使用统计信息
|
||||||
pub fn get_material_usage_stats(&self, project_id: &str) -> Result<Vec<MaterialUsageStats>> {
|
pub fn get_material_usage_stats(&self, project_id: &str) -> Result<Vec<MaterialUsageStats>> {
|
||||||
let conn = self.database.get_connection();
|
let conn = self.database.get_connection();
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ impl TemplateMatchingResultRepository {
|
|||||||
id, project_id, template_id, binding_id, result_name, description,
|
id, project_id, template_id, binding_id, result_name, description,
|
||||||
total_segments, matched_segments, failed_segments, success_rate,
|
total_segments, matched_segments, failed_segments, success_rate,
|
||||||
used_materials, used_models, matching_duration_ms, quality_score,
|
used_materials, used_models, matching_duration_ms, quality_score,
|
||||||
status, metadata, export_count, created_at, updated_at, is_active
|
status, metadata, export_count, is_exported, last_exported_at,
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)",
|
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, ?20, ?21, ?22)",
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
&final_result.id,
|
&final_result.id,
|
||||||
&final_result.project_id,
|
&final_result.project_id,
|
||||||
@@ -64,6 +65,8 @@ impl TemplateMatchingResultRepository {
|
|||||||
&serde_json::to_string(&final_result.status).unwrap(),
|
&serde_json::to_string(&final_result.status).unwrap(),
|
||||||
&final_result.metadata,
|
&final_result.metadata,
|
||||||
&final_result.export_count,
|
&final_result.export_count,
|
||||||
|
&(final_result.is_exported as i32),
|
||||||
|
&final_result.last_exported_at.map(|dt| dt.to_rfc3339()),
|
||||||
&final_result.created_at.to_rfc3339(),
|
&final_result.created_at.to_rfc3339(),
|
||||||
&final_result.updated_at.to_rfc3339(),
|
&final_result.updated_at.to_rfc3339(),
|
||||||
&(final_result.is_active as i32),
|
&(final_result.is_active as i32),
|
||||||
@@ -107,8 +110,9 @@ impl TemplateMatchingResultRepository {
|
|||||||
result_name = ?1, description = ?2, total_segments = ?3,
|
result_name = ?1, description = ?2, total_segments = ?3,
|
||||||
matched_segments = ?4, failed_segments = ?5, success_rate = ?6,
|
matched_segments = ?4, failed_segments = ?5, success_rate = ?6,
|
||||||
used_materials = ?7, used_models = ?8, matching_duration_ms = ?9,
|
used_materials = ?7, used_models = ?8, matching_duration_ms = ?9,
|
||||||
quality_score = ?10, status = ?11, metadata = ?12, updated_at = ?13
|
quality_score = ?10, status = ?11, metadata = ?12, export_count = ?13,
|
||||||
WHERE id = ?14",
|
is_exported = ?14, last_exported_at = ?15, updated_at = ?16
|
||||||
|
WHERE id = ?17",
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
&result.result_name,
|
&result.result_name,
|
||||||
&result.description,
|
&result.description,
|
||||||
@@ -122,6 +126,9 @@ impl TemplateMatchingResultRepository {
|
|||||||
&result.quality_score.map(|s| s.to_string()),
|
&result.quality_score.map(|s| s.to_string()),
|
||||||
&serde_json::to_string(&result.status).unwrap(),
|
&serde_json::to_string(&result.status).unwrap(),
|
||||||
&result.metadata,
|
&result.metadata,
|
||||||
|
&result.export_count.to_string(),
|
||||||
|
&(result.is_exported as i32),
|
||||||
|
&result.last_exported_at.map(|dt| dt.to_rfc3339()),
|
||||||
&result.updated_at.to_rfc3339(),
|
&result.updated_at.to_rfc3339(),
|
||||||
&result.id,
|
&result.id,
|
||||||
],
|
],
|
||||||
@@ -478,6 +485,22 @@ impl TemplateMatchingResultRepository {
|
|||||||
_ => true, // 默认为 true
|
_ => true, // 默认为 true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理 is_exported 字段
|
||||||
|
let is_exported = match row.get::<_, rusqlite::types::Value>("is_exported") {
|
||||||
|
Ok(rusqlite::types::Value::Integer(i)) => i != 0,
|
||||||
|
Ok(rusqlite::types::Value::Text(s)) => s == "1" || s.to_lowercase() == "true",
|
||||||
|
Ok(rusqlite::types::Value::Real(f)) => f != 0.0,
|
||||||
|
_ => false, // 默认为 false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 last_exported_at 字段
|
||||||
|
let last_exported_at = match row.get::<_, Option<String>>("last_exported_at") {
|
||||||
|
Ok(Some(date_str)) => DateTime::parse_from_rfc3339(&date_str)
|
||||||
|
.ok()
|
||||||
|
.map(|dt| dt.with_timezone(&Utc)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(TemplateMatchingResult {
|
Ok(TemplateMatchingResult {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
project_id: row.get("project_id")?,
|
project_id: row.get("project_id")?,
|
||||||
@@ -496,6 +519,8 @@ impl TemplateMatchingResultRepository {
|
|||||||
status,
|
status,
|
||||||
metadata: row.get("metadata")?,
|
metadata: row.get("metadata")?,
|
||||||
export_count: row.get("export_count").unwrap_or(0),
|
export_count: row.get("export_count").unwrap_or(0),
|
||||||
|
is_exported,
|
||||||
|
last_exported_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
is_active,
|
is_active,
|
||||||
@@ -562,14 +587,31 @@ impl TemplateMatchingResultRepository {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 增加导出次数
|
/// 增加导出次数并标记为已导出
|
||||||
pub fn increment_export_count(&self, result_id: &str) -> Result<()> {
|
pub fn increment_export_count(&self, result_id: &str) -> Result<()> {
|
||||||
let conn = self.database.get_connection();
|
let conn = self.database.get_connection();
|
||||||
let conn = conn.lock().unwrap();
|
let conn = conn.lock().unwrap();
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE template_matching_results
|
||||||
|
SET export_count = export_count + 1, is_exported = 1,
|
||||||
|
last_exported_at = ?2, updated_at = ?3
|
||||||
|
WHERE id = ?1",
|
||||||
|
rusqlite::params![result_id, now.to_rfc3339(), now.to_rfc3339()],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置导出状态
|
||||||
|
pub fn reset_export_status(&self, result_id: &str) -> Result<()> {
|
||||||
|
let conn = self.database.get_connection();
|
||||||
|
let conn = conn.lock().unwrap();
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE template_matching_results
|
"UPDATE template_matching_results
|
||||||
SET export_count = export_count + 1, updated_at = ?2
|
SET is_exported = 0, last_exported_at = NULL, updated_at = ?2
|
||||||
WHERE id = ?1",
|
WHERE id = ?1",
|
||||||
rusqlite::params![result_id, Utc::now().to_rfc3339()],
|
rusqlite::params![result_id, Utc::now().to_rfc3339()],
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -1672,6 +1672,21 @@ impl Database {
|
|||||||
println!("Added export_count column to template_matching_results table");
|
println!("Added export_count column to template_matching_results table");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加导出状态字段到模板匹配结果表
|
||||||
|
let has_is_exported_column = conn.prepare("SELECT is_exported FROM template_matching_results LIMIT 1").is_ok();
|
||||||
|
if !has_is_exported_column {
|
||||||
|
println!("Adding export status columns to template_matching_results table");
|
||||||
|
conn.execute(
|
||||||
|
"ALTER TABLE template_matching_results ADD COLUMN is_exported BOOLEAN DEFAULT 0",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"ALTER TABLE template_matching_results ADD COLUMN last_exported_at DATETIME",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
println!("Added export status columns to template_matching_results table");
|
||||||
|
}
|
||||||
|
|
||||||
// 暂时禁用自动清理,避免启动时卡住
|
// 暂时禁用自动清理,避免启动时卡住
|
||||||
// self.cleanup_invalid_projects()?;
|
// self.cleanup_invalid_projects()?;
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,11 @@ pub fn run() {
|
|||||||
commands::template_matching_result_commands::list_matching_results,
|
commands::template_matching_result_commands::list_matching_results,
|
||||||
commands::template_matching_result_commands::delete_matching_result,
|
commands::template_matching_result_commands::delete_matching_result,
|
||||||
commands::template_matching_result_commands::soft_delete_matching_result,
|
commands::template_matching_result_commands::soft_delete_matching_result,
|
||||||
|
commands::template_matching_result_commands::batch_delete_matching_results,
|
||||||
|
commands::template_matching_result_commands::batch_soft_delete_matching_results,
|
||||||
|
commands::template_matching_result_commands::batch_delete_matching_results_with_usage_reset,
|
||||||
|
commands::template_matching_result_commands::batch_soft_delete_matching_results_with_usage_reset,
|
||||||
|
commands::template_matching_result_commands::reset_matching_result_export_status,
|
||||||
commands::template_matching_result_commands::update_matching_result_info,
|
commands::template_matching_result_commands::update_matching_result_info,
|
||||||
commands::template_matching_result_commands::set_matching_result_quality_score,
|
commands::template_matching_result_commands::set_matching_result_quality_score,
|
||||||
commands::template_matching_result_commands::get_matching_statistics,
|
commands::template_matching_result_commands::get_matching_statistics,
|
||||||
|
|||||||
@@ -153,6 +153,77 @@ pub async fn soft_delete_matching_result(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 批量删除匹配结果
|
||||||
|
#[command]
|
||||||
|
pub async fn batch_delete_matching_results(
|
||||||
|
result_ids: Vec<String>,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<u32, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.batch_delete_matching_results(&result_ids)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量软删除匹配结果
|
||||||
|
#[command]
|
||||||
|
pub async fn batch_soft_delete_matching_results(
|
||||||
|
result_ids: Vec<String>,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<u32, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.batch_soft_delete_matching_results(&result_ids)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量删除匹配结果并重置资源使用状态
|
||||||
|
#[command]
|
||||||
|
pub async fn batch_delete_matching_results_with_usage_reset(
|
||||||
|
result_ids: Vec<String>,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<(u32, u32), String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let material_usage_repo = Arc::new(crate::data::repositories::material_usage_repository::MaterialUsageRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.batch_delete_matching_results_with_usage_reset(&result_ids, material_usage_repo)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量软删除匹配结果并重置资源使用状态
|
||||||
|
#[command]
|
||||||
|
pub async fn batch_soft_delete_matching_results_with_usage_reset(
|
||||||
|
result_ids: Vec<String>,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<(u32, u32), String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
let material_usage_repo = Arc::new(crate::data::repositories::material_usage_repository::MaterialUsageRepository::new(database.inner().clone()));
|
||||||
|
let service = TemplateMatchingResultService::new(repository);
|
||||||
|
|
||||||
|
service.batch_soft_delete_matching_results_with_usage_reset(&result_ids, material_usage_repo)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置匹配结果的导出状态
|
||||||
|
#[command]
|
||||||
|
pub async fn reset_matching_result_export_status(
|
||||||
|
result_id: String,
|
||||||
|
database: State<'_, Arc<Database>>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
|
|
||||||
|
repository.reset_export_status(&result_id)
|
||||||
|
.map(|_| true)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// 更新匹配结果信息
|
/// 更新匹配结果信息
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn update_matching_result_info(
|
pub async fn update_matching_result_info(
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ interface TemplateMatchingResultCardProps {
|
|||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onExportToJianying?: () => void;
|
onExportToJianying?: () => void;
|
||||||
onExportToJianyingV2?: () => void;
|
onExportToJianyingV2?: () => void;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onToggleSelect?: () => void;
|
||||||
|
showExportStatus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
|
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
|
||||||
@@ -16,6 +19,9 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
|
|||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
onExportToJianyingV2,
|
onExportToJianyingV2,
|
||||||
|
isSelected = false,
|
||||||
|
onToggleSelect,
|
||||||
|
showExportStatus = false,
|
||||||
}) => {
|
}) => {
|
||||||
// 格式化时长显示
|
// 格式化时长显示
|
||||||
const formatDuration = (ms: number): string => {
|
const formatDuration = (ms: number): string => {
|
||||||
@@ -77,14 +83,39 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card card-interactive group">
|
<div className={`card card-interactive group ${isSelected ? 'ring-2 ring-blue-500 bg-blue-50' : ''}`}>
|
||||||
{/* 卡片头部 */}
|
{/* 卡片头部 */}
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
|
{/* 选择框 */}
|
||||||
|
{onToggleSelect && (
|
||||||
|
<div className="flex items-center mr-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={onToggleSelect}
|
||||||
|
className="form-checkbox h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 truncate group-hover:text-primary-600 transition-colors">
|
<h3 className="text-lg font-semibold text-gray-900 truncate group-hover:text-primary-600 transition-colors">
|
||||||
{result.result_name}
|
{result.result_name}
|
||||||
</h3>
|
</h3>
|
||||||
|
{/* 导出状态标识 */}
|
||||||
|
{showExportStatus && (
|
||||||
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
result.is_exported
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{result.is_exported ? '✓ 已导出' : '○ 未导出'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{result.description && (
|
{result.description && (
|
||||||
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
||||||
{result.description}
|
{result.description}
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
result: TemplateMatchingResult | null;
|
result: TemplateMatchingResult | null;
|
||||||
}>({ show: false, result: null });
|
}>({ show: false, result: null });
|
||||||
|
|
||||||
|
// 批量操作状态
|
||||||
|
const [selectedResults, setSelectedResults] = useState<Set<string>>(new Set());
|
||||||
|
const [batchDeleteConfirm, setBatchDeleteConfirm] = useState<{
|
||||||
|
show: boolean;
|
||||||
|
resultIds: string[];
|
||||||
|
}>({ show: false, resultIds: [] });
|
||||||
|
const [batchOperationLoading, setBatchOperationLoading] = useState(false);
|
||||||
|
|
||||||
// 过滤和排序状态
|
// 过滤和排序状态
|
||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
status?: MatchingResultStatus;
|
status?: MatchingResultStatus;
|
||||||
@@ -122,6 +130,51 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 批量删除匹配结果
|
||||||
|
const handleBatchDelete = async (resultIds: string[]) => {
|
||||||
|
setBatchOperationLoading(true);
|
||||||
|
try {
|
||||||
|
const [deletedResults, deletedUsageRecords] = await invoke<[number, number]>(
|
||||||
|
'batch_soft_delete_matching_results_with_usage_reset',
|
||||||
|
{ resultIds }
|
||||||
|
);
|
||||||
|
|
||||||
|
success(`成功删除 ${deletedResults} 个匹配结果,重置 ${deletedUsageRecords} 条使用记录`);
|
||||||
|
|
||||||
|
// 重新加载列表
|
||||||
|
await loadResults();
|
||||||
|
await loadStatistics();
|
||||||
|
|
||||||
|
// 清空选择
|
||||||
|
setSelectedResults(new Set());
|
||||||
|
setBatchDeleteConfirm({ show: false, resultIds: [] });
|
||||||
|
} catch (err) {
|
||||||
|
setError(`批量删除失败: ${err}`);
|
||||||
|
} finally {
|
||||||
|
setBatchOperationLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换选择状态
|
||||||
|
const handleToggleSelect = (resultId: string) => {
|
||||||
|
const newSelected = new Set(selectedResults);
|
||||||
|
if (newSelected.has(resultId)) {
|
||||||
|
newSelected.delete(resultId);
|
||||||
|
} else {
|
||||||
|
newSelected.add(resultId);
|
||||||
|
}
|
||||||
|
setSelectedResults(newSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全选/取消全选
|
||||||
|
const handleToggleSelectAll = () => {
|
||||||
|
if (selectedResults.size === results.length) {
|
||||||
|
setSelectedResults(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedResults(new Set(results.map(r => r.id)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 查看详情
|
// 查看详情
|
||||||
const handleViewDetail = (result: TemplateMatchingResult) => {
|
const handleViewDetail = (result: TemplateMatchingResult) => {
|
||||||
setSelectedResult(result);
|
setSelectedResult(result);
|
||||||
@@ -305,9 +358,38 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<div className="flex justify-between items-center mt-6 pt-4 border-t border-gray-100">
|
<div className="flex justify-between items-center mt-6 pt-4 border-t border-gray-100">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
<div className="text-sm text-gray-600 font-medium">
|
<div className="text-sm text-gray-600 font-medium">
|
||||||
共 {pagination.total} 个匹配结果
|
共 {pagination.total} 个匹配结果
|
||||||
</div>
|
</div>
|
||||||
|
{selectedResults.size > 0 && (
|
||||||
|
<div className="text-sm text-blue-600 font-medium">
|
||||||
|
已选择 {selectedResults.size} 个
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{/* 批量操作按钮 */}
|
||||||
|
{selectedResults.size > 0 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setBatchDeleteConfirm({
|
||||||
|
show: true,
|
||||||
|
resultIds: Array.from(selectedResults)
|
||||||
|
})}
|
||||||
|
disabled={batchOperationLoading}
|
||||||
|
className="btn btn-danger btn-sm"
|
||||||
|
>
|
||||||
|
{batchOperationLoading ? '删除中...' : `批量删除 (${selectedResults.size})`}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedResults(new Set())}
|
||||||
|
className="btn btn-secondary btn-sm"
|
||||||
|
>
|
||||||
|
取消选择
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loadResults();
|
loadResults();
|
||||||
@@ -319,6 +401,7 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 错误信息 */}
|
{/* 错误信息 */}
|
||||||
{error && (
|
{error && (
|
||||||
@@ -336,6 +419,42 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
icon="📊"
|
icon="📊"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 列表控制栏 */}
|
||||||
|
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border border-gray-200/50 p-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedResults.size === results.length && results.length > 0}
|
||||||
|
onChange={handleToggleSelectAll}
|
||||||
|
className="form-checkbox h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-gray-700">
|
||||||
|
{selectedResults.size === results.length && results.length > 0 ? '取消全选' : '全选'}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{selectedResults.size > 0 && (
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
已选择 {selectedResults.size} / {results.length} 个结果
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
显示导出状态
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
✓ 已导出
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||||
|
○ 未导出
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{results.map((result) => (
|
{results.map((result) => (
|
||||||
<TemplateMatchingResultCard
|
<TemplateMatchingResultCard
|
||||||
@@ -345,9 +464,13 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
onDelete={() => setDeleteConfirm({ show: true, result })}
|
onDelete={() => setDeleteConfirm({ show: true, result })}
|
||||||
onExportToJianying={() => handleExportToJianying(result)}
|
onExportToJianying={() => handleExportToJianying(result)}
|
||||||
onExportToJianyingV2={() => handleExportToJianyingV2(result)}
|
onExportToJianyingV2={() => handleExportToJianyingV2(result)}
|
||||||
|
isSelected={selectedResults.has(result.id)}
|
||||||
|
onToggleSelect={() => handleToggleSelect(result.id)}
|
||||||
|
showExportStatus={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 分页 */}
|
{/* 分页 */}
|
||||||
@@ -398,6 +521,15 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
onConfirm={() => deleteConfirm.result && handleDelete(deleteConfirm.result)}
|
onConfirm={() => deleteConfirm.result && handleDelete(deleteConfirm.result)}
|
||||||
onCancel={() => setDeleteConfirm({ show: false, result: null })}
|
onCancel={() => setDeleteConfirm({ show: false, result: null })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 批量删除确认对话框 */}
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
isOpen={batchDeleteConfirm.show}
|
||||||
|
title="批量删除匹配结果"
|
||||||
|
message={`确定要删除选中的 ${batchDeleteConfirm.resultIds.length} 个匹配结果吗?此操作将同时重置相关资源的使用状态,且不可撤销。`}
|
||||||
|
onConfirm={() => handleBatchDelete(batchDeleteConfirm.resultIds)}
|
||||||
|
onCancel={() => setBatchDeleteConfirm({ show: false, resultIds: [] })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export interface TemplateMatchingResult {
|
|||||||
status: MatchingResultStatus;
|
status: MatchingResultStatus;
|
||||||
metadata?: string;
|
metadata?: string;
|
||||||
export_count: number;
|
export_count: number;
|
||||||
|
is_exported: boolean;
|
||||||
|
last_exported_at?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user