fix: 修复模板导入的bug
This commit is contained in:
parent
49c5b1a033
commit
595d2f75fd
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Reference in New Issue