fix: 修复模板导入的bug

This commit is contained in:
imeepos 2025-07-14 23:14:45 +08:00
parent 49c5b1a033
commit 595d2f75fd
7 changed files with 401 additions and 72 deletions

View File

@ -44,6 +44,7 @@ pub struct MaterialsRaw {
#[derive(Debug, Deserialize)]
pub struct VideoMaterialRaw {
pub id: String,
pub local_material_id: Option<String>,
pub material_name: Option<String>,
pub path: String,
pub duration: Option<u64>,
@ -57,6 +58,7 @@ pub struct VideoMaterialRaw {
#[derive(Debug, Deserialize)]
pub struct AudioMaterialRaw {
pub id: String,
pub local_material_id: Option<String>,
pub name: String,
pub path: String,
pub duration: Option<u64>,
@ -67,6 +69,7 @@ pub struct AudioMaterialRaw {
#[derive(Debug, Deserialize)]
pub struct ImageMaterialRaw {
pub id: String,
pub local_material_id: Option<String>,
pub material_name: Option<String>,
pub path: Option<String>,
pub width: Option<u32>,
@ -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(),

View File

@ -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,

View File

@ -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<i64>>("width")?.map(|w| w as u32),
height: row.get::<_, Option<i64>>("height")?.map(|h| h as u32),
upload_status,
file_exists: row.get::<_, Option<bool>>("file_exists")?.unwrap_or(false),
upload_success: row.get::<_, Option<bool>>("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(())
}
}

View File

@ -44,6 +44,8 @@ pub struct TemplateMaterial {
pub width: Option<u32>,
pub height: Option<u32>,
pub upload_status: UploadStatus,
pub file_exists: bool, // 本地文件是否存在
pub upload_success: bool, // 是否上传成功
pub metadata: Option<String>, // JSON格式的额外元数据
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@ -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 {

View File

@ -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)",

View File

@ -134,7 +134,7 @@ export const ImportTemplateModal: React.FC<ImportTemplateModalProps> = ({
</label>
<div
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
className={`cursour-pointer border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
dragOver
? 'border-blue-400 bg-blue-50'
: errors.file_path
@ -161,11 +161,8 @@ export const ImportTemplateModal: React.FC<ImportTemplateModalProps> = ({
) : (
<>
<Upload className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<div className="text-sm text-gray-600 mb-2">
draft_content.json
</div>
<div className="text-xs text-gray-500 mb-3">
<div className="text-sm text-gray-600 mb-2 cursour-pointer ">
draft_content.json
</div>
</>
)}

View File

@ -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<TemplateDetailModalProps> = ({
}
};
// 检查文件是否存在(基于数据库字段和路径)
const getFileExistenceInfo = (material: any) => {
const hasRemoteFile = material.remote_url && material.remote_url.trim() !== '';
if (hasRemoteFile) {
return {
icon: <Cloud className="w-4 h-4 text-blue-600" />,
text: '云端文件',
color: 'text-blue-600'
};
} else if (material.file_exists) {
return {
icon: <CheckCircle className="w-4 h-4 text-green-600" />,
text: '本地文件存在',
color: 'text-green-600'
};
} else {
return {
icon: <XCircle className="w-4 h-4 text-red-600" />,
text: '文件不存在',
color: 'text-red-600'
};
}
};
// 获取上传状态(基于数据库字段)
const getUploadStatusInfo = (material: any) => {
if (material.upload_success) {
return {
icon: <CheckCircle className="w-4 h-4 text-green-600" />,
text: '上传成功',
color: 'text-green-600'
};
} else {
// 根据 upload_status 显示具体状态
switch (material.upload_status) {
case 'Uploading':
return {
icon: <Upload className="w-4 h-4 text-blue-600" />,
text: '上传中',
color: 'text-blue-600'
};
case 'Failed':
case 'Pending':
return {
icon: <AlertCircle className="w-4 h-4 text-yellow-600" />,
text: '待上传',
color: 'text-yellow-600'
};
case 'Skipped':
return {
icon: <AlertCircle className="w-4 h-4 text-gray-600" />,
text: '已跳过',
color: 'text-gray-600'
};
default:
return {
icon: <AlertCircle className="w-4 h-4 text-gray-600" />,
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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl mx-4 max-h-[90vh] overflow-hidden">
@ -100,11 +204,10 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
className={`flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors ${activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Icon className="w-4 h-4 mr-2" />
{tab.label}
@ -165,6 +268,25 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
</div>
</div>
{/* 模板信息 */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-sm font-medium text-gray-700 mb-3"></h3>
<div className="grid grid-cols-1 gap-3 text-sm">
<div className="flex items-center">
<FileText className="w-4 h-4 text-gray-500 mr-2" />
<span className="text-gray-600">ID</span>
<span className="ml-1 font-mono text-blue-600 text-xs">{template.id}</span>
</div>
{template.source_file_path && (
<div className="flex items-center">
<FileText className="w-4 h-4 text-gray-500 mr-2" />
<span className="text-gray-600"></span>
<span className="ml-1 text-gray-900 break-all">{template.source_file_path}</span>
</div>
)}
</div>
</div>
{/* 创建信息 */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-sm font-medium text-gray-700 mb-3"></h3>
@ -179,13 +301,6 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
<span className="text-gray-600"></span>
<span className="ml-1 text-gray-900">{formatDate(template.updated_at)}</span>
</div>
{template.source_file_path && (
<div className="flex items-center md:col-span-2">
<FileText className="w-4 h-4 text-gray-500 mr-2" />
<span className="text-gray-600"></span>
<span className="ml-1 text-gray-900 break-all">{template.source_file_path}</span>
</div>
)}
</div>
</div>
</div>
@ -198,42 +313,97 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
({template.materials.length})
</h3>
</div>
<div className="space-y-3">
{template.materials.map((material) => (
<div key={material.id} className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
{getMaterialIcon(material.material_type)}
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">
{material.name}
</div>
<div className="text-xs text-gray-500 mt-1">
: {material.material_type}
</div>
{material.original_path && (
<div className="text-xs text-gray-500 break-all">
: {material.original_path}
{template.materials.map((material) => {
const uploadStatus = getUploadStatusInfo(material);
const fileExistence = getFileExistenceInfo(material);
return (
<div key={material.id} className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
{getMaterialIcon(material.material_type)}
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">
{getMaterialName(material)}
</div>
<div className="text-xs text-gray-500 mt-1 space-y-1">
<div>ID: <span className="font-mono text-blue-600">{material.id}</span></div>
<div>ID: <span className="font-mono text-purple-600">{material.original_id}</span></div>
<div>: {material.material_type}</div>
{/* 文件存在状态 */}
<div className="flex items-center space-x-1">
{fileExistence.icon}
<span className={fileExistence.color}>{fileExistence.text}</span>
</div>
{/* 上传状态 */}
<div className="flex items-center space-x-1">
{uploadStatus.icon}
<span className={uploadStatus.color}>{uploadStatus.text}</span>
</div>
</div>
{/* 文字素材显示文本内容 */}
{material.material_type === 'Text' && material.metadata && (() => {
const textData = parseTextContent(material.metadata);
return textData ? (
<div className="text-xs text-gray-700 bg-gray-100 p-2 rounded mt-1">
<div className="font-medium text-gray-600 mb-1">:</div>
<div className="break-words mb-2">
{textData.content?.text || '无文本内容'}
</div>
{(textData.font_family || textData.font_size || textData.color) && (
<div className="text-xs text-gray-500 space-y-1">
{textData.font_family && (
<div>: {textData.font_family}</div>
)}
{textData.font_size && (
<div>: {textData.font_size}px</div>
)}
{textData.color && (
<div>: {textData.color}</div>
)}
</div>
)}
</div>
) : (
<div className="text-xs text-gray-500 bg-gray-100 p-2 rounded mt-1">
</div>
);
})()}
{/* 非文字素材显示文件路径 */}
{material.material_type !== 'Text' && material.original_path && (
<div className="text-xs text-gray-500 break-all mt-1">
: {material.original_path}
</div>
)}
{material.remote_url && (
<div className="text-xs text-gray-500 break-all mt-1">
URL: {material.remote_url}
</div>
)}
</div>
</div>
<div className="text-right text-xs text-gray-500">
{material.duration && (
<div>: {formatDuration(material.duration)}</div>
)}
{material.file_size && (
<div>: {formatFileSize(material.file_size)}</div>
)}
{material.width && material.height && (
<div>: {material.width}×{material.height}</div>
)}
</div>
</div>
<div className="text-right text-xs text-gray-500">
{material.duration && (
<div>: {formatDuration(material.duration)}</div>
)}
{material.file_size && (
<div>: {formatFileSize(material.file_size)}</div>
)}
{material.width && material.height && (
<div>: {material.width}×{material.height}</div>
)}
</div>
</div>
</div>
))}
);
})}
</div>
</div>
)}
@ -245,37 +415,53 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
({template.tracks.length})
</h3>
</div>
<div className="space-y-4">
{template.tracks.map((track) => (
<div key={track.id} className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
{getTrackIcon(track.track_type)}
<span className="text-sm font-medium text-gray-900">
{track.name}
</span>
<span className="text-xs text-gray-500">
({track.track_type})
</span>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900">
{track.name}
</span>
<div className="text-xs text-gray-500">
<span>ID: <span className="font-mono text-green-600">{track.id}</span></span>
<span className="ml-3">: {track.track_type}</span>
<span className="ml-3">: {track.track_index}</span>
</div>
</div>
</div>
<span className="text-xs text-gray-500">
{track.segments.length}
</span>
</div>
{track.segments.length > 0 && (
<div className="space-y-2">
{track.segments.map((segment) => (
<div key={segment.id} className="bg-white p-3 rounded border">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-900">{segment.name}</span>
<span className="text-xs text-gray-500">
{formatDuration(segment.start_time)} - {formatDuration(segment.end_time)}
</span>
</div>
<div className="text-xs text-gray-500 mt-1">
: {formatDuration(segment.duration)}
<div className="text-xs text-gray-500 space-y-1">
<div>
<span>ID: <span className="font-mono text-orange-600">{segment.id}</span></span>
<span className="ml-3">: {segment.segment_index}</span>
</div>
<div>: {formatDuration(segment.duration)}</div>
{segment.template_material_id && (
<div>
使ID: <span className="font-mono text-blue-600">{segment.template_material_id}</span>
</div>
)}
{!segment.template_material_id && (
<div className="text-gray-400"></div>
)}
</div>
</div>
))}