feat: 完善模板导入功能
新增功能: - 添加详细的模板导入日志系统 - 实现全局进度存储机制 - 完善模板状态管理 修复问题: - 修复进度监控无限轮询问题 - 修复模板列表状态显示不正确问题 - 修复所有unwrap()导致的panic错误 - 修复外键约束失败问题 改进: - 优化素材上传逻辑,只上传视频/音频/图片 - 上传失败时自动跳过而不是中断导入 - 缺失文件时继续导入而不是失败 - 改进错误处理机制
This commit is contained in:
303
apps/desktop/src/components/template/TemplateDetailModal.tsx
Normal file
303
apps/desktop/src/components/template/TemplateDetailModal.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
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';
|
||||
|
||||
interface TemplateDetailModalProps {
|
||||
template: Template;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
|
||||
template,
|
||||
onClose,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'materials' | 'tracks'>('overview');
|
||||
|
||||
// 格式化时长
|
||||
const formatDuration = (microseconds: number) => {
|
||||
const seconds = Math.floor(microseconds / 1000000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes?: number) => {
|
||||
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 formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
// 获取素材类型图标
|
||||
const getMaterialIcon = (type: TemplateMaterialType) => {
|
||||
switch (type) {
|
||||
case TemplateMaterialType.Video:
|
||||
return <Video className="w-4 h-4 text-blue-600" />;
|
||||
case TemplateMaterialType.Audio:
|
||||
return <Music className="w-4 h-4 text-green-600" />;
|
||||
case TemplateMaterialType.Image:
|
||||
return <Image className="w-4 h-4 text-purple-600" />;
|
||||
case TemplateMaterialType.Text:
|
||||
return <Type className="w-4 h-4 text-orange-600" />;
|
||||
case TemplateMaterialType.Effect:
|
||||
return <Sparkles className="w-4 h-4 text-pink-600" />;
|
||||
default:
|
||||
return <FileText className="w-4 h-4 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取轨道类型图标
|
||||
const getTrackIcon = (type: TrackType) => {
|
||||
switch (type) {
|
||||
case TrackType.Video:
|
||||
return <Video className="w-4 h-4 text-blue-600" />;
|
||||
case TrackType.Audio:
|
||||
return <Music className="w-4 h-4 text-green-600" />;
|
||||
case TrackType.Text:
|
||||
return <Type className="w-4 h-4 text-orange-600" />;
|
||||
default:
|
||||
return <Layers className="w-4 h-4 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: '概览', icon: Monitor },
|
||||
{ id: 'materials', label: '素材', icon: FileText },
|
||||
{ id: 'tracks', label: '轨道', icon: Layers },
|
||||
];
|
||||
|
||||
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">
|
||||
{/* 模态框头部 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">{template.name}</h2>
|
||||
{template.description && (
|
||||
<p className="text-sm text-gray-600 mt-1">{template.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded-full hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 标签页导航 */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="flex space-x-8 px-6">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4 mr-2" />
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 标签页内容 */}
|
||||
<div className="p-6 overflow-y-auto max-h-[60vh]">
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6">
|
||||
{/* 基本信息 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="flex items-center mb-2">
|
||||
<Monitor className="w-4 h-4 text-gray-600 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-700">分辨率</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{template.canvas_config.width}×{template.canvas_config.height}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{template.canvas_config.ratio}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="flex items-center mb-2">
|
||||
<Clock className="w-4 h-4 text-gray-600 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-700">时长</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{formatDuration(template.duration)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{template.fps} FPS</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="flex items-center mb-2">
|
||||
<FileText className="w-4 h-4 text-gray-600 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-700">素材</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{template.materials.length}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">个素材</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="flex items-center mb-2">
|
||||
<Layers className="w-4 h-4 text-gray-600 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-700">轨道</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{template.tracks.length}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">个轨道</div>
|
||||
</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 md:grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="w-4 h-4 text-gray-500 mr-2" />
|
||||
<span className="text-gray-600">创建时间:</span>
|
||||
<span className="ml-1 text-gray-900">{formatDate(template.created_at)}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Calendar className="w-4 h-4 text-gray-500 mr-2" />
|
||||
<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>
|
||||
)}
|
||||
|
||||
{activeTab === 'materials' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
素材列表 ({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}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{activeTab === 'tracks' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
轨道列表 ({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>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 模态框底部 */}
|
||||
<div className="flex items-center justify-end p-6 border-t border-gray-200">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user