feat: 完善模板导入功能

新增功能:
- 添加详细的模板导入日志系统
- 实现全局进度存储机制
- 完善模板状态管理

 修复问题:
- 修复进度监控无限轮询问题
- 修复模板列表状态显示不正确问题
- 修复所有unwrap()导致的panic错误
- 修复外键约束失败问题

 改进:
- 优化素材上传逻辑,只上传视频/音频/图片
- 上传失败时自动跳过而不是中断导入
- 缺失文件时继续导入而不是失败
- 改进错误处理机制
This commit is contained in:
imeepos
2025-07-14 21:34:07 +08:00
parent b3321114ac
commit 939efd70d4
39 changed files with 9152 additions and 6 deletions

View 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>
);
};