710 lines
30 KiB
TypeScript
710 lines
30 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
||
import { Upload, FolderOpen, Trash2, Eye, Download, Search, Filter, Grid, List } from 'lucide-react'
|
||
import { invoke } from '@tauri-apps/api/core'
|
||
import { TemplateService } from '../services/tauri'
|
||
import { useTemplateProgress } from '../hooks/useProgressCommand'
|
||
|
||
interface TemplateInfo {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
thumbnail_path: string
|
||
draft_content_path: string
|
||
resources_path: string
|
||
created_at: string
|
||
updated_at: string
|
||
canvas_config: any
|
||
duration: number
|
||
material_count: number
|
||
track_count: number
|
||
tags: string[]
|
||
}
|
||
|
||
// 轨道和片段的数据结构
|
||
interface TrackSegment {
|
||
id: string
|
||
type: 'video' | 'audio' | 'image' | 'text' | 'effect'
|
||
name: string
|
||
start_time: number
|
||
end_time: number
|
||
duration: number
|
||
resource_path?: string
|
||
properties?: any
|
||
effects?: any[]
|
||
}
|
||
|
||
interface Track {
|
||
id: string
|
||
name: string
|
||
type: 'video' | 'audio' | 'subtitle'
|
||
index: number
|
||
segments: TrackSegment[]
|
||
properties?: any
|
||
}
|
||
|
||
interface TemplateDetail {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
canvas_config: any
|
||
tracks: Track[]
|
||
duration: number
|
||
fps: number
|
||
sample_rate?: number
|
||
}
|
||
|
||
// Import the progress interface from the hook
|
||
import type { ProgressState } from '../hooks/useProgressCommand'
|
||
|
||
interface ImportResult {
|
||
status: boolean
|
||
msg: string
|
||
imported_count: number
|
||
failed_count: number
|
||
imported_templates: TemplateInfo[]
|
||
failed_templates: Array<{
|
||
name: string
|
||
error: string
|
||
}>
|
||
}
|
||
|
||
const TemplateManagePage: React.FC = () => {
|
||
const [templates, setTemplates] = useState<TemplateInfo[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [searchTerm, setSearchTerm] = useState('')
|
||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateInfo | null>(null)
|
||
const [templateDetail, setTemplateDetail] = useState<TemplateDetail | null>(null)
|
||
const [loadingDetail, setLoadingDetail] = useState(false)
|
||
const [showImportModal, setShowImportModal] = useState(false)
|
||
|
||
// Use the progress hook for template operations
|
||
const {
|
||
isExecuting: importing,
|
||
progress: importProgress,
|
||
result: importResult,
|
||
logs: importLogs,
|
||
batchImport,
|
||
reset: resetImport
|
||
} = useTemplateProgress({
|
||
onSuccess: async (result) => {
|
||
if (result.status && result.imported_count > 0) {
|
||
await loadTemplates() // Reload templates after successful import
|
||
}
|
||
}
|
||
})
|
||
|
||
// Load templates on component mount
|
||
useEffect(() => {
|
||
loadTemplates()
|
||
}, [])
|
||
|
||
const loadTemplates = async () => {
|
||
try {
|
||
setLoading(true)
|
||
const result = await TemplateService.getTemplates()
|
||
if (result.status) {
|
||
setTemplates(result.templates || [])
|
||
} else {
|
||
console.error('Failed to load templates:', result.msg)
|
||
setTemplates([])
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading templates:', error)
|
||
setTemplates([])
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 加载模板详情(包含轨道和片段信息)
|
||
const loadTemplateDetail = async (template: TemplateInfo) => {
|
||
try {
|
||
setLoadingDetail(true)
|
||
setSelectedTemplate(template)
|
||
|
||
// 调用后端API获取模板详情
|
||
const detail = await TemplateService.getTemplateDetail(template.id)
|
||
setTemplateDetail(detail)
|
||
} catch (error) {
|
||
console.error('Failed to load template detail:', error)
|
||
// 如果加载详情失败,至少显示基本信息
|
||
setTemplateDetail(null)
|
||
} finally {
|
||
setLoadingDetail(false)
|
||
}
|
||
}
|
||
|
||
const handleBatchImport = async () => {
|
||
try {
|
||
// Select folder using Tauri dialog
|
||
const folderResult = await invoke<string>('select_folder')
|
||
if (!folderResult) return
|
||
|
||
// Show import modal and start import
|
||
setShowImportModal(true)
|
||
await batchImport(folderResult)
|
||
} catch (error) {
|
||
console.error('Import failed:', error)
|
||
}
|
||
}
|
||
|
||
const handleDeleteTemplate = async (templateId: string) => {
|
||
if (!confirm('确定要删除这个模板吗?此操作不可撤销。')) return
|
||
|
||
try {
|
||
const result = await TemplateService.deleteTemplate(templateId)
|
||
if (result.status) {
|
||
setTemplates(templates.filter(t => t.id !== templateId))
|
||
if (selectedTemplate?.id === templateId) {
|
||
setSelectedTemplate(null)
|
||
}
|
||
} else {
|
||
alert('删除失败: ' + result.msg)
|
||
}
|
||
} catch (error) {
|
||
console.error('Delete failed:', error)
|
||
alert('删除失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
|
||
}
|
||
}
|
||
|
||
const filteredTemplates = templates.filter(template =>
|
||
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
template.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||
)
|
||
|
||
const formatDuration = (duration: number) => {
|
||
const seconds = Math.floor(duration / 1000000)
|
||
const minutes = Math.floor(seconds / 60)
|
||
const remainingSeconds = seconds % 60
|
||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
||
}
|
||
|
||
// Helper function to format time (for segments, in seconds)
|
||
const formatTime = (seconds: number): string => {
|
||
const minutes = Math.floor(seconds / 60)
|
||
const secs = (seconds % 60).toFixed(2)
|
||
return `${minutes}:${secs.padStart(5, '0')}`
|
||
}
|
||
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleDateString('zh-CN')
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 p-6">
|
||
<div className="max-w-7xl mx-auto">
|
||
{/* Header */}
|
||
<div className="mb-8">
|
||
<h1 className="text-3xl font-bold text-gray-900 mb-2">模板管理</h1>
|
||
<p className="text-gray-600">管理和导入视频编辑模板</p>
|
||
</div>
|
||
|
||
{/* Actions Bar */}
|
||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-4">
|
||
<button
|
||
onClick={handleBatchImport}
|
||
disabled={importing}
|
||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{importing ? (
|
||
<>
|
||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
|
||
导入中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Upload size={16} />
|
||
批量导入模板
|
||
</>
|
||
)}
|
||
</button>
|
||
|
||
<div className="relative">
|
||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={16} />
|
||
<input
|
||
type="text"
|
||
placeholder="搜索模板..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
<button
|
||
onClick={() => setViewMode('grid')}
|
||
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
|
||
>
|
||
<Grid size={16} />
|
||
</button>
|
||
<button
|
||
onClick={() => setViewMode('list')}
|
||
className={`p-2 rounded ${viewMode === 'list' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
|
||
>
|
||
<List size={16} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Import Result */}
|
||
{importResult && (
|
||
<div className={`mb-6 p-4 rounded-lg ${importResult.status ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h3 className={`font-medium ${importResult.status ? 'text-green-800' : 'text-red-800'}`}>
|
||
导入结果
|
||
</h3>
|
||
<p className={`text-sm ${importResult.status ? 'text-green-600' : 'text-red-600'}`}>
|
||
{importResult.msg}
|
||
</p>
|
||
{importResult.imported_count > 0 && (
|
||
<p className="text-sm text-green-600 mt-1">
|
||
成功导入 {importResult.imported_count} 个模板
|
||
</p>
|
||
)}
|
||
{importResult.failed_count > 0 && (
|
||
<details className="mt-2">
|
||
<summary className="text-sm text-red-600 cursor-pointer">
|
||
{importResult.failed_count} 个模板导入失败 (点击查看详情)
|
||
</summary>
|
||
<div className="mt-2 space-y-1">
|
||
{importResult.failed_templates.map((failed: any, index: number) => (
|
||
<div key={index} className="text-xs text-red-500">
|
||
{failed.name}: {failed.error}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</details>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => setImportResult(null)}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Templates Grid/List */}
|
||
{loading ? (
|
||
<div className="flex items-center justify-center py-12">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-600 border-t-transparent"></div>
|
||
<span className="ml-2 text-gray-600">加载中...</span>
|
||
</div>
|
||
) : filteredTemplates.length === 0 ? (
|
||
<div className="text-center py-12">
|
||
<FolderOpen className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无模板</h3>
|
||
<p className="text-gray-600 mb-4">
|
||
{searchTerm ? '没有找到匹配的模板' : '点击上方按钮开始导入模板'}
|
||
</p>
|
||
{!searchTerm && (
|
||
<button
|
||
onClick={handleBatchImport}
|
||
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||
>
|
||
<Upload size={16} />
|
||
导入模板
|
||
</button>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className={viewMode === 'grid' ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6' : 'space-y-4'}>
|
||
{filteredTemplates.map((template) => (
|
||
<div
|
||
key={template.id}
|
||
className={`bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow ${
|
||
viewMode === 'list' ? 'flex items-center p-4' : 'overflow-hidden'
|
||
}`}
|
||
>
|
||
{viewMode === 'grid' ? (
|
||
<>
|
||
{/* Thumbnail */}
|
||
<div className="h-32 bg-gray-100 flex items-center justify-center">
|
||
{template.thumbnail_path ? (
|
||
<img
|
||
src={template.thumbnail_path}
|
||
alt={template.name}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
) : (
|
||
<div className="text-gray-400">
|
||
<Grid size={32} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="p-4">
|
||
<h3 className="font-medium text-gray-900 mb-1 truncate" title={template.name}>
|
||
{template.name}
|
||
</h3>
|
||
<p className="text-sm text-gray-600 mb-2 line-clamp-2">
|
||
{template.description}
|
||
</p>
|
||
|
||
<div className="text-xs text-gray-500 space-y-1">
|
||
<div>时长: {formatDuration(template.duration)}</div>
|
||
<div>素材: {template.material_count} 个</div>
|
||
<div>轨道: {template.track_count} 个</div>
|
||
<div>创建: {formatDate(template.created_at)}</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-100">
|
||
<button
|
||
onClick={() => loadTemplateDetail(template)}
|
||
className="flex items-center gap-1 text-blue-600 hover:text-blue-700 text-sm"
|
||
>
|
||
<Eye size={14} />
|
||
预览
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteTemplate(template.id)}
|
||
className="flex items-center gap-1 text-red-600 hover:text-red-700 text-sm"
|
||
>
|
||
<Trash2 size={14} />
|
||
删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
{/* List View */}
|
||
<div className="flex-1">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h3 className="font-medium text-gray-900">{template.name}</h3>
|
||
<p className="text-sm text-gray-600">{template.description}</p>
|
||
</div>
|
||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||
<span>{formatDuration(template.duration)}</span>
|
||
<span>{template.material_count} 素材</span>
|
||
<span>{template.track_count} 轨道</span>
|
||
<span>{formatDate(template.created_at)}</span>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<button
|
||
onClick={() => loadTemplateDetail(template)}
|
||
className="p-2 text-blue-600 hover:text-blue-700"
|
||
>
|
||
<Eye size={16} />
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteTemplate(template.id)}
|
||
className="p-2 text-red-600 hover:text-red-700"
|
||
>
|
||
<Trash2 size={16} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Import Progress Modal */}
|
||
{showImportModal && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[80vh] overflow-hidden">
|
||
<div className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-xl font-bold text-gray-900">模板导入进度</h2>
|
||
{!importing && (
|
||
<button
|
||
onClick={() => setShowImportModal(false)}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Progress Bar */}
|
||
{importProgress && (
|
||
<div className="mb-6">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm font-medium text-gray-700">{importProgress.step}</span>
|
||
<span className="text-sm text-gray-500">
|
||
{importProgress.progress >= 0 ? `${Math.round(importProgress.progress)}%` : '处理中...'}
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||
<div
|
||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||
style={{
|
||
width: importProgress.progress >= 0 ? `${importProgress.progress}%` : '50%'
|
||
}}
|
||
></div>
|
||
</div>
|
||
<p className="text-sm text-gray-600 mt-2">{importProgress.message}</p>
|
||
{importProgress.details?.processed_templates !== undefined && importProgress.details?.total_templates !== undefined && (
|
||
<p className="text-xs text-gray-500 mt-1">
|
||
已处理: {importProgress.details.processed_templates} / {importProgress.details.total_templates}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Real-time Logs */}
|
||
<div className="mb-6">
|
||
<h3 className="text-sm font-medium text-gray-700 mb-2">实时日志</h3>
|
||
<div className="bg-gray-50 rounded-lg p-4 h-64 overflow-y-auto">
|
||
<div className="space-y-1">
|
||
{importLogs.map((log, index) => (
|
||
<div key={index} className="text-xs text-gray-600 font-mono">
|
||
{log}
|
||
</div>
|
||
))}
|
||
{importLogs.length === 0 && (
|
||
<div className="text-xs text-gray-400 italic">等待日志输出...</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Import Result */}
|
||
{importResult && (
|
||
<div className={`p-4 rounded-lg ${importResult.status ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h3 className={`font-medium ${importResult.status ? 'text-green-800' : 'text-red-800'}`}>
|
||
导入完成
|
||
</h3>
|
||
<p className={`text-sm ${importResult.status ? 'text-green-600' : 'text-red-600'}`}>
|
||
{importResult.msg}
|
||
</p>
|
||
{importResult.imported_count > 0 && (
|
||
<p className="text-sm text-green-600 mt-1">
|
||
成功导入 {importResult.imported_count} 个模板
|
||
</p>
|
||
)}
|
||
{importResult.failed_count > 0 && (
|
||
<details className="mt-2">
|
||
<summary className="text-sm text-red-600 cursor-pointer">
|
||
{importResult.failed_count} 个模板导入失败 (点击查看详情)
|
||
</summary>
|
||
<div className="mt-2 space-y-1">
|
||
{importResult.failed_templates.map((failed: any, index: number) => (
|
||
<div key={index} className="text-xs text-red-500">
|
||
{failed.name}: {failed.error}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</details>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200 mt-6">
|
||
{importing ? (
|
||
<div className="flex items-center text-blue-600">
|
||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-600 border-t-transparent mr-2"></div>
|
||
导入中...
|
||
</div>
|
||
) : (
|
||
<button
|
||
onClick={() => setShowImportModal(false)}
|
||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||
>
|
||
关闭
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Template Detail Modal */}
|
||
{selectedTemplate && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
||
<div className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-xl font-bold text-gray-900">模板详情</h2>
|
||
<button
|
||
onClick={() => {
|
||
setSelectedTemplate(null)
|
||
setTemplateDetail(null)
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">名称</label>
|
||
<p className="text-gray-900">{selectedTemplate.name}</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">描述</label>
|
||
<p className="text-gray-900">{selectedTemplate.description}</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">时长</label>
|
||
<p className="text-gray-900">{formatDuration(selectedTemplate.duration)}</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">素材数量</label>
|
||
<p className="text-gray-900">{selectedTemplate.material_count}</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">轨道数量</label>
|
||
<p className="text-gray-900">{selectedTemplate.track_count}</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">画布尺寸</label>
|
||
<p className="text-gray-900">
|
||
{selectedTemplate.canvas_config?.width || 0} × {selectedTemplate.canvas_config?.height || 0}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">创建时间</label>
|
||
<p className="text-gray-900">{new Date(selectedTemplate.created_at).toLocaleString('zh-CN')}</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">更新时间</label>
|
||
<p className="text-gray-900">{new Date(selectedTemplate.updated_at).toLocaleString('zh-CN')}</p>
|
||
</div>
|
||
|
||
{/* 轨道和片段信息 */}
|
||
<div className="border-t border-gray-200 pt-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-3">轨道和片段信息</label>
|
||
|
||
{loadingDetail ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||
<span className="ml-2 text-gray-600">加载详情中...</span>
|
||
</div>
|
||
) : templateDetail ? (
|
||
<div className="space-y-4">
|
||
{/* 画布信息 */}
|
||
<div className="bg-gray-50 rounded-lg p-3">
|
||
<h4 className="font-medium text-gray-900 mb-2">画布配置</h4>
|
||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||
<div>尺寸: {templateDetail.canvas_config?.width || 0} × {templateDetail.canvas_config?.height || 0}</div>
|
||
<div>帧率: {templateDetail.fps || 30} FPS</div>
|
||
<div>时长: {formatDuration(templateDetail.duration)}</div>
|
||
{templateDetail.sample_rate && (
|
||
<div>采样率: {templateDetail.sample_rate} Hz</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 轨道列表 */}
|
||
<div className="space-y-3">
|
||
{templateDetail.tracks.map((track, trackIndex) => (
|
||
<div key={track.id} className="border border-gray-200 rounded-lg p-3">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<h4 className="font-medium text-gray-900">
|
||
轨道 {track.index + 1}: {track.name}
|
||
</h4>
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||
track.type === 'video' ? 'bg-blue-100 text-blue-800' :
|
||
track.type === 'audio' ? 'bg-green-100 text-green-800' :
|
||
'bg-purple-100 text-purple-800'
|
||
}`}>
|
||
{track.type === 'video' ? '视频' : track.type === 'audio' ? '音频' : '字幕'}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 片段列表 */}
|
||
<div className="space-y-2">
|
||
{track.segments.length > 0 ? (
|
||
track.segments.map((segment, segmentIndex) => (
|
||
<div key={segment.id} className="bg-gray-50 rounded p-2 text-sm">
|
||
<div className="flex items-center justify-between mb-1">
|
||
<span className="font-medium">{segment.name}</span>
|
||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||
segment.type === 'video' ? 'bg-blue-100 text-blue-700' :
|
||
segment.type === 'audio' ? 'bg-green-100 text-green-700' :
|
||
segment.type === 'image' ? 'bg-yellow-100 text-yellow-700' :
|
||
segment.type === 'text' ? 'bg-purple-100 text-purple-700' :
|
||
'bg-gray-100 text-gray-700'
|
||
}`}>
|
||
{segment.type === 'video' ? '视频' :
|
||
segment.type === 'audio' ? '音频' :
|
||
segment.type === 'image' ? '图片' :
|
||
segment.type === 'text' ? '文本' : '特效'}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-2 text-xs text-gray-600">
|
||
<div>开始: {formatTime(segment.start_time)}</div>
|
||
<div>结束: {formatTime(segment.end_time)}</div>
|
||
<div>时长: {formatTime(segment.duration)}</div>
|
||
</div>
|
||
{segment.resource_path && (
|
||
<div className="text-xs text-gray-500 mt-1 truncate">
|
||
资源: {segment.resource_path}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))
|
||
) : (
|
||
<div className="text-sm text-gray-500 italic">该轨道暂无片段</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-8 text-gray-500">
|
||
<p>无法加载模板详情</p>
|
||
<p className="text-sm">请检查模板文件是否完整</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||
<button
|
||
onClick={() => {
|
||
setSelectedTemplate(null)
|
||
setTemplateDetail(null)
|
||
}}
|
||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||
>
|
||
关闭
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteTemplate(selectedTemplate.id)}
|
||
className="px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 transition-colors"
|
||
>
|
||
删除模板
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default TemplateManagePage
|