From 595d2f75fd43dd58d29a20522a4f53fca1ea61c7 Mon Sep 17 00:00:00 2001 From: imeepos Date: Mon, 14 Jul 2025 23:14:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/business/services/draft_parser.rs | 21 +- .../services/template_import_service.rs | 34 +- .../src/business/services/template_service.rs | 77 ++++- .../src-tauri/src/data/models/template.rs | 23 +- .../src-tauri/src/infrastructure/database.rs | 13 + .../template/ImportTemplateModal.tsx | 9 +- .../template/TemplateDetailModal.tsx | 296 ++++++++++++++---- 7 files changed, 401 insertions(+), 72 deletions(-) diff --git a/apps/desktop/src-tauri/src/business/services/draft_parser.rs b/apps/desktop/src-tauri/src/business/services/draft_parser.rs index cafd9ff..40cfa8b 100644 --- a/apps/desktop/src-tauri/src/business/services/draft_parser.rs +++ b/apps/desktop/src-tauri/src/business/services/draft_parser.rs @@ -44,6 +44,7 @@ pub struct MaterialsRaw { #[derive(Debug, Deserialize)] pub struct VideoMaterialRaw { pub id: String, + pub local_material_id: Option, pub material_name: Option, pub path: String, pub duration: Option, @@ -57,6 +58,7 @@ pub struct VideoMaterialRaw { #[derive(Debug, Deserialize)] pub struct AudioMaterialRaw { pub id: String, + pub local_material_id: Option, pub name: String, pub path: String, pub duration: Option, @@ -67,6 +69,7 @@ pub struct AudioMaterialRaw { #[derive(Debug, Deserialize)] pub struct ImageMaterialRaw { pub id: String, + pub local_material_id: Option, pub material_name: Option, pub path: Option, pub width: Option, @@ -186,6 +189,8 @@ impl DraftContentParser { }); let mut template = Template::new(name, canvas_config, draft.duration, draft.fps); + // 使用草稿文件中的ID作为模板ID,确保同一个草稿文件导入时不会重复 + template.id = draft.id.clone(); template.source_file_path = source_file_path; // 解析素材 @@ -327,12 +332,19 @@ impl DraftContentParser { let mut material = TemplateMaterial::new( String::new(), // template_id 稍后设置 - video.id.clone(), + video.id.clone(), // 主键使用 id name, TemplateMaterialType::Video, video.path.clone(), ); + // 设置 original_id 为 local_material_id + if let Some(local_material_id) = &video.local_material_id { + if !local_material_id.is_empty() { + material.original_id = local_material_id.clone(); + } + } + material.duration = video.duration; material.width = video.width; material.height = video.height; @@ -355,9 +367,14 @@ impl DraftContentParser { missing_files.push(audio.path.clone()); } + // 使用 local_material_id 作为主键,如果没有则使用 id + let material_id = audio.local_material_id.clone() + .filter(|id| !id.is_empty()) + .unwrap_or_else(|| audio.id.clone()); + let mut material = TemplateMaterial::new( String::new(), // template_id 稍后设置 - audio.id.clone(), + material_id, // 使用 local_material_id 作为主键 audio.name.clone(), TemplateMaterialType::Audio, audio.path.clone(), diff --git a/apps/desktop/src-tauri/src/business/services/template_import_service.rs b/apps/desktop/src-tauri/src/business/services/template_import_service.rs index f33ae79..148f144 100644 --- a/apps/desktop/src-tauri/src/business/services/template_import_service.rs +++ b/apps/desktop/src-tauri/src/business/services/template_import_service.rs @@ -106,6 +106,31 @@ impl TemplateImportService { }; let mut template = parse_result.template; + // 添加详细的解析结果日志 + info!( + template_id = %template.id, + template_name = %template.name, + materials_count = %template.materials.len(), + tracks_count = %template.tracks.len(), + missing_files_count = %parse_result.missing_files.len(), + warnings_count = %parse_result.warnings.len(), + "草稿文件解析完成" + ); + + // 如果有警告,记录详细信息 + if !parse_result.warnings.is_empty() { + for warning in &parse_result.warnings { + warn!("解析警告: {}", warning); + } + } + + // 如果有缺失文件,记录详细信息 + if !parse_result.missing_files.is_empty() { + for missing_file in &parse_result.missing_files { + warn!("缺失文件: {}", missing_file); + } + } + // 初始化进度跟踪 let progress = ImportProgress { template_id: template.id.clone(), @@ -317,8 +342,12 @@ impl TemplateImportService { let mut uploaded_count = 0; for (index, material) in template.materials.iter_mut().enumerate() { + // 更新文件存在状态 + material.update_file_exists(); + // 跳过没有文件路径的素材(如文本、画布等) - if material.original_path.is_empty() || !material.file_exists() { + if material.original_path.is_empty() || !material.file_exists { + material.upload_success = false; material.update_upload_status(UploadStatus::Skipped, None); continue; } @@ -371,6 +400,7 @@ impl TemplateImportService { UploadStatus::Completed, upload_result.remote_url.clone() ); + material.update_upload_success(true); material.file_size = Some(upload_result.file_size); uploaded_count += 1; info!( @@ -381,6 +411,7 @@ impl TemplateImportService { ); } else { // 上传失败时标记为跳过,不影响整体导入 + material.update_upload_success(false); material.update_upload_status(UploadStatus::Skipped, None); warn!( material_name = %material.name, @@ -391,6 +422,7 @@ impl TemplateImportService { } Err(e) => { // 上传异常时标记为跳过,不影响整体导入 + material.update_upload_success(false); material.update_upload_status(UploadStatus::Skipped, None); warn!( material_name = %material.name, diff --git a/apps/desktop/src-tauri/src/business/services/template_service.rs b/apps/desktop/src-tauri/src/business/services/template_service.rs index 5e1c627..4a6b537 100644 --- a/apps/desktop/src-tauri/src/business/services/template_service.rs +++ b/apps/desktop/src-tauri/src/business/services/template_service.rs @@ -105,8 +105,8 @@ impl TemplateService { "INSERT OR REPLACE INTO template_materials ( id, template_id, original_id, name, material_type, original_path, remote_url, file_size, duration, width, height, upload_status, - metadata, created_at, updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", + file_exists, upload_success, metadata, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)", params![ material.id, material.template_id, @@ -120,6 +120,8 @@ impl TemplateService { material.width.map(|w| w as i64), material.height.map(|h| h as i64), format!("{:?}", material.upload_status), + material.file_exists, + material.upload_success, material.metadata, material.created_at.to_rfc3339(), material.updated_at.to_rfc3339() @@ -199,7 +201,7 @@ impl TemplateService { let mut stmt = conn.prepare( "SELECT id, template_id, original_id, name, material_type, original_path, remote_url, file_size, duration, width, height, upload_status, - metadata, created_at, updated_at + file_exists, upload_success, metadata, created_at, updated_at FROM template_materials WHERE template_id = ?1" )?; @@ -280,7 +282,10 @@ impl TemplateService { let mut templates = Vec::new(); for template_result in template_rows { - templates.push(template_result?); + let mut template = template_result?; + // 填充素材和轨道数量 + self.fill_template_counts(&conn, &mut template)?; + templates.push(template); } return Ok(TemplateListResponse { templates, total }); @@ -311,7 +316,10 @@ impl TemplateService { let mut templates = Vec::new(); for template_result in template_rows { - templates.push(template_result?); + let mut template = template_result?; + // 填充素材和轨道数量 + self.fill_template_counts(&conn, &mut template)?; + templates.push(template); } return Ok(TemplateListResponse { templates, total }); @@ -344,7 +352,10 @@ impl TemplateService { let mut templates = Vec::new(); for template_result in template_rows { - templates.push(template_result?); + let mut template = template_result?; + // 填充素材和轨道数量 + self.fill_template_counts(&conn, &mut template)?; + templates.push(template); } Ok(TemplateListResponse { templates, total }) @@ -468,6 +479,8 @@ impl TemplateService { width: row.get::<_, Option>("width")?.map(|w| w as u32), height: row.get::<_, Option>("height")?.map(|h| h as u32), upload_status, + file_exists: row.get::<_, Option>("file_exists")?.unwrap_or(false), + upload_success: row.get::<_, Option>("upload_success")?.unwrap_or(false), metadata: row.get("metadata")?, created_at: { let created_at_str: String = row.get("created_at")?; @@ -546,4 +559,56 @@ impl TemplateService { .with_timezone(&chrono::Utc), }) } + + /// 填充模板的真实素材和轨道数据 + fn fill_template_counts(&self, conn: &rusqlite::Connection, template: &mut Template) -> Result<()> { + // 查询真实的素材数据 + let mut stmt = conn.prepare( + "SELECT id, template_id, original_id, name, material_type, original_path, + remote_url, file_size, duration, width, height, upload_status, + file_exists, upload_success, metadata, created_at, updated_at + FROM template_materials WHERE template_id = ?1" + )?; + + let material_rows = stmt.query_map(params![template.id], |row| { + self.row_to_template_material(row) + })?; + + for material_result in material_rows { + template.materials.push(material_result?); + } + + // 查询真实的轨道数据 + let mut stmt = conn.prepare( + "SELECT id, template_id, name, track_type, track_index, created_at, updated_at + FROM tracks WHERE template_id = ?1 ORDER BY track_index" + )?; + + let track_rows = stmt.query_map(params![template.id], |row| { + self.row_to_track_basic(row) + })?; + + for track_result in track_rows { + let mut track = track_result?; + + // 查询每个轨道的真实片段数据 + let mut segment_stmt = conn.prepare( + "SELECT id, track_id, template_material_id, name, start_time, end_time, + duration, segment_index, properties, created_at, updated_at + FROM track_segments WHERE track_id = ?1 ORDER BY segment_index" + )?; + + let segment_rows = segment_stmt.query_map(params![track.id], |row| { + self.row_to_track_segment(row) + })?; + + for segment_result in segment_rows { + track.segments.push(segment_result?); + } + + template.tracks.push(track); + } + + Ok(()) + } } diff --git a/apps/desktop/src-tauri/src/data/models/template.rs b/apps/desktop/src-tauri/src/data/models/template.rs index a7d755c..d6cdff7 100644 --- a/apps/desktop/src-tauri/src/data/models/template.rs +++ b/apps/desktop/src-tauri/src/data/models/template.rs @@ -44,6 +44,8 @@ pub struct TemplateMaterial { pub width: Option, pub height: Option, pub upload_status: UploadStatus, + pub file_exists: bool, // 本地文件是否存在 + pub upload_success: bool, // 是否上传成功 pub metadata: Option, // JSON格式的额外元数据 pub created_at: DateTime, pub updated_at: DateTime, @@ -243,7 +245,7 @@ impl TemplateMaterial { ) -> Self { let now = Utc::now(); Self { - id: uuid::Uuid::new_v4().to_string(), + id: original_id.clone(), // 使用原始ID作为主键,保持与剪映草稿的一致性 template_id, original_id, name, @@ -255,6 +257,8 @@ impl TemplateMaterial { width: None, height: None, upload_status: UploadStatus::Pending, + file_exists: false, + upload_success: false, metadata: None, created_at: now, updated_at: now, @@ -270,10 +274,25 @@ impl TemplateMaterial { self.updated_at = Utc::now(); } - /// 检查文件是否存在 + /// 检查本地文件是否存在 pub fn file_exists(&self) -> bool { + if self.original_path.is_empty() { + return false; + } std::path::Path::new(&self.original_path).exists() } + + /// 更新文件存在状态 + pub fn update_file_exists(&mut self) { + self.file_exists = self.file_exists(); + self.updated_at = Utc::now(); + } + + /// 更新上传成功状态 + pub fn update_upload_success(&mut self, success: bool) { + self.upload_success = success; + self.updated_at = Utc::now(); + } } impl Track { diff --git a/apps/desktop/src-tauri/src/infrastructure/database.rs b/apps/desktop/src-tauri/src/infrastructure/database.rs index 2b5b6d6..f0c6952 100644 --- a/apps/desktop/src-tauri/src/infrastructure/database.rs +++ b/apps/desktop/src-tauri/src/infrastructure/database.rs @@ -296,6 +296,8 @@ impl Database { width INTEGER, height INTEGER, upload_status TEXT NOT NULL DEFAULT 'Pending', + file_exists BOOLEAN DEFAULT FALSE, + upload_success BOOLEAN DEFAULT FALSE, metadata TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -393,6 +395,17 @@ impl Database { [], )?; + // 添加新字段(如果不存在)- 数据库迁移 + let _ = conn.execute( + "ALTER TABLE template_materials ADD COLUMN file_exists BOOLEAN DEFAULT FALSE", + [], + ); + + let _ = conn.execute( + "ALTER TABLE template_materials ADD COLUMN upload_success BOOLEAN DEFAULT FALSE", + [], + ); + // 创建模板素材表索引 conn.execute( "CREATE INDEX IF NOT EXISTS idx_template_materials_template_id ON template_materials (template_id)", diff --git a/apps/desktop/src/components/template/ImportTemplateModal.tsx b/apps/desktop/src/components/template/ImportTemplateModal.tsx index 777683e..058c06b 100644 --- a/apps/desktop/src/components/template/ImportTemplateModal.tsx +++ b/apps/desktop/src/components/template/ImportTemplateModal.tsx @@ -134,7 +134,7 @@ export const ImportTemplateModal: React.FC = ({
= ({ ) : ( <> -
- 拖拽 draft_content.json 文件到此处 -
-
- 或者点击下方按钮选择文件 +
+ 选择 draft_content.json 文件上传
)} diff --git a/apps/desktop/src/components/template/TemplateDetailModal.tsx b/apps/desktop/src/components/template/TemplateDetailModal.tsx index 8681793..1ae8029 100644 --- a/apps/desktop/src/components/template/TemplateDetailModal.tsx +++ b/apps/desktop/src/components/template/TemplateDetailModal.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { X, Calendar, Clock, Monitor, Layers, FileText, Image, Video, Music, Type, Sparkles } from 'lucide-react'; -import { Template, TemplateMaterialType, TrackType } from '../../types/template'; +import { X, Calendar, Clock, Monitor, Layers, FileText, Image, Video, Music, Type, Sparkles, CheckCircle, XCircle, AlertCircle, Upload, Cloud } from 'lucide-react'; +import { Template, TemplateMaterial, TemplateMaterialType, TrackType } from '../../types/template'; interface TemplateDetailModalProps { template: Template; @@ -66,12 +66,116 @@ export const TemplateDetailModal: React.FC = ({ } }; + + + // 检查文件是否存在(基于数据库字段和路径) + const getFileExistenceInfo = (material: any) => { + const hasRemoteFile = material.remote_url && material.remote_url.trim() !== ''; + + if (hasRemoteFile) { + return { + icon: , + text: '云端文件', + color: 'text-blue-600' + }; + } else if (material.file_exists) { + return { + icon: , + text: '本地文件存在', + color: 'text-green-600' + }; + } else { + return { + icon: , + text: '文件不存在', + color: 'text-red-600' + }; + } + }; + + // 获取上传状态(基于数据库字段) + const getUploadStatusInfo = (material: any) => { + if (material.upload_success) { + return { + icon: , + text: '上传成功', + color: 'text-green-600' + }; + } else { + // 根据 upload_status 显示具体状态 + switch (material.upload_status) { + case 'Uploading': + return { + icon: , + text: '上传中', + color: 'text-blue-600' + }; + case 'Failed': + + case 'Pending': + return { + icon: , + text: '待上传', + color: 'text-yellow-600' + }; + case 'Skipped': + return { + icon: , + text: '已跳过', + color: 'text-gray-600' + }; + default: + return { + icon: , + text: '未上传', + color: 'text-gray-600' + }; + } + } + }; + + // 解析文字素材的文本内容和样式 + const parseTextContent = (metadata: string | null) => { + if (!metadata) return null; + try { + const parsed = JSON.parse(metadata); + const obj = { + content: tryParse(parsed.content || null), + font_family: parsed.font_family || null, + font_size: parsed.font_size || null, + color: parsed.color || null + }; + console.log({ obj }) + return obj; + } catch (e) { + return null; + } + }; + + const tryParse = (str: any) => { + try { + if (typeof str === 'string') { + return JSON.parse(str) + } + return str; + } catch (e) { + return str; + } + } + const tabs = [ { id: 'overview', label: '概览', icon: Monitor }, { id: 'materials', label: '素材', icon: FileText }, { id: 'tracks', label: '轨道', icon: Layers }, ]; + const getMaterialName = (material: TemplateMaterial) => { + if (material.material_type === TemplateMaterialType.Text) { + return parseTextContent(material.metadata!)?.content?.text + } + return material.name + } + return (
@@ -100,11 +204,10 @@ export const TemplateDetailModal: React.FC = ({
+ {/* 模板信息 */} +
+

模板信息

+
+
+ + 模板ID: + {template.id} +
+ {template.source_file_path && ( +
+ + 源文件: + {template.source_file_path} +
+ )} +
+
+ {/* 创建信息 */}

创建信息

@@ -179,13 +301,6 @@ export const TemplateDetailModal: React.FC = ({ 更新时间: {formatDate(template.updated_at)}
- {template.source_file_path && ( -
- - 源文件: - {template.source_file_path} -
- )}
@@ -198,42 +313,97 @@ export const TemplateDetailModal: React.FC = ({ 素材列表 ({template.materials.length}) - +
- {template.materials.map((material) => ( -
-
-
- {getMaterialIcon(material.material_type)} -
-
- {material.name} -
-
- 类型: {material.material_type} -
- {material.original_path && ( -
- 路径: {material.original_path} + {template.materials.map((material) => { + const uploadStatus = getUploadStatusInfo(material); + const fileExistence = getFileExistenceInfo(material); + + return ( +
+
+
+ {getMaterialIcon(material.material_type)} +
+
+ {getMaterialName(material)}
+
+
ID: {material.id}
+
原始ID: {material.original_id}
+
类型: {material.material_type}
+ + {/* 文件存在状态 */} +
+ {fileExistence.icon} + {fileExistence.text} +
+ + {/* 上传状态 */} +
+ {uploadStatus.icon} + {uploadStatus.text} +
+
+ {/* 文字素材显示文本内容 */} + {material.material_type === 'Text' && material.metadata && (() => { + const textData = parseTextContent(material.metadata); + return textData ? ( +
+
文本内容:
+
+ {textData.content?.text || '无文本内容'} +
+ {(textData.font_family || textData.font_size || textData.color) && ( +
+ {textData.font_family && ( +
字体: {textData.font_family}
+ )} + {textData.font_size && ( +
大小: {textData.font_size}px
+ )} + {textData.color && ( +
颜色: {textData.color}
+ )} +
+ )} +
+ ) : ( +
+ 无法解析文本内容 +
+ ); + })()} + + {/* 非文字素材显示文件路径 */} + {material.material_type !== 'Text' && material.original_path && ( +
+ 路径: {material.original_path} +
+ )} + {material.remote_url && ( +
+ 云端URL: {material.remote_url} +
+ )} +
+
+ +
+ {material.duration && ( +
时长: {formatDuration(material.duration)}
+ )} + {material.file_size && ( +
大小: {formatFileSize(material.file_size)}
+ )} + {material.width && material.height && ( +
尺寸: {material.width}×{material.height}
)}
- -
- {material.duration && ( -
时长: {formatDuration(material.duration)}
- )} - {material.file_size && ( -
大小: {formatFileSize(material.file_size)}
- )} - {material.width && material.height && ( -
尺寸: {material.width}×{material.height}
- )} -
-
- ))} + ); + })}
)} @@ -245,37 +415,53 @@ export const TemplateDetailModal: React.FC = ({ 轨道列表 ({template.tracks.length})
- +
{template.tracks.map((track) => (
{getTrackIcon(track.track_type)} - - {track.name} - - - ({track.track_type}) - +
+ + {track.name} + +
+ ID: {track.id} + 类型: {track.track_type} + 索引: {track.track_index} +
+
{track.segments.length} 个片段
- + {track.segments.length > 0 && (
{track.segments.map((segment) => (
-
+
{segment.name} {formatDuration(segment.start_time)} - {formatDuration(segment.end_time)}
-
- 时长: {formatDuration(segment.duration)} +
+
+ 片段ID: {segment.id} + 索引: {segment.segment_index} +
+
时长: {formatDuration(segment.duration)}
+ {segment.template_material_id && ( +
+ 使用素材ID: {segment.template_material_id} +
+ )} + {!segment.template_material_id && ( +
未关联素材
+ )}
))}