feat: implement export record tracking system
- Add export_count field to template_matching_results table - Create ExportRecord data model with comprehensive tracking - Implement ExportRecordRepository for CRUD operations - Create ExportRecordService for business logic - Add export record tracking to jianying export functions - Create ExportRecordManager component with filtering and pagination - Add ExportRecordsPage with full management interface - Integrate export records into navigation and routing - Add Tauri commands for export record management - Include statistics, validation, and cleanup functionality Follows Tauri development specifications and frontend standards.
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use std::fs;
|
||||
|
||||
use crate::data::repositories::export_record_repository::ExportRecordRepository;
|
||||
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||
use crate::data::models::export_record::{
|
||||
ExportRecord, ExportType, ExportFormat,
|
||||
CreateExportRecordRequest, ExportRecordQueryOptions, ExportRecordStatistics,
|
||||
};
|
||||
|
||||
/// 导出记录服务
|
||||
/// 遵循 Tauri 开发规范的业务逻辑设计原则
|
||||
pub struct ExportRecordService {
|
||||
export_record_repository: Arc<ExportRecordRepository>,
|
||||
matching_result_repository: Arc<TemplateMatchingResultRepository>,
|
||||
}
|
||||
|
||||
impl ExportRecordService {
|
||||
/// 创建新的导出记录服务实例
|
||||
pub fn new(
|
||||
export_record_repository: Arc<ExportRecordRepository>,
|
||||
matching_result_repository: Arc<TemplateMatchingResultRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
export_record_repository,
|
||||
matching_result_repository,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建导出记录
|
||||
pub async fn create_export_record(
|
||||
&self,
|
||||
matching_result_id: String,
|
||||
export_type: ExportType,
|
||||
export_format: ExportFormat,
|
||||
file_path: String,
|
||||
metadata: Option<String>,
|
||||
) -> Result<ExportRecord> {
|
||||
let request = CreateExportRecordRequest {
|
||||
matching_result_id,
|
||||
export_type,
|
||||
export_format,
|
||||
file_path,
|
||||
metadata,
|
||||
};
|
||||
|
||||
let export_record = self.export_record_repository.create(request)
|
||||
.map_err(|e| anyhow::anyhow!("创建导出记录失败: {}", e))?;
|
||||
|
||||
Ok(export_record)
|
||||
}
|
||||
|
||||
/// 标记导出成功并更新匹配结果的导出次数
|
||||
pub async fn mark_export_success(
|
||||
&self,
|
||||
export_record_id: String,
|
||||
file_path: String,
|
||||
duration_ms: u64,
|
||||
) -> Result<()> {
|
||||
// 获取导出记录
|
||||
let mut export_record = self.export_record_repository.get_by_id(&export_record_id)
|
||||
.map_err(|e| anyhow::anyhow!("获取导出记录失败: {}", e))?
|
||||
.ok_or_else(|| anyhow::anyhow!("导出记录不存在: {}", export_record_id))?;
|
||||
|
||||
// 获取文件大小
|
||||
let file_size = match fs::metadata(&file_path) {
|
||||
Ok(metadata) => Some(metadata.len()),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
// 标记导出成功
|
||||
export_record.mark_success(file_size, duration_ms);
|
||||
|
||||
// 更新导出记录
|
||||
self.export_record_repository.update(&export_record)
|
||||
.map_err(|e| anyhow::anyhow!("更新导出记录失败: {}", e))?;
|
||||
|
||||
// 增加匹配结果的导出次数
|
||||
self.matching_result_repository.increment_export_count(&export_record.matching_result_id)
|
||||
.map_err(|e| anyhow::anyhow!("更新导出次数失败: {}", e))?;
|
||||
|
||||
println!("✅ 导出记录已更新: {} -> 成功", export_record_id);
|
||||
println!("📊 匹配结果导出次数已增加: {}", export_record.matching_result_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 标记导出失败
|
||||
pub async fn mark_export_failed(
|
||||
&self,
|
||||
export_record_id: String,
|
||||
error_message: String,
|
||||
duration_ms: u64,
|
||||
) -> Result<()> {
|
||||
// 获取导出记录
|
||||
let mut export_record = self.export_record_repository.get_by_id(&export_record_id)
|
||||
.map_err(|e| anyhow::anyhow!("获取导出记录失败: {}", e))?
|
||||
.ok_or_else(|| anyhow::anyhow!("导出记录不存在: {}", export_record_id))?;
|
||||
|
||||
// 标记导出失败
|
||||
export_record.mark_failed(error_message.clone(), duration_ms);
|
||||
|
||||
// 更新导出记录
|
||||
self.export_record_repository.update(&export_record)
|
||||
.map_err(|e| anyhow::anyhow!("更新导出记录失败: {}", e))?;
|
||||
|
||||
println!("❌ 导出记录已更新: {} -> 失败: {}", export_record_id, error_message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取导出记录详情
|
||||
pub async fn get_export_record(&self, id: &str) -> Result<Option<ExportRecord>> {
|
||||
let export_record = self.export_record_repository.get_by_id(id)
|
||||
.map_err(|e| anyhow::anyhow!("获取导出记录失败: {}", e))?;
|
||||
|
||||
Ok(export_record)
|
||||
}
|
||||
|
||||
/// 查询导出记录列表
|
||||
pub async fn list_export_records(
|
||||
&self,
|
||||
options: ExportRecordQueryOptions,
|
||||
) -> Result<Vec<ExportRecord>> {
|
||||
let export_records = self.export_record_repository.list(options)
|
||||
.map_err(|e| anyhow::anyhow!("查询导出记录列表失败: {}", e))?;
|
||||
|
||||
Ok(export_records)
|
||||
}
|
||||
|
||||
/// 删除导出记录
|
||||
pub async fn delete_export_record(&self, id: &str) -> Result<()> {
|
||||
self.export_record_repository.delete(id)
|
||||
.map_err(|e| anyhow::anyhow!("删除导出记录失败: {}", e))?;
|
||||
|
||||
println!("🗑️ 导出记录已删除: {}", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取项目的导出记录统计信息
|
||||
pub async fn get_project_export_statistics(
|
||||
&self,
|
||||
project_id: &str,
|
||||
) -> Result<ExportRecordStatistics> {
|
||||
let statistics = self.export_record_repository.get_statistics(Some(project_id))
|
||||
.map_err(|e| anyhow::anyhow!("获取导出统计信息失败: {}", e))?;
|
||||
|
||||
Ok(statistics)
|
||||
}
|
||||
|
||||
/// 获取全局导出记录统计信息
|
||||
pub async fn get_global_export_statistics(&self) -> Result<ExportRecordStatistics> {
|
||||
let statistics = self.export_record_repository.get_statistics(None)
|
||||
.map_err(|e| anyhow::anyhow!("获取全局导出统计信息失败: {}", e))?;
|
||||
|
||||
Ok(statistics)
|
||||
}
|
||||
|
||||
/// 清理过期的导出记录
|
||||
pub async fn cleanup_expired_records(&self, days: u32) -> Result<u32> {
|
||||
let cutoff_date = Utc::now() - chrono::Duration::days(days as i64);
|
||||
|
||||
// 查询过期的导出记录
|
||||
let options = ExportRecordQueryOptions {
|
||||
project_id: None,
|
||||
matching_result_id: None,
|
||||
template_id: None,
|
||||
export_type: None,
|
||||
export_status: None,
|
||||
limit: None,
|
||||
offset: None,
|
||||
search_keyword: None,
|
||||
sort_by: None,
|
||||
sort_order: None,
|
||||
date_range: Some(crate::data::models::export_record::DateRange {
|
||||
start_date: chrono::DateTime::from_timestamp(0, 0).unwrap_or_default(),
|
||||
end_date: cutoff_date,
|
||||
}),
|
||||
};
|
||||
|
||||
let expired_records = self.list_export_records(options).await?;
|
||||
let count = expired_records.len() as u32;
|
||||
|
||||
// 删除过期记录
|
||||
for record in expired_records {
|
||||
self.delete_export_record(&record.id).await?;
|
||||
}
|
||||
|
||||
println!("🧹 已清理 {} 条过期导出记录(超过 {} 天)", count, days);
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// 验证文件是否存在
|
||||
pub async fn validate_export_file(&self, export_record_id: &str) -> Result<bool> {
|
||||
let export_record = self.get_export_record(export_record_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("导出记录不存在: {}", export_record_id))?;
|
||||
|
||||
let file_exists = fs::metadata(&export_record.file_path).is_ok();
|
||||
|
||||
if !file_exists {
|
||||
println!("⚠️ 导出文件不存在: {}", export_record.file_path);
|
||||
}
|
||||
|
||||
Ok(file_exists)
|
||||
}
|
||||
|
||||
/// 获取匹配结果的所有导出记录
|
||||
pub async fn get_matching_result_exports(
|
||||
&self,
|
||||
matching_result_id: &str,
|
||||
) -> Result<Vec<ExportRecord>> {
|
||||
let options = ExportRecordQueryOptions {
|
||||
project_id: None,
|
||||
matching_result_id: Some(matching_result_id.to_string()),
|
||||
template_id: None,
|
||||
export_type: None,
|
||||
export_status: None,
|
||||
limit: None,
|
||||
offset: None,
|
||||
search_keyword: None,
|
||||
sort_by: Some("created_at".to_string()),
|
||||
sort_order: Some("desc".to_string()),
|
||||
date_range: None,
|
||||
};
|
||||
|
||||
self.list_export_records(options).await
|
||||
}
|
||||
|
||||
/// 获取项目的导出记录
|
||||
pub async fn get_project_exports(
|
||||
&self,
|
||||
project_id: &str,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
) -> Result<Vec<ExportRecord>> {
|
||||
let options = ExportRecordQueryOptions {
|
||||
project_id: Some(project_id.to_string()),
|
||||
matching_result_id: None,
|
||||
template_id: None,
|
||||
export_type: None,
|
||||
export_status: None,
|
||||
limit,
|
||||
offset,
|
||||
search_keyword: None,
|
||||
sort_by: Some("created_at".to_string()),
|
||||
sort_order: Some("desc".to_string()),
|
||||
date_range: None,
|
||||
};
|
||||
|
||||
self.list_export_records(options).await
|
||||
}
|
||||
|
||||
/// 重新导出(基于现有导出记录)
|
||||
pub async fn re_export(
|
||||
&self,
|
||||
export_record_id: &str,
|
||||
new_file_path: String,
|
||||
) -> Result<ExportRecord> {
|
||||
let original_record = self.get_export_record(export_record_id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("原始导出记录不存在: {}", export_record_id))?;
|
||||
|
||||
// 创建新的导出记录
|
||||
let new_record = self.create_export_record(
|
||||
original_record.matching_result_id,
|
||||
original_record.export_type,
|
||||
original_record.export_format,
|
||||
new_file_path,
|
||||
original_record.metadata,
|
||||
).await?;
|
||||
|
||||
println!("🔄 基于记录 {} 创建了新的导出记录: {}", export_record_id, new_record.id);
|
||||
|
||||
Ok(new_record)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ pub mod enhanced_template_import_service;
|
||||
pub mod project_template_binding_service;
|
||||
pub mod material_matching_service;
|
||||
pub mod template_matching_result_service;
|
||||
pub mod export_record_service;
|
||||
pub mod video_generation_service;
|
||||
pub mod jianying_export;
|
||||
|
||||
|
||||
202
apps/desktop/src-tauri/src/data/models/export_record.rs
Normal file
202
apps/desktop/src-tauri/src/data/models/export_record.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// 导出记录实体模型
|
||||
/// 遵循 Tauri 开发规范的数据模型设计原则
|
||||
/// 记录每次导出操作的详细信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExportRecord {
|
||||
pub id: String,
|
||||
pub matching_result_id: String, // 关联的匹配结果ID
|
||||
pub project_id: String, // 项目ID
|
||||
pub template_id: String, // 模板ID
|
||||
pub export_type: ExportType, // 导出类型
|
||||
pub export_format: ExportFormat, // 导出格式
|
||||
pub file_path: String, // 导出文件路径
|
||||
pub file_size: Option<u64>, // 文件大小(字节)
|
||||
pub export_status: ExportStatus, // 导出状态
|
||||
pub export_duration_ms: u64, // 导出耗时(毫秒)
|
||||
pub error_message: Option<String>, // 错误信息
|
||||
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 ExportType {
|
||||
/// 剪映导出 V1
|
||||
JianYingV1,
|
||||
/// 剪映导出 V2
|
||||
JianYingV2,
|
||||
/// JSON格式导出
|
||||
Json,
|
||||
/// CSV格式导出
|
||||
Csv,
|
||||
/// Excel格式导出
|
||||
Excel,
|
||||
}
|
||||
|
||||
impl Default for ExportType {
|
||||
fn default() -> Self {
|
||||
Self::JianYingV2
|
||||
}
|
||||
}
|
||||
|
||||
/// 导出格式
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ExportFormat {
|
||||
/// JSON文件
|
||||
Json,
|
||||
/// CSV文件
|
||||
Csv,
|
||||
/// Excel文件
|
||||
Excel,
|
||||
/// 其他格式
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Default for ExportFormat {
|
||||
fn default() -> Self {
|
||||
Self::Json
|
||||
}
|
||||
}
|
||||
|
||||
/// 导出状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ExportStatus {
|
||||
/// 导出成功
|
||||
Success,
|
||||
/// 导出失败
|
||||
Failed,
|
||||
/// 导出进行中
|
||||
InProgress,
|
||||
/// 导出被取消
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Default for ExportStatus {
|
||||
fn default() -> Self {
|
||||
Self::Success
|
||||
}
|
||||
}
|
||||
|
||||
impl ExportRecord {
|
||||
/// 创建新的导出记录实例
|
||||
pub fn new(
|
||||
id: String,
|
||||
matching_result_id: String,
|
||||
project_id: String,
|
||||
template_id: String,
|
||||
export_type: ExportType,
|
||||
export_format: ExportFormat,
|
||||
file_path: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
matching_result_id,
|
||||
project_id,
|
||||
template_id,
|
||||
export_type,
|
||||
export_format,
|
||||
file_path,
|
||||
file_size: None,
|
||||
export_status: ExportStatus::InProgress,
|
||||
export_duration_ms: 0,
|
||||
error_message: None,
|
||||
metadata: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
is_active: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记导出成功
|
||||
pub fn mark_success(&mut self, file_size: Option<u64>, duration_ms: u64) {
|
||||
self.export_status = ExportStatus::Success;
|
||||
self.file_size = file_size;
|
||||
self.export_duration_ms = duration_ms;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 标记导出失败
|
||||
pub fn mark_failed(&mut self, error_message: String, duration_ms: u64) {
|
||||
self.export_status = ExportStatus::Failed;
|
||||
self.error_message = Some(error_message);
|
||||
self.export_duration_ms = duration_ms;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 设置元数据
|
||||
pub fn set_metadata(&mut self, metadata: String) {
|
||||
self.metadata = Some(metadata);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 获取导出类型的显示名称
|
||||
pub fn get_export_type_display(&self) -> &'static str {
|
||||
match self.export_type {
|
||||
ExportType::JianYingV1 => "剪映导出 V1",
|
||||
ExportType::JianYingV2 => "剪映导出 V2",
|
||||
ExportType::Json => "JSON导出",
|
||||
ExportType::Csv => "CSV导出",
|
||||
ExportType::Excel => "Excel导出",
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取导出状态的显示名称
|
||||
pub fn get_export_status_display(&self) -> &'static str {
|
||||
match self.export_status {
|
||||
ExportStatus::Success => "成功",
|
||||
ExportStatus::Failed => "失败",
|
||||
ExportStatus::InProgress => "进行中",
|
||||
ExportStatus::Cancelled => "已取消",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建导出记录请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateExportRecordRequest {
|
||||
pub matching_result_id: String,
|
||||
pub export_type: ExportType,
|
||||
pub export_format: ExportFormat,
|
||||
pub file_path: String,
|
||||
pub metadata: Option<String>,
|
||||
}
|
||||
|
||||
/// 导出记录查询选项
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExportRecordQueryOptions {
|
||||
pub project_id: Option<String>,
|
||||
pub matching_result_id: Option<String>,
|
||||
pub template_id: Option<String>,
|
||||
pub export_type: Option<ExportType>,
|
||||
pub export_status: Option<ExportStatus>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub search_keyword: Option<String>,
|
||||
pub sort_by: Option<String>, // "created_at", "export_duration_ms", "file_size"
|
||||
pub sort_order: Option<String>, // "asc", "desc"
|
||||
pub date_range: Option<DateRange>,
|
||||
}
|
||||
|
||||
/// 日期范围
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DateRange {
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 导出记录统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExportRecordStatistics {
|
||||
pub total_exports: u32,
|
||||
pub successful_exports: u32,
|
||||
pub failed_exports: u32,
|
||||
pub total_file_size: u64,
|
||||
pub average_export_duration_ms: u64,
|
||||
pub export_type_counts: std::collections::HashMap<String, u32>,
|
||||
}
|
||||
@@ -8,4 +8,5 @@ pub mod video_classification;
|
||||
pub mod template;
|
||||
pub mod project_template_binding;
|
||||
pub mod template_matching_result;
|
||||
pub mod export_record;
|
||||
pub mod video_generation;
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct TemplateMatchingResult {
|
||||
pub quality_score: Option<f64>, // 匹配质量评分
|
||||
pub status: MatchingResultStatus,
|
||||
pub metadata: Option<String>, // JSON格式的额外元数据
|
||||
pub export_count: u32, // 导出次数
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub is_active: bool,
|
||||
@@ -136,6 +137,7 @@ impl TemplateMatchingResult {
|
||||
quality_score: None,
|
||||
status: MatchingResultStatus::default(),
|
||||
metadata: None,
|
||||
export_count: 0,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
is_active: true,
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
use rusqlite::{Result, Row};
|
||||
use std::sync::Arc;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::infrastructure::database::Database;
|
||||
|
||||
use crate::data::models::export_record::{
|
||||
ExportRecord, ExportType, ExportFormat, ExportStatus,
|
||||
CreateExportRecordRequest, ExportRecordQueryOptions, ExportRecordStatistics,
|
||||
};
|
||||
|
||||
/// 导出记录数据库操作仓储
|
||||
/// 遵循 Tauri 开发规范的数据访问层设计原则
|
||||
pub struct ExportRecordRepository {
|
||||
database: Arc<Database>,
|
||||
}
|
||||
|
||||
impl ExportRecordRepository {
|
||||
/// 创建新的导出记录仓储实例
|
||||
pub fn new(database: Arc<Database>) -> Self {
|
||||
Self { database }
|
||||
}
|
||||
|
||||
/// 创建导出记录
|
||||
pub fn create(&self, request: CreateExportRecordRequest) -> Result<ExportRecord> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
// 生成唯一ID
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
// 获取项目ID和模板ID(从匹配结果中查询)
|
||||
let (project_id, template_id) = conn.query_row(
|
||||
"SELECT project_id, template_id FROM template_matching_results WHERE id = ?1",
|
||||
[&request.matching_result_id],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
)?;
|
||||
|
||||
let mut export_record = ExportRecord::new(
|
||||
id,
|
||||
request.matching_result_id,
|
||||
project_id,
|
||||
template_id,
|
||||
request.export_type,
|
||||
request.export_format,
|
||||
request.file_path,
|
||||
);
|
||||
|
||||
if let Some(metadata) = request.metadata {
|
||||
export_record.set_metadata(metadata);
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO export_records (
|
||||
id, matching_result_id, project_id, template_id, export_type, export_format,
|
||||
file_path, file_size, export_status, export_duration_ms, error_message,
|
||||
metadata, created_at, updated_at, is_active
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
rusqlite::params![
|
||||
&export_record.id,
|
||||
&export_record.matching_result_id,
|
||||
&export_record.project_id,
|
||||
&export_record.template_id,
|
||||
&serde_json::to_string(&export_record.export_type).unwrap(),
|
||||
&serde_json::to_string(&export_record.export_format).unwrap(),
|
||||
&export_record.file_path,
|
||||
&export_record.file_size,
|
||||
&serde_json::to_string(&export_record.export_status).unwrap(),
|
||||
&export_record.export_duration_ms,
|
||||
&export_record.error_message,
|
||||
&export_record.metadata,
|
||||
&export_record.created_at.to_rfc3339(),
|
||||
&export_record.updated_at.to_rfc3339(),
|
||||
&(export_record.is_active as i32),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(export_record)
|
||||
}
|
||||
|
||||
/// 根据ID获取导出记录
|
||||
pub fn get_by_id(&self, id: &str) -> Result<Option<ExportRecord>> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, matching_result_id, project_id, template_id, export_type, export_format,
|
||||
file_path, file_size, export_status, export_duration_ms, error_message,
|
||||
metadata, created_at, updated_at, is_active
|
||||
FROM export_records WHERE id = ?1"
|
||||
)?;
|
||||
|
||||
let result_iter = stmt.query_map([id], |row| {
|
||||
self.row_to_export_record(row)
|
||||
})?;
|
||||
|
||||
for result in result_iter {
|
||||
return Ok(Some(result?));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// 更新导出记录
|
||||
pub fn update(&self, export_record: &ExportRecord) -> Result<()> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
conn.execute(
|
||||
"UPDATE export_records SET
|
||||
file_size = ?2, export_status = ?3, export_duration_ms = ?4,
|
||||
error_message = ?5, metadata = ?6, updated_at = ?7
|
||||
WHERE id = ?1",
|
||||
rusqlite::params![
|
||||
&export_record.id,
|
||||
&export_record.file_size,
|
||||
&serde_json::to_string(&export_record.export_status).unwrap(),
|
||||
&export_record.export_duration_ms,
|
||||
&export_record.error_message,
|
||||
&export_record.metadata,
|
||||
&export_record.updated_at.to_rfc3339(),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 查询导出记录列表
|
||||
pub fn list(&self, options: ExportRecordQueryOptions) -> Result<Vec<ExportRecord>> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
let mut query = "SELECT id, matching_result_id, project_id, template_id, export_type, export_format,
|
||||
file_path, file_size, export_status, export_duration_ms, error_message,
|
||||
metadata, created_at, updated_at, is_active
|
||||
FROM export_records 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(matching_result_id) = &options.matching_result_id {
|
||||
query.push_str(" AND matching_result_id = ?");
|
||||
params.push(matching_result_id.clone());
|
||||
}
|
||||
|
||||
if let Some(template_id) = &options.template_id {
|
||||
query.push_str(" AND template_id = ?");
|
||||
params.push(template_id.clone());
|
||||
}
|
||||
|
||||
if let Some(export_type) = &options.export_type {
|
||||
query.push_str(" AND export_type = ?");
|
||||
params.push(serde_json::to_string(export_type).unwrap());
|
||||
}
|
||||
|
||||
if let Some(export_status) = &options.export_status {
|
||||
query.push_str(" AND export_status = ?");
|
||||
params.push(serde_json::to_string(export_status).unwrap());
|
||||
}
|
||||
|
||||
if let Some(keyword) = &options.search_keyword {
|
||||
query.push_str(" AND (file_path LIKE ? OR error_message LIKE ?)");
|
||||
let search_pattern = format!("%{}%", keyword);
|
||||
params.push(search_pattern.clone());
|
||||
params.push(search_pattern);
|
||||
}
|
||||
|
||||
// 添加日期范围过滤
|
||||
if let Some(date_range) = &options.date_range {
|
||||
query.push_str(" AND created_at >= ? AND created_at <= ?");
|
||||
params.push(date_range.start_date.to_rfc3339());
|
||||
params.push(date_range.end_date.to_rfc3339());
|
||||
}
|
||||
|
||||
// 添加排序
|
||||
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(&format!(" LIMIT {}", limit));
|
||||
if let Some(offset) = options.offset {
|
||||
query.push_str(&format!(" OFFSET {}", offset));
|
||||
}
|
||||
}
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
|
||||
|
||||
let export_record_iter = stmt.query_map(param_refs.as_slice(), |row| {
|
||||
self.row_to_export_record(row)
|
||||
})?;
|
||||
|
||||
let mut export_records = Vec::new();
|
||||
for export_record in export_record_iter {
|
||||
export_records.push(export_record?);
|
||||
}
|
||||
|
||||
Ok(export_records)
|
||||
}
|
||||
|
||||
/// 删除导出记录(软删除)
|
||||
pub fn delete(&self, id: &str) -> Result<()> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
conn.execute(
|
||||
"UPDATE export_records SET is_active = 0, updated_at = ?2 WHERE id = ?1",
|
||||
rusqlite::params![id, Utc::now().to_rfc3339()],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取导出记录统计信息
|
||||
pub fn get_statistics(&self, project_id: Option<&str>) -> Result<ExportRecordStatistics> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
let mut base_query = "FROM export_records WHERE is_active = 1".to_string();
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(pid) = project_id {
|
||||
base_query.push_str(" AND project_id = ?");
|
||||
params.push(pid.to_string());
|
||||
}
|
||||
|
||||
// 总导出次数
|
||||
let total_exports: u32 = conn.query_row(
|
||||
&format!("SELECT COUNT(*) {}", base_query),
|
||||
params.iter().map(|p| p as &dyn rusqlite::ToSql).collect::<Vec<_>>().as_slice(),
|
||||
|row| row.get(0)
|
||||
)?;
|
||||
|
||||
// 成功导出次数
|
||||
let mut success_params = params.clone();
|
||||
success_params.push("\"Success\"".to_string());
|
||||
let successful_exports: u32 = conn.query_row(
|
||||
&format!("SELECT COUNT(*) {} AND export_status = ?", base_query),
|
||||
success_params.iter().map(|p| p as &dyn rusqlite::ToSql).collect::<Vec<_>>().as_slice(),
|
||||
|row| row.get(0)
|
||||
)?;
|
||||
|
||||
// 失败导出次数
|
||||
let failed_exports = total_exports - successful_exports;
|
||||
|
||||
// 总文件大小
|
||||
let total_file_size: u64 = conn.query_row(
|
||||
&format!("SELECT COALESCE(SUM(file_size), 0) {} AND export_status = '\"Success\"'", base_query),
|
||||
params.iter().map(|p| p as &dyn rusqlite::ToSql).collect::<Vec<_>>().as_slice(),
|
||||
|row| row.get(0)
|
||||
).unwrap_or(0);
|
||||
|
||||
// 平均导出时长
|
||||
let average_export_duration_ms: u64 = conn.query_row(
|
||||
&format!("SELECT COALESCE(AVG(export_duration_ms), 0) {}", base_query),
|
||||
params.iter().map(|p| p as &dyn rusqlite::ToSql).collect::<Vec<_>>().as_slice(),
|
||||
|row| row.get(0)
|
||||
).unwrap_or(0);
|
||||
|
||||
// 导出类型统计
|
||||
let mut export_type_counts = std::collections::HashMap::new();
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT export_type, COUNT(*) {} GROUP BY export_type", base_query
|
||||
))?;
|
||||
|
||||
let type_iter = stmt.query_map(
|
||||
params.iter().map(|p| p as &dyn rusqlite::ToSql).collect::<Vec<_>>().as_slice(),
|
||||
|row| {
|
||||
let export_type: String = row.get(0)?;
|
||||
let count: u32 = row.get(1)?;
|
||||
Ok((export_type, count))
|
||||
}
|
||||
)?;
|
||||
|
||||
for result in type_iter {
|
||||
let (export_type, count) = result?;
|
||||
export_type_counts.insert(export_type, count);
|
||||
}
|
||||
|
||||
Ok(ExportRecordStatistics {
|
||||
total_exports,
|
||||
successful_exports,
|
||||
failed_exports,
|
||||
total_file_size,
|
||||
average_export_duration_ms,
|
||||
export_type_counts,
|
||||
})
|
||||
}
|
||||
|
||||
/// 将数据库行转换为导出记录实体
|
||||
fn row_to_export_record(&self, row: &Row) -> Result<ExportRecord> {
|
||||
let export_type_str: String = row.get("export_type")?;
|
||||
let export_type: ExportType = serde_json::from_str(&export_type_str)
|
||||
.unwrap_or(ExportType::default());
|
||||
|
||||
let export_format_str: String = row.get("export_format")?;
|
||||
let export_format: ExportFormat = serde_json::from_str(&export_format_str)
|
||||
.unwrap_or(ExportFormat::default());
|
||||
|
||||
let export_status_str: String = row.get("export_status")?;
|
||||
let export_status: ExportStatus = serde_json::from_str(&export_status_str)
|
||||
.unwrap_or(ExportStatus::default());
|
||||
|
||||
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);
|
||||
|
||||
// 正确处理 is_active 字段
|
||||
let is_active = match row.get::<_, rusqlite::types::Value>("is_active")? {
|
||||
rusqlite::types::Value::Integer(i) => i != 0,
|
||||
rusqlite::types::Value::Text(s) => s == "1" || s.to_lowercase() == "true",
|
||||
rusqlite::types::Value::Real(f) => f != 0.0,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
Ok(ExportRecord {
|
||||
id: row.get("id")?,
|
||||
matching_result_id: row.get("matching_result_id")?,
|
||||
project_id: row.get("project_id")?,
|
||||
template_id: row.get("template_id")?,
|
||||
export_type,
|
||||
export_format,
|
||||
file_path: row.get("file_path")?,
|
||||
file_size: row.get("file_size")?,
|
||||
export_status,
|
||||
export_duration_ms: row.get("export_duration_ms")?,
|
||||
error_message: row.get("error_message")?,
|
||||
metadata: row.get("metadata")?,
|
||||
created_at,
|
||||
updated_at,
|
||||
is_active,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@ pub mod ai_classification_repository;
|
||||
pub mod video_classification_repository;
|
||||
pub mod project_template_binding_repository;
|
||||
pub mod template_matching_result_repository;
|
||||
pub mod export_record_repository;
|
||||
pub mod video_generation_repository;
|
||||
|
||||
@@ -44,8 +44,8 @@ impl TemplateMatchingResultRepository {
|
||||
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)",
|
||||
status, metadata, export_count, 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)",
|
||||
rusqlite::params![
|
||||
&final_result.id,
|
||||
&final_result.project_id,
|
||||
@@ -63,6 +63,7 @@ impl TemplateMatchingResultRepository {
|
||||
&final_result.quality_score,
|
||||
&serde_json::to_string(&final_result.status).unwrap(),
|
||||
&final_result.metadata,
|
||||
&final_result.export_count,
|
||||
&final_result.created_at.to_rfc3339(),
|
||||
&final_result.updated_at.to_rfc3339(),
|
||||
&(final_result.is_active as i32),
|
||||
@@ -81,7 +82,7 @@ impl TemplateMatchingResultRepository {
|
||||
"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
|
||||
status, metadata, export_count, created_at, updated_at, is_active
|
||||
FROM template_matching_results WHERE id = ?1"
|
||||
)?;
|
||||
|
||||
@@ -163,7 +164,7 @@ impl TemplateMatchingResultRepository {
|
||||
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
|
||||
status, metadata, export_count, created_at, updated_at, is_active
|
||||
FROM template_matching_results WHERE is_active = 1".to_string();
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
|
||||
@@ -494,6 +495,7 @@ impl TemplateMatchingResultRepository {
|
||||
quality_score,
|
||||
status,
|
||||
metadata: row.get("metadata")?,
|
||||
export_count: row.get("export_count").unwrap_or(0),
|
||||
created_at,
|
||||
updated_at,
|
||||
is_active,
|
||||
@@ -559,4 +561,19 @@ impl TemplateMatchingResultRepository {
|
||||
updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
/// 增加导出次数
|
||||
pub fn increment_export_count(&self, result_id: &str) -> Result<()> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
conn.execute(
|
||||
"UPDATE template_matching_results
|
||||
SET export_count = export_count + 1, updated_at = ?2
|
||||
WHERE id = ?1",
|
||||
rusqlite::params![result_id, Utc::now().to_rfc3339()],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,6 +690,7 @@ impl Database {
|
||||
quality_score REAL,
|
||||
status TEXT NOT NULL DEFAULT 'Success',
|
||||
metadata TEXT,
|
||||
export_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
@@ -749,6 +750,31 @@ impl Database {
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 创建导出记录表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS export_records (
|
||||
id TEXT PRIMARY KEY,
|
||||
matching_result_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
template_id TEXT NOT NULL,
|
||||
export_type TEXT NOT NULL,
|
||||
export_format TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size INTEGER,
|
||||
export_status TEXT NOT NULL DEFAULT 'Success',
|
||||
export_duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
error_message TEXT,
|
||||
metadata TEXT,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
FOREIGN KEY (matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (template_id) REFERENCES templates (id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 创建素材使用记录表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS material_usage_records (
|
||||
@@ -894,6 +920,37 @@ impl Database {
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 创建导出记录表索引
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_export_records_matching_result_id ON export_records (matching_result_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_export_records_project_id ON export_records (project_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_export_records_template_id ON export_records (template_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_export_records_export_type ON export_records (export_type)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_export_records_export_status ON export_records (export_status)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_export_records_created_at ON export_records (created_at)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 添加新字段(如果不存在)- 数据库迁移
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE template_materials ADD COLUMN file_exists BOOLEAN DEFAULT FALSE",
|
||||
@@ -1410,6 +1467,17 @@ impl Database {
|
||||
println!("Added usage tracking columns to material_segments table");
|
||||
}
|
||||
|
||||
// 添加导出次数字段到模板匹配结果表
|
||||
let has_export_count_column = conn.prepare("SELECT export_count FROM template_matching_results LIMIT 1").is_ok();
|
||||
if !has_export_count_column {
|
||||
println!("Adding export_count column to template_matching_results table");
|
||||
conn.execute(
|
||||
"ALTER TABLE template_matching_results ADD COLUMN export_count INTEGER DEFAULT 0",
|
||||
[],
|
||||
)?;
|
||||
println!("Added export_count column to template_matching_results table");
|
||||
}
|
||||
|
||||
// 暂时禁用自动清理,避免启动时卡住
|
||||
// self.cleanup_invalid_projects()?;
|
||||
|
||||
|
||||
@@ -222,6 +222,17 @@ pub fn run() {
|
||||
commands::template_matching_result_commands::get_matching_result_status_options,
|
||||
commands::template_matching_result_commands::export_matching_result_to_jianying,
|
||||
commands::template_matching_result_commands::export_matching_result_to_jianying_v2,
|
||||
// 导出记录命令
|
||||
commands::export_record_commands::get_export_record,
|
||||
commands::export_record_commands::list_export_records,
|
||||
commands::export_record_commands::get_project_export_records,
|
||||
commands::export_record_commands::get_matching_result_export_records,
|
||||
commands::export_record_commands::delete_export_record,
|
||||
commands::export_record_commands::get_project_export_statistics,
|
||||
commands::export_record_commands::get_global_export_statistics,
|
||||
commands::export_record_commands::validate_export_file,
|
||||
commands::export_record_commands::cleanup_expired_export_records,
|
||||
commands::export_record_commands::re_export_record,
|
||||
// MaterialSegment聚合视图命令
|
||||
commands::material_segment_view_commands::get_project_segment_view,
|
||||
commands::material_segment_view_commands::get_project_segment_view_with_query,
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 导出记录相关的 Tauri 命令
|
||||
* 遵循 Tauri 开发规范的 API 设计原则
|
||||
*/
|
||||
|
||||
use tauri::{command, State};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::business::services::export_record_service::ExportRecordService;
|
||||
use crate::data::models::export_record::{
|
||||
ExportRecord, ExportRecordQueryOptions, ExportRecordStatistics,
|
||||
};
|
||||
use crate::data::repositories::export_record_repository::ExportRecordRepository;
|
||||
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||
use crate::infrastructure::database::Database;
|
||||
|
||||
/// 获取导出记录详情
|
||||
#[command]
|
||||
pub async fn get_export_record(
|
||||
record_id: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<Option<ExportRecord>, String> {
|
||||
println!("📋 获取导出记录详情: {}", record_id);
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.get_export_record(&record_id).await {
|
||||
Ok(record) => {
|
||||
println!("✅ 获取导出记录成功");
|
||||
Ok(record)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 获取导出记录失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询导出记录列表
|
||||
#[command]
|
||||
pub async fn list_export_records(
|
||||
options: ExportRecordQueryOptions,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<Vec<ExportRecord>, String> {
|
||||
println!("📋 查询导出记录列表");
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.list_export_records(options).await {
|
||||
Ok(records) => {
|
||||
println!("✅ 查询导出记录列表成功,共 {} 条记录", records.len());
|
||||
Ok(records)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 查询导出记录列表失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取项目的导出记录
|
||||
#[command]
|
||||
pub async fn get_project_export_records(
|
||||
project_id: String,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<Vec<ExportRecord>, String> {
|
||||
println!("📋 获取项目导出记录: {}", project_id);
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.get_project_exports(&project_id, limit, offset).await {
|
||||
Ok(records) => {
|
||||
println!("✅ 获取项目导出记录成功,共 {} 条记录", records.len());
|
||||
Ok(records)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 获取项目导出记录失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取匹配结果的导出记录
|
||||
#[command]
|
||||
pub async fn get_matching_result_export_records(
|
||||
matching_result_id: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<Vec<ExportRecord>, String> {
|
||||
println!("📋 获取匹配结果导出记录: {}", matching_result_id);
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.get_matching_result_exports(&matching_result_id).await {
|
||||
Ok(records) => {
|
||||
println!("✅ 获取匹配结果导出记录成功,共 {} 条记录", records.len());
|
||||
Ok(records)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 获取匹配结果导出记录失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除导出记录
|
||||
#[command]
|
||||
pub async fn delete_export_record(
|
||||
record_id: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<(), String> {
|
||||
println!("🗑️ 删除导出记录: {}", record_id);
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.delete_export_record(&record_id).await {
|
||||
Ok(_) => {
|
||||
println!("✅ 删除导出记录成功");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 删除导出记录失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取项目导出统计信息
|
||||
#[command]
|
||||
pub async fn get_project_export_statistics(
|
||||
project_id: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<ExportRecordStatistics, String> {
|
||||
println!("📊 获取项目导出统计信息: {}", project_id);
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.get_project_export_statistics(&project_id).await {
|
||||
Ok(statistics) => {
|
||||
println!("✅ 获取项目导出统计信息成功");
|
||||
Ok(statistics)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 获取项目导出统计信息失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取全局导出统计信息
|
||||
#[command]
|
||||
pub async fn get_global_export_statistics(
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<ExportRecordStatistics, String> {
|
||||
println!("📊 获取全局导出统计信息");
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.get_global_export_statistics().await {
|
||||
Ok(statistics) => {
|
||||
println!("✅ 获取全局导出统计信息成功");
|
||||
Ok(statistics)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 获取全局导出统计信息失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证导出文件是否存在
|
||||
#[command]
|
||||
pub async fn validate_export_file(
|
||||
record_id: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<bool, String> {
|
||||
println!("🔍 验证导出文件: {}", record_id);
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.validate_export_file(&record_id).await {
|
||||
Ok(exists) => {
|
||||
println!("✅ 验证导出文件完成: {}", if exists { "存在" } else { "不存在" });
|
||||
Ok(exists)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 验证导出文件失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理过期的导出记录
|
||||
#[command]
|
||||
pub async fn cleanup_expired_export_records(
|
||||
days: u32,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<u32, String> {
|
||||
println!("🧹 清理过期导出记录(超过 {} 天)", days);
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.cleanup_expired_records(days).await {
|
||||
Ok(count) => {
|
||||
println!("✅ 清理过期导出记录完成,共清理 {} 条记录", count);
|
||||
Ok(count)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 清理过期导出记录失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新导出
|
||||
#[command]
|
||||
pub async fn re_export_record(
|
||||
record_id: String,
|
||||
new_file_path: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<ExportRecord, String> {
|
||||
println!("🔄 重新导出: {} -> {}", record_id, new_file_path);
|
||||
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let matching_result_repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let service = ExportRecordService::new(export_record_repository, matching_result_repository);
|
||||
|
||||
match service.re_export(&record_id, new_file_path).await {
|
||||
Ok(new_record) => {
|
||||
println!("✅ 重新导出成功,新记录ID: {}", new_record.id);
|
||||
Ok(new_record)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 重新导出失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,5 @@ pub mod debug_commands;
|
||||
pub mod project_template_binding_commands;
|
||||
pub mod material_matching_commands;
|
||||
pub mod template_matching_result_commands;
|
||||
pub mod export_record_commands;
|
||||
pub mod video_generation_commands;
|
||||
|
||||
@@ -240,22 +240,70 @@ pub async fn export_matching_result_to_jianying(
|
||||
output_path: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<String, String> {
|
||||
use std::time::Instant;
|
||||
use crate::data::repositories::export_record_repository::ExportRecordRepository;
|
||||
use crate::business::services::export_record_service::ExportRecordService;
|
||||
use crate::data::models::export_record::{ExportType, ExportFormat};
|
||||
|
||||
println!("🎬 开始导出匹配结果到剪映格式 (V1)");
|
||||
println!("📋 导出参数:");
|
||||
println!(" - 匹配结果ID: {}", result_id);
|
||||
println!(" - 输出路径: {}", output_path);
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
// 创建服务实例
|
||||
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let material_repository = Arc::new(MaterialRepository::new(database.inner().clone())
|
||||
.map_err(|e| format!("创建MaterialRepository失败: {}", e))?);
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let export_record_service = ExportRecordService::new(export_record_repository, repository.clone());
|
||||
let service = TemplateMatchingResultService::new(repository);
|
||||
|
||||
// 创建导出记录
|
||||
let export_record = match export_record_service.create_export_record(
|
||||
result_id.clone(),
|
||||
ExportType::JianYingV1,
|
||||
ExportFormat::Json,
|
||||
output_path.clone(),
|
||||
None,
|
||||
).await {
|
||||
Ok(record) => record,
|
||||
Err(e) => {
|
||||
println!("❌ 创建导出记录失败: {}", e);
|
||||
return Err(format!("创建导出记录失败: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
// 执行导出
|
||||
match service.export_to_jianying(&result_id, &output_path, material_repository).await {
|
||||
Ok(file_path) => {
|
||||
let duration_ms = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
// 标记导出成功
|
||||
if let Err(e) = export_record_service.mark_export_success(
|
||||
export_record.id,
|
||||
file_path.clone(),
|
||||
duration_ms,
|
||||
).await {
|
||||
println!("⚠️ 更新导出记录失败: {}", e);
|
||||
}
|
||||
|
||||
println!("✅ 导出成功: {}", file_path);
|
||||
Ok(file_path)
|
||||
}
|
||||
Err(e) => {
|
||||
let duration_ms = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
// 标记导出失败
|
||||
if let Err(err) = export_record_service.mark_export_failed(
|
||||
export_record.id,
|
||||
e.to_string(),
|
||||
duration_ms,
|
||||
).await {
|
||||
println!("⚠️ 更新导出记录失败: {}", err);
|
||||
}
|
||||
|
||||
println!("❌ 导出失败: {}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
@@ -270,23 +318,71 @@ pub async fn export_matching_result_to_jianying_v2(
|
||||
output_path: String,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<String, String> {
|
||||
use std::time::Instant;
|
||||
use crate::data::repositories::export_record_repository::ExportRecordRepository;
|
||||
use crate::business::services::export_record_service::ExportRecordService;
|
||||
use crate::data::models::export_record::{ExportType, ExportFormat};
|
||||
|
||||
println!("🎬 开始导出匹配结果到剪映格式");
|
||||
println!("📋 导出参数:");
|
||||
println!(" - 匹配结果ID: {}", result_id);
|
||||
println!(" - 输出路径: {}", output_path);
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
// 创建服务实例
|
||||
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||
let material_repository = Arc::new(MaterialRepository::new(database.inner().clone())
|
||||
.map_err(|e| format!("创建MaterialRepository失败: {}", e))?);
|
||||
let template_service = Arc::new(crate::business::services::template_service::TemplateService::new(database.inner().clone()));
|
||||
let export_record_repository = Arc::new(ExportRecordRepository::new(database.inner().clone()));
|
||||
let export_record_service = ExportRecordService::new(export_record_repository, repository.clone());
|
||||
let service = TemplateMatchingResultService::new(repository);
|
||||
|
||||
// 创建导出记录
|
||||
let export_record = match export_record_service.create_export_record(
|
||||
result_id.clone(),
|
||||
ExportType::JianYingV2,
|
||||
ExportFormat::Json,
|
||||
output_path.clone(),
|
||||
None,
|
||||
).await {
|
||||
Ok(record) => record,
|
||||
Err(e) => {
|
||||
println!("❌ 创建导出记录失败: {}", e);
|
||||
return Err(format!("创建导出记录失败: {}", e));
|
||||
}
|
||||
};
|
||||
|
||||
// 执行导出
|
||||
match service.export_to_jianying_v2(&result_id, &output_path, material_repository, template_service).await {
|
||||
Ok(file_path) => {
|
||||
let duration_ms = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
// 标记导出成功
|
||||
if let Err(e) = export_record_service.mark_export_success(
|
||||
export_record.id,
|
||||
file_path.clone(),
|
||||
duration_ms,
|
||||
).await {
|
||||
println!("⚠️ 更新导出记录失败: {}", e);
|
||||
}
|
||||
|
||||
println!("✅ 导出成功: {}", file_path);
|
||||
Ok(file_path)
|
||||
}
|
||||
Err(e) => {
|
||||
let duration_ms = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
// 标记导出失败
|
||||
if let Err(err) = export_record_service.mark_export_failed(
|
||||
export_record.id,
|
||||
e.to_string(),
|
||||
duration_ms,
|
||||
).await {
|
||||
println!("⚠️ 更新导出记录失败: {}", err);
|
||||
}
|
||||
|
||||
eprintln!("❌ 导出失败: {}", e);
|
||||
Err(format!("导出失败: {}", e))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import ModelDetail from './pages/ModelDetail';
|
||||
import AiClassificationSettings from './pages/AiClassificationSettings';
|
||||
import TemplateManagement from './pages/TemplateManagement';
|
||||
import { MaterialModelBinding } from './pages/MaterialModelBinding';
|
||||
import ExportRecordsPage from './pages/ExportRecordsPage';
|
||||
import Navigation from './components/Navigation';
|
||||
import { NotificationSystem, useNotifications } from './components/NotificationSystem';
|
||||
import { useProjectStore } from './store/projectStore';
|
||||
@@ -78,6 +79,7 @@ function App() {
|
||||
<Route path="/ai-classification-settings" element={<AiClassificationSettings />} />
|
||||
<Route path="/templates" element={<TemplateManagement />} />
|
||||
<Route path="/material-model-binding" element={<MaterialModelBinding />} />
|
||||
<Route path="/export-records" element={<ExportRecordsPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
387
apps/desktop/src/components/ExportRecordManager.tsx
Normal file
387
apps/desktop/src/components/ExportRecordManager.tsx
Normal file
@@ -0,0 +1,387 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import {
|
||||
ExportRecord,
|
||||
ExportRecordQueryOptions,
|
||||
ExportRecordStatistics,
|
||||
ExportType,
|
||||
ExportStatus
|
||||
} from '../types/exportRecord';
|
||||
|
||||
interface ExportRecordManagerProps {
|
||||
projectId?: string;
|
||||
matchingResultId?: string;
|
||||
}
|
||||
|
||||
const ExportRecordManager: React.FC<ExportRecordManagerProps> = ({
|
||||
projectId,
|
||||
matchingResultId
|
||||
}) => {
|
||||
const [records, setRecords] = useState<ExportRecord[]>([]);
|
||||
const [statistics, setStatistics] = useState<ExportRecordStatistics | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// const [selectedRecords] = useState<string[]>([]);
|
||||
|
||||
// 过滤和分页状态
|
||||
const [filters, setFilters] = useState({
|
||||
export_type: '' as ExportType | '',
|
||||
export_status: '' as ExportStatus | '',
|
||||
search_keyword: '',
|
||||
});
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 加载导出记录列表
|
||||
const loadExportRecords = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const options: ExportRecordQueryOptions = {
|
||||
project_id: projectId,
|
||||
matching_result_id: matchingResultId,
|
||||
export_type: filters.export_type || undefined,
|
||||
export_status: filters.export_status || undefined,
|
||||
search_keyword: filters.search_keyword || undefined,
|
||||
limit: pagination.page_size,
|
||||
offset: (pagination.page - 1) * pagination.page_size,
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc',
|
||||
};
|
||||
|
||||
const result = await invoke<ExportRecord[]>('list_export_records', { options });
|
||||
setRecords(result);
|
||||
|
||||
// 更新总数(这里简化处理,实际应该从后端返回)
|
||||
setPagination(prev => ({ ...prev, total: result.length }));
|
||||
} catch (err) {
|
||||
setError(`加载导出记录失败: ${err}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载统计信息
|
||||
const loadStatistics = async () => {
|
||||
try {
|
||||
let stats: ExportRecordStatistics;
|
||||
if (projectId) {
|
||||
stats = await invoke<ExportRecordStatistics>('get_project_export_statistics', {
|
||||
projectId
|
||||
});
|
||||
} else {
|
||||
stats = await invoke<ExportRecordStatistics>('get_global_export_statistics');
|
||||
}
|
||||
setStatistics(stats);
|
||||
} catch (err) {
|
||||
console.error('加载统计信息失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除导出记录
|
||||
const handleDeleteRecord = async (recordId: string) => {
|
||||
if (!window.confirm('确定要删除这条导出记录吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('delete_export_record', { recordId });
|
||||
await loadExportRecords();
|
||||
await loadStatistics();
|
||||
} catch (err) {
|
||||
setError(`删除导出记录失败: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 验证文件是否存在
|
||||
const handleValidateFile = async (recordId: string) => {
|
||||
try {
|
||||
const exists = await invoke<boolean>('validate_export_file', { recordId });
|
||||
alert(exists ? '文件存在' : '文件不存在');
|
||||
} catch (err) {
|
||||
setError(`验证文件失败: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 重新导出
|
||||
const handleReExport = async (recordId: string) => {
|
||||
// 这里应该打开文件选择对话框,简化处理
|
||||
const newFilePath = prompt('请输入新的文件路径:');
|
||||
if (!newFilePath) return;
|
||||
|
||||
try {
|
||||
await invoke('re_export_record', { recordId, newFilePath });
|
||||
await loadExportRecords();
|
||||
} catch (err) {
|
||||
setError(`重新导出失败: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 清理过期记录
|
||||
const handleCleanupExpired = async () => {
|
||||
const days = prompt('请输入要清理多少天前的记录:', '30');
|
||||
if (!days) return;
|
||||
|
||||
try {
|
||||
const count = await invoke<number>('cleanup_expired_export_records', {
|
||||
days: parseInt(days)
|
||||
});
|
||||
alert(`已清理 ${count} 条过期记录`);
|
||||
await loadExportRecords();
|
||||
await loadStatistics();
|
||||
} catch (err) {
|
||||
setError(`清理过期记录失败: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes?: number): string => {
|
||||
if (!bytes) return '-';
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
// 格式化持续时间
|
||||
const formatDuration = (ms: number): string => {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
};
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: ExportStatus): string => {
|
||||
switch (status) {
|
||||
case ExportStatus.Success: return 'text-green-600';
|
||||
case ExportStatus.Failed: return 'text-red-600';
|
||||
case ExportStatus.InProgress: return 'text-blue-600';
|
||||
case ExportStatus.Cancelled: return 'text-gray-600';
|
||||
default: return 'text-gray-600';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取类型图标
|
||||
const getTypeIcon = (type: ExportType): string => {
|
||||
switch (type) {
|
||||
case ExportType.JianYingV1: return '🎬';
|
||||
case ExportType.JianYingV2: return '🎭';
|
||||
case ExportType.Json: return '📄';
|
||||
case ExportType.Csv: return '📊';
|
||||
case ExportType.Excel: return '📈';
|
||||
default: return '📁';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadExportRecords();
|
||||
loadStatistics();
|
||||
}, [projectId, matchingResultId, filters, pagination.page]);
|
||||
|
||||
return (
|
||||
<div className="export-record-manager p-6">
|
||||
<div className="header mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-4">
|
||||
导出记录管理
|
||||
{projectId && <span className="text-sm text-gray-600 ml-2">(项目范围)</span>}
|
||||
{matchingResultId && <span className="text-sm text-gray-600 ml-2">(匹配结果范围)</span>}
|
||||
</h2>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{statistics && (
|
||||
<div className="stats-grid grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="stat-card bg-blue-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">{statistics.total_exports}</div>
|
||||
<div className="text-sm text-gray-600">总导出次数</div>
|
||||
</div>
|
||||
<div className="stat-card bg-green-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">{statistics.successful_exports}</div>
|
||||
<div className="text-sm text-gray-600">成功导出</div>
|
||||
</div>
|
||||
<div className="stat-card bg-red-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-600">{statistics.failed_exports}</div>
|
||||
<div className="text-sm text-gray-600">失败导出</div>
|
||||
</div>
|
||||
<div className="stat-card bg-purple-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{formatFileSize(statistics.total_file_size)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">总文件大小</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 过滤器 */}
|
||||
<div className="filters flex flex-wrap gap-4 mb-4">
|
||||
<select
|
||||
value={filters.export_type}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, export_type: e.target.value as ExportType }))}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="">所有类型</option>
|
||||
<option value={ExportType.JianYingV1}>剪映 V1</option>
|
||||
<option value={ExportType.JianYingV2}>剪映 V2</option>
|
||||
<option value={ExportType.Json}>JSON</option>
|
||||
<option value={ExportType.Csv}>CSV</option>
|
||||
<option value={ExportType.Excel}>Excel</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filters.export_status}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, export_status: e.target.value as ExportStatus }))}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="">所有状态</option>
|
||||
<option value={ExportStatus.Success}>成功</option>
|
||||
<option value={ExportStatus.Failed}>失败</option>
|
||||
<option value={ExportStatus.InProgress}>进行中</option>
|
||||
<option value={ExportStatus.Cancelled}>已取消</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索文件路径或错误信息..."
|
||||
value={filters.search_keyword}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, search_keyword: e.target.value }))}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md flex-1 min-w-64"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleCleanupExpired}
|
||||
className="px-4 py-2 bg-orange-500 text-white rounded-md hover:bg-orange-600"
|
||||
>
|
||||
清理过期记录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<div className="error-message bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<div className="loading text-center py-8">
|
||||
<div className="text-gray-600">加载中...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 导出记录列表 */}
|
||||
{!loading && (
|
||||
<div className="records-table">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white border border-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
类型
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
文件路径
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
状态
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
文件大小
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
耗时
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
创建时间
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{records.map((record) => (
|
||||
<tr key={record.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{getTypeIcon(record.export_type)}</span>
|
||||
<span className="text-sm text-gray-900">
|
||||
{record.export_type.replace(/([A-Z])/g, ' $1').trim()}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="text-sm text-gray-900 truncate max-w-xs" title={record.file_path}>
|
||||
{record.file_path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className={`text-sm font-medium ${getStatusColor(record.export_status)}`}>
|
||||
{record.export_status === ExportStatus.Success && '成功'}
|
||||
{record.export_status === ExportStatus.Failed && '失败'}
|
||||
{record.export_status === ExportStatus.InProgress && '进行中'}
|
||||
{record.export_status === ExportStatus.Cancelled && '已取消'}
|
||||
</span>
|
||||
{record.error_message && (
|
||||
<div className="text-xs text-red-600 mt-1" title={record.error_message}>
|
||||
{record.error_message.substring(0, 50)}...
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{formatFileSize(record.file_size)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{formatDuration(record.export_duration_ms)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(record.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handleValidateFile(record.id)}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
title="验证文件"
|
||||
>
|
||||
🔍
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleReExport(record.id)}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
title="重新导出"
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteRecord(record.id)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
title="删除记录"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{records.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
暂无导出记录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportRecordManager;
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
UserGroupIcon,
|
||||
CpuChipIcon,
|
||||
DocumentDuplicateIcon,
|
||||
LinkIcon
|
||||
LinkIcon,
|
||||
DocumentArrowDownIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const Navigation: React.FC = () => {
|
||||
@@ -41,6 +42,12 @@ const Navigation: React.FC = () => {
|
||||
href: '/ai-classification-settings',
|
||||
icon: CpuChipIcon,
|
||||
description: '管理AI视频分类规则'
|
||||
},
|
||||
{
|
||||
name: '导出记录',
|
||||
href: '/export-records',
|
||||
icon: DocumentArrowDownIcon,
|
||||
description: '查看和管理导出记录'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
16
apps/desktop/src/pages/ExportRecordsPage.tsx
Normal file
16
apps/desktop/src/pages/ExportRecordsPage.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import ExportRecordManager from '../components/ExportRecordManager';
|
||||
|
||||
const ExportRecordsPage: React.FC = () => {
|
||||
return (
|
||||
<div className="export-records-page min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="bg-white rounded-lg shadow-sm">
|
||||
<ExportRecordManager />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportRecordsPage;
|
||||
294
apps/desktop/src/types/exportRecord.ts
Normal file
294
apps/desktop/src/types/exportRecord.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 导出记录相关的类型定义
|
||||
* 遵循前端开发规范的类型设计原则
|
||||
*/
|
||||
|
||||
// 导出类型枚举
|
||||
export enum ExportType {
|
||||
JianYingV1 = 'JianYingV1',
|
||||
JianYingV2 = 'JianYingV2',
|
||||
Json = 'Json',
|
||||
Csv = 'Csv',
|
||||
Excel = 'Excel'
|
||||
}
|
||||
|
||||
// 导出格式枚举
|
||||
export enum ExportFormat {
|
||||
Json = 'Json',
|
||||
Csv = 'Csv',
|
||||
Excel = 'Excel',
|
||||
Other = 'Other'
|
||||
}
|
||||
|
||||
// 导出状态枚举
|
||||
export enum ExportStatus {
|
||||
Success = 'Success',
|
||||
Failed = 'Failed',
|
||||
InProgress = 'InProgress',
|
||||
Cancelled = 'Cancelled'
|
||||
}
|
||||
|
||||
// 导出记录
|
||||
export interface ExportRecord {
|
||||
id: string;
|
||||
matching_result_id: string;
|
||||
project_id: string;
|
||||
template_id: string;
|
||||
export_type: ExportType;
|
||||
export_format: ExportFormat;
|
||||
file_path: string;
|
||||
file_size?: number;
|
||||
export_status: ExportStatus;
|
||||
export_duration_ms: number;
|
||||
error_message?: string;
|
||||
metadata?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
// 创建导出记录请求
|
||||
export interface CreateExportRecordRequest {
|
||||
matching_result_id: string;
|
||||
export_type: ExportType;
|
||||
export_format: ExportFormat;
|
||||
file_path: string;
|
||||
metadata?: string;
|
||||
}
|
||||
|
||||
// 日期范围
|
||||
export interface DateRange {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
}
|
||||
|
||||
// 导出记录查询选项
|
||||
export interface ExportRecordQueryOptions {
|
||||
project_id?: string;
|
||||
matching_result_id?: string;
|
||||
template_id?: string;
|
||||
export_type?: ExportType;
|
||||
export_status?: ExportStatus;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search_keyword?: string;
|
||||
sort_by?: string; // "created_at", "export_duration_ms", "file_size"
|
||||
sort_order?: string; // "asc", "desc"
|
||||
date_range?: DateRange;
|
||||
}
|
||||
|
||||
// 导出记录统计信息
|
||||
export interface ExportRecordStatistics {
|
||||
total_exports: number;
|
||||
successful_exports: number;
|
||||
failed_exports: number;
|
||||
total_file_size: number;
|
||||
average_export_duration_ms: number;
|
||||
export_type_counts: Record<string, number>;
|
||||
}
|
||||
|
||||
// 导出记录过滤选项
|
||||
export interface ExportRecordFilterOptions {
|
||||
project_id?: string;
|
||||
template_id?: string;
|
||||
export_type?: ExportType;
|
||||
export_status?: ExportStatus;
|
||||
date_range?: DateRange;
|
||||
}
|
||||
|
||||
// 导出记录排序选项
|
||||
export interface ExportRecordSortOptions {
|
||||
field: 'created_at' | 'updated_at' | 'export_duration_ms' | 'file_size' | 'export_status';
|
||||
order: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 导出记录分页选项
|
||||
export interface ExportRecordPaginationOptions {
|
||||
page: number;
|
||||
page_size: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
// 导出记录搜索选项
|
||||
export interface ExportRecordSearchOptions {
|
||||
keyword?: string;
|
||||
filter?: ExportRecordFilterOptions;
|
||||
sort?: ExportRecordSortOptions;
|
||||
pagination?: ExportRecordPaginationOptions;
|
||||
}
|
||||
|
||||
// 导出记录列表响应
|
||||
export interface ExportRecordListResponse {
|
||||
records: ExportRecord[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
// 导出记录详情响应
|
||||
export interface ExportRecordDetailResponse {
|
||||
record: ExportRecord;
|
||||
matching_result?: {
|
||||
id: string;
|
||||
result_name: string;
|
||||
project_id: string;
|
||||
template_id: string;
|
||||
};
|
||||
project?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
template?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 导出记录操作选项
|
||||
export interface ExportRecordActionOptions {
|
||||
// 重新导出
|
||||
re_export?: {
|
||||
new_file_path: string;
|
||||
};
|
||||
// 验证文件
|
||||
validate_file?: boolean;
|
||||
// 删除记录
|
||||
delete?: boolean;
|
||||
}
|
||||
|
||||
// 导出记录批量操作请求
|
||||
export interface ExportRecordBatchActionRequest {
|
||||
record_ids: string[];
|
||||
action: 'delete' | 'validate' | 'cleanup';
|
||||
options?: {
|
||||
force?: boolean;
|
||||
backup?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// 导出记录批量操作响应
|
||||
export interface ExportRecordBatchActionResponse {
|
||||
success_count: number;
|
||||
failed_count: number;
|
||||
errors: Array<{
|
||||
record_id: string;
|
||||
error: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 导出记录清理选项
|
||||
export interface ExportRecordCleanupOptions {
|
||||
days_old: number;
|
||||
include_failed?: boolean;
|
||||
include_successful?: boolean;
|
||||
dry_run?: boolean;
|
||||
}
|
||||
|
||||
// 导出记录清理响应
|
||||
export interface ExportRecordCleanupResponse {
|
||||
cleaned_count: number;
|
||||
total_size_freed: number;
|
||||
records_cleaned: Array<{
|
||||
id: string;
|
||||
file_path: string;
|
||||
file_size?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 导出记录表格列定义
|
||||
export interface ExportRecordTableColumn {
|
||||
key: keyof ExportRecord | 'actions';
|
||||
title: string;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
width?: number;
|
||||
render?: (record: ExportRecord) => React.ReactNode;
|
||||
}
|
||||
|
||||
// 导出记录表格配置
|
||||
export interface ExportRecordTableConfig {
|
||||
columns: ExportRecordTableColumn[];
|
||||
pagination: ExportRecordPaginationOptions;
|
||||
sort: ExportRecordSortOptions;
|
||||
filter: ExportRecordFilterOptions;
|
||||
selection?: {
|
||||
enabled: boolean;
|
||||
selected_ids: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// 导出记录图表数据
|
||||
export interface ExportRecordChartData {
|
||||
// 按日期统计
|
||||
daily_stats: Array<{
|
||||
date: string;
|
||||
total_exports: number;
|
||||
successful_exports: number;
|
||||
failed_exports: number;
|
||||
}>;
|
||||
// 按类型统计
|
||||
type_stats: Array<{
|
||||
export_type: ExportType;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}>;
|
||||
// 按状态统计
|
||||
status_stats: Array<{
|
||||
export_status: ExportStatus;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 导出记录仪表板数据
|
||||
export interface ExportRecordDashboardData {
|
||||
statistics: ExportRecordStatistics;
|
||||
recent_records: ExportRecord[];
|
||||
chart_data: ExportRecordChartData;
|
||||
trends: {
|
||||
export_count_trend: number; // 相比上周的变化百分比
|
||||
success_rate_trend: number;
|
||||
average_duration_trend: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 导出记录表单数据
|
||||
export interface ExportRecordFormData {
|
||||
matching_result_id: string;
|
||||
export_type: ExportType;
|
||||
export_format: ExportFormat;
|
||||
file_path: string;
|
||||
metadata?: string;
|
||||
}
|
||||
|
||||
// 导出记录验证规则
|
||||
export interface ExportRecordValidationRules {
|
||||
matching_result_id: {
|
||||
required: boolean;
|
||||
message: string;
|
||||
};
|
||||
export_type: {
|
||||
required: boolean;
|
||||
message: string;
|
||||
};
|
||||
export_format: {
|
||||
required: boolean;
|
||||
message: string;
|
||||
};
|
||||
file_path: {
|
||||
required: boolean;
|
||||
pattern?: RegExp;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 导出记录工具函数类型
|
||||
export type ExportRecordUtils = {
|
||||
formatFileSize: (bytes?: number) => string;
|
||||
formatDuration: (ms: number) => string;
|
||||
getStatusColor: (status: ExportStatus) => string;
|
||||
getTypeIcon: (type: ExportType) => string;
|
||||
validateFilePath: (path: string) => boolean;
|
||||
generateFileName: (record: ExportRecord) => string;
|
||||
};
|
||||
@@ -30,6 +30,7 @@ export interface TemplateMatchingResult {
|
||||
quality_score?: number;
|
||||
status: MatchingResultStatus;
|
||||
metadata?: string;
|
||||
export_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_active: boolean;
|
||||
|
||||
Reference in New Issue
Block a user