fix: 重构 模板管理
This commit is contained in:
@@ -555,11 +555,7 @@ def main():
|
|||||||
|
|
||||||
detail = manager.get_template_detail(args.template_id)
|
detail = manager.get_template_detail(args.template_id)
|
||||||
if detail:
|
if detail:
|
||||||
result = {
|
rpc.success(detail)
|
||||||
"status": True,
|
|
||||||
"detail": detail
|
|
||||||
}
|
|
||||||
rpc.success(result)
|
|
||||||
else:
|
else:
|
||||||
rpc.error(JSONRPCError.TEMPLATE_NOT_FOUND, f"Template detail not found: {args.template_id}")
|
rpc.error(JSONRPCError.TEMPLATE_NOT_FOUND, f"Template detail not found: {args.template_id}")
|
||||||
|
|
||||||
|
|||||||
125
src/components/template/ImportProgressModal.tsx
Normal file
125
src/components/template/ImportProgressModal.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import type { ProgressState, ImportResult } from '../../hooks/useProgressCommand'
|
||||||
|
|
||||||
|
interface ImportProgressModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
isExecuting: boolean
|
||||||
|
progress: ProgressState | null
|
||||||
|
result: ImportResult | null
|
||||||
|
logs: string[]
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportProgressModal: React.FC<ImportProgressModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
isExecuting,
|
||||||
|
progress,
|
||||||
|
result,
|
||||||
|
logs,
|
||||||
|
onClose
|
||||||
|
}) => {
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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-hidden">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">模板导入进度</h2>
|
||||||
|
{!isExecuting && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
{progress && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium text-gray-700">{progress.step}</span>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{progress.progress >= 0 ? `${progress.progress.toFixed(1)}%` : '处理中...'}
|
||||||
|
</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: `${Math.max(0, progress.progress)}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">{progress.message}</p>
|
||||||
|
{progress.details?.processed_templates !== undefined && progress.details?.total_templates !== undefined && (
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
已处理: {progress.details.processed_templates} / {progress.details.total_templates}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Import Result */}
|
||||||
|
{result && (
|
||||||
|
<div className={`mb-4 p-4 rounded-lg ${result.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 ${result.status ? 'text-green-800' : 'text-red-800'}`}>
|
||||||
|
导入结果
|
||||||
|
</h3>
|
||||||
|
<p className={`text-sm ${result.status ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{result.msg}
|
||||||
|
</p>
|
||||||
|
{result.imported_count > 0 && (
|
||||||
|
<p className="text-sm text-green-600 mt-1">
|
||||||
|
成功导入 {result.imported_count} 个模板
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{result.failed_count > 0 && (
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary className="text-sm text-red-600 cursor-pointer">
|
||||||
|
{result.failed_count} 个模板导入失败 (点击查看详情)
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{result.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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Logs */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4 h-64 overflow-y-auto">
|
||||||
|
<h4 className="font-medium text-gray-900 mb-2">导入日志</h4>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<div key={index} className="text-xs text-gray-600 font-mono">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex justify-end mt-4">
|
||||||
|
{!isExecuting && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
src/components/template/SearchAndFilters.tsx
Normal file
68
src/components/template/SearchAndFilters.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Upload, FolderOpen, Search, Grid, List } from 'lucide-react'
|
||||||
|
|
||||||
|
interface SearchAndFiltersProps {
|
||||||
|
searchTerm: string
|
||||||
|
setSearchTerm: (term: string) => void
|
||||||
|
viewMode: 'grid' | 'list'
|
||||||
|
setViewMode: (mode: 'grid' | 'list') => void
|
||||||
|
onBatchImport: () => void
|
||||||
|
loading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchAndFilters: React.FC<SearchAndFiltersProps> = ({
|
||||||
|
searchTerm,
|
||||||
|
setSearchTerm,
|
||||||
|
viewMode,
|
||||||
|
setViewMode,
|
||||||
|
onBatchImport,
|
||||||
|
loading
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* View Mode Toggle */}
|
||||||
|
<div className="flex items-center bg-gray-100 rounded-lg p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('grid')}
|
||||||
|
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-white shadow-sm' : 'text-gray-600'}`}
|
||||||
|
>
|
||||||
|
<Grid size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('list')}
|
||||||
|
className={`p-2 rounded ${viewMode === 'list' ? 'bg-white shadow-sm' : 'text-gray-600'}`}
|
||||||
|
>
|
||||||
|
<List size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={onBatchImport}
|
||||||
|
disabled={loading}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<FolderOpen size={16} />
|
||||||
|
批量导入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
73
src/components/template/TemplateCard.tsx
Normal file
73
src/components/template/TemplateCard.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Eye, Trash2 } from 'lucide-react'
|
||||||
|
import type { TemplateInfo } from '../../hooks/useProgressCommand'
|
||||||
|
|
||||||
|
interface TemplateCardProps {
|
||||||
|
template: TemplateInfo
|
||||||
|
onView: (template: TemplateInfo) => void
|
||||||
|
onDelete: (templateId: string) => void
|
||||||
|
formatDuration: (duration: number) => string
|
||||||
|
formatDate: (date: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||||
|
template,
|
||||||
|
onView,
|
||||||
|
onDelete,
|
||||||
|
formatDuration,
|
||||||
|
formatDate
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
|
||||||
|
{/* Thumbnail */}
|
||||||
|
<div className="aspect-video 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">
|
||||||
|
<Eye size={32} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="font-medium text-gray-900 mb-2 line-clamp-1">
|
||||||
|
{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={() => onView(template)}
|
||||||
|
className="flex items-center gap-1 text-blue-600 hover:text-blue-700 text-sm"
|
||||||
|
>
|
||||||
|
<Eye size={14} />
|
||||||
|
预览
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onDelete(template.id)}
|
||||||
|
className="flex items-center gap-1 text-red-600 hover:text-red-700 text-sm"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
225
src/components/template/TemplateDetailModal.tsx
Normal file
225
src/components/template/TemplateDetailModal.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import type { TemplateInfo } from '../../hooks/useProgressCommand'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TemplateDetailModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
template: TemplateInfo | null
|
||||||
|
templateDetail: TemplateDetail | null
|
||||||
|
loadingDetail: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onDelete: (templateId: string) => void
|
||||||
|
formatDuration: (duration: number) => string
|
||||||
|
formatTime: (seconds: number) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
template,
|
||||||
|
templateDetail,
|
||||||
|
loadingDetail,
|
||||||
|
onClose,
|
||||||
|
onDelete,
|
||||||
|
formatDuration,
|
||||||
|
formatTime
|
||||||
|
}) => {
|
||||||
|
if (!isOpen || !template) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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-[90vh] 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={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">名称</label>
|
||||||
|
<p className="text-gray-900">{template.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">描述</label>
|
||||||
|
<p className="text-gray-900">{template.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(template.duration)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">素材数量</label>
|
||||||
|
<p className="text-gray-900">{template.material_count}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">轨道数量</label>
|
||||||
|
<p className="text-gray-900">{template.track_count}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">画布尺寸</label>
|
||||||
|
<p className="text-gray-900">
|
||||||
|
{template.canvas_config?.width || 0} × {template.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(template.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(template.updated_at).toLocaleString('zh-CN')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Track and Segment Details */}
|
||||||
|
<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">
|
||||||
|
{/* Canvas Config */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Tracks */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{templateDetail.tracks.map((track) => (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Segments */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{track.segments.length > 0 ? (
|
||||||
|
track.segments.map((segment) => (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onDelete(template.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>
|
||||||
|
)
|
||||||
|
}
|
||||||
79
src/components/template/TemplateGrid.tsx
Normal file
79
src/components/template/TemplateGrid.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { TemplateCard } from './TemplateCard'
|
||||||
|
import { TemplateListItem } from './TemplateListItem'
|
||||||
|
import type { TemplateInfo } from '../../hooks/useProgressCommand'
|
||||||
|
|
||||||
|
interface TemplateGridProps {
|
||||||
|
templates: TemplateInfo[]
|
||||||
|
viewMode: 'grid' | 'list'
|
||||||
|
loading: boolean
|
||||||
|
onView: (template: TemplateInfo) => void
|
||||||
|
onDelete: (templateId: string) => void
|
||||||
|
formatDuration: (duration: number) => string
|
||||||
|
formatDate: (date: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateGrid: React.FC<TemplateGridProps> = ({
|
||||||
|
templates,
|
||||||
|
viewMode,
|
||||||
|
loading,
|
||||||
|
onView,
|
||||||
|
onDelete,
|
||||||
|
formatDuration,
|
||||||
|
formatDate
|
||||||
|
}) => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templates.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-400 mb-4">
|
||||||
|
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2m-9 0h10m-10 0a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V6a2 2 0 00-2-2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无模板</h3>
|
||||||
|
<p className="text-gray-600">点击"批量导入"按钮开始导入模板</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewMode === 'grid') {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<TemplateCard
|
||||||
|
key={template.id}
|
||||||
|
template={template}
|
||||||
|
onView={onView}
|
||||||
|
onDelete={onDelete}
|
||||||
|
formatDuration={formatDuration}
|
||||||
|
formatDate={formatDate}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<TemplateListItem
|
||||||
|
key={template.id}
|
||||||
|
template={template}
|
||||||
|
onView={onView}
|
||||||
|
onDelete={onDelete}
|
||||||
|
formatDuration={formatDuration}
|
||||||
|
formatDate={formatDate}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
src/components/template/TemplateListItem.tsx
Normal file
68
src/components/template/TemplateListItem.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Eye, Trash2 } from 'lucide-react'
|
||||||
|
import type { TemplateInfo } from '../../hooks/useProgressCommand'
|
||||||
|
|
||||||
|
interface TemplateListItemProps {
|
||||||
|
template: TemplateInfo
|
||||||
|
onView: (template: TemplateInfo) => void
|
||||||
|
onDelete: (templateId: string) => void
|
||||||
|
formatDuration: (duration: number) => string
|
||||||
|
formatDate: (date: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateListItem: React.FC<TemplateListItemProps> = ({
|
||||||
|
template,
|
||||||
|
onView,
|
||||||
|
onDelete,
|
||||||
|
formatDuration,
|
||||||
|
formatDate
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{/* Thumbnail */}
|
||||||
|
<div className="w-16 h-12 bg-gray-100 rounded flex items-center justify-center flex-shrink-0">
|
||||||
|
{template.thumbnail_path ? (
|
||||||
|
<img
|
||||||
|
src={template.thumbnail_path}
|
||||||
|
alt={template.name}
|
||||||
|
className="w-full h-full object-cover rounded"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Eye size={16} className="text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-gray-900">{template.name}</h3>
|
||||||
|
<p className="text-sm text-gray-600">{template.description}</p>
|
||||||
|
</div>
|
||||||
|
</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={() => onView(template)}
|
||||||
|
className="p-2 text-blue-600 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
<Eye size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onDelete(template.id)}
|
||||||
|
className="p-2 text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
src/components/template/index.ts
Normal file
6
src/components/template/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { SearchAndFilters } from './SearchAndFilters'
|
||||||
|
export { TemplateCard } from './TemplateCard'
|
||||||
|
export { TemplateListItem } from './TemplateListItem'
|
||||||
|
export { TemplateGrid } from './TemplateGrid'
|
||||||
|
export { ImportProgressModal } from './ImportProgressModal'
|
||||||
|
export { TemplateDetailModal } from './TemplateDetailModal'
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
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 { invoke } from '@tauri-apps/api/core'
|
||||||
import { TemplateService } from '../services/tauri'
|
import { TemplateService } from '../services/tauri'
|
||||||
import { useTemplateProgress } from '../hooks/useProgressCommand'
|
import { useTemplateProgress } from '../hooks/useProgressCommand'
|
||||||
|
import type { TemplateInfo } from '../hooks/useProgressCommand'
|
||||||
|
import {
|
||||||
|
SearchAndFilters,
|
||||||
|
TemplateGrid,
|
||||||
|
ImportProgressModal,
|
||||||
|
TemplateDetailModal
|
||||||
|
} from '../components/template'
|
||||||
|
|
||||||
// 轨道和片段的数据结构
|
// 轨道和片段的数据结构
|
||||||
interface TrackSegment {
|
interface TrackSegment {
|
||||||
@@ -38,11 +43,8 @@ interface TemplateDetail {
|
|||||||
sample_rate?: number
|
sample_rate?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import the progress interface from the hook
|
|
||||||
import type { ProgressState, TemplateInfo } from '../hooks/useProgressCommand'
|
|
||||||
|
|
||||||
|
|
||||||
const TemplateManagePage: React.FC = () => {
|
const TemplateManagePage: React.FC = () => {
|
||||||
|
// State
|
||||||
const [templates, setTemplates] = useState<TemplateInfo[]>([])
|
const [templates, setTemplates] = useState<TemplateInfo[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
@@ -73,6 +75,31 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
loadTemplates()
|
loadTemplates()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
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')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter templates based on search term
|
||||||
|
const filteredTemplates = templates.filter(template =>
|
||||||
|
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
template.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
// API functions
|
||||||
const loadTemplates = async () => {
|
const loadTemplates = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -91,18 +118,15 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载模板详情(包含轨道和片段信息)
|
|
||||||
const loadTemplateDetail = async (template: TemplateInfo) => {
|
const loadTemplateDetail = async (template: TemplateInfo) => {
|
||||||
try {
|
try {
|
||||||
setLoadingDetail(true)
|
setLoadingDetail(true)
|
||||||
setSelectedTemplate(template)
|
setSelectedTemplate(template)
|
||||||
|
|
||||||
// 调用后端API获取模板详情
|
|
||||||
const detail = await TemplateService.getTemplateDetail(template.id)
|
const detail = await TemplateService.getTemplateDetail(template.id)
|
||||||
setTemplateDetail(detail)
|
setTemplateDetail(detail)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load template detail:', error)
|
console.error('Failed to load template detail:', error)
|
||||||
// 如果加载详情失败,至少显示基本信息
|
|
||||||
setTemplateDetail(null)
|
setTemplateDetail(null)
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingDetail(false)
|
setLoadingDetail(false)
|
||||||
@@ -111,11 +135,9 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
|
|
||||||
const handleBatchImport = async () => {
|
const handleBatchImport = async () => {
|
||||||
try {
|
try {
|
||||||
// Select folder using Tauri dialog
|
|
||||||
const folderResult = await invoke<string>('select_folder')
|
const folderResult = await invoke<string>('select_folder')
|
||||||
if (!folderResult) return
|
if (!folderResult) return
|
||||||
|
|
||||||
// Show import modal and start import
|
|
||||||
setShowImportModal(true)
|
setShowImportModal(true)
|
||||||
await batchImport(folderResult)
|
await batchImport(folderResult)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -132,6 +154,7 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
setTemplates(templates.filter(t => t.id !== templateId))
|
setTemplates(templates.filter(t => t.id !== templateId))
|
||||||
if (selectedTemplate?.id === templateId) {
|
if (selectedTemplate?.id === templateId) {
|
||||||
setSelectedTemplate(null)
|
setSelectedTemplate(null)
|
||||||
|
setTemplateDetail(null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('删除失败: ' + result.msg)
|
alert('删除失败: ' + result.msg)
|
||||||
@@ -142,539 +165,67 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredTemplates = templates.filter(template =>
|
const handleCloseDetailModal = () => {
|
||||||
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
setSelectedTemplate(null)
|
||||||
template.description.toLowerCase().includes(searchTerm.toLowerCase())
|
setTemplateDetail(null)
|
||||||
)
|
|
||||||
|
|
||||||
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 handleCloseImportModal = () => {
|
||||||
const formatTime = (seconds: number): string => {
|
setShowImportModal(false)
|
||||||
const minutes = Math.floor(seconds / 60)
|
resetImport()
|
||||||
const secs = (seconds % 60).toFixed(2)
|
|
||||||
return `${minutes}:${secs.padStart(5, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
return new Date(dateString).toLocaleDateString('zh-CN')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-6">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">模板管理</h1>
|
<h1 className="text-3xl font-bold text-gray-900">模板管理</h1>
|
||||||
<p className="text-gray-600">管理和导入视频编辑模板</p>
|
<p className="text-gray-600">管理和导入视频编辑模板</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions Bar */}
|
{/* Search and Filters */}
|
||||||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
<SearchAndFilters
|
||||||
<div className="flex items-center justify-between">
|
searchTerm={searchTerm}
|
||||||
<div className="flex items-center space-x-4">
|
setSearchTerm={setSearchTerm}
|
||||||
<button
|
viewMode={viewMode}
|
||||||
onClick={handleBatchImport}
|
setViewMode={setViewMode}
|
||||||
disabled={importing}
|
onBatchImport={handleBatchImport}
|
||||||
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"
|
loading={loading}
|
||||||
>
|
|
||||||
{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">
|
{/* Template Grid */}
|
||||||
<button
|
<TemplateGrid
|
||||||
onClick={() => setViewMode('grid')}
|
templates={filteredTemplates}
|
||||||
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
|
viewMode={viewMode}
|
||||||
>
|
loading={loading}
|
||||||
<Grid size={16} />
|
onView={loadTemplateDetail}
|
||||||
</button>
|
onDelete={handleDeleteTemplate}
|
||||||
<button
|
formatDuration={formatDuration}
|
||||||
onClick={() => setViewMode('list')}
|
formatDate={formatDate}
|
||||||
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={() => resetImport()}
|
|
||||||
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 */}
|
{/* Import Progress Modal */}
|
||||||
{showImportModal && (
|
<ImportProgressModal
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
isOpen={showImportModal}
|
||||||
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[80vh] overflow-hidden">
|
isExecuting={importing}
|
||||||
<div className="p-6">
|
progress={importProgress}
|
||||||
<div className="flex items-center justify-between mb-4">
|
result={importResult}
|
||||||
<h2 className="text-xl font-bold text-gray-900">模板导入进度</h2>
|
logs={importLogs}
|
||||||
{!importing && (
|
onClose={handleCloseImportModal}
|
||||||
<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 */}
|
{/* Template Detail Modal */}
|
||||||
{selectedTemplate && (
|
<TemplateDetailModal
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
isOpen={!!selectedTemplate}
|
||||||
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
template={selectedTemplate}
|
||||||
<div className="p-6">
|
templateDetail={templateDetail}
|
||||||
<div className="flex items-center justify-between mb-4">
|
loadingDetail={loadingDetail}
|
||||||
<h2 className="text-xl font-bold text-gray-900">模板详情</h2>
|
onClose={handleCloseDetailModal}
|
||||||
<button
|
onDelete={handleDeleteTemplate}
|
||||||
onClick={() => {
|
formatDuration={formatDuration}
|
||||||
setSelectedTemplate(null)
|
formatTime={formatTime}
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user