fix: template page
This commit is contained in:
@@ -8,7 +8,7 @@ import EditorPage from './pages/EditorPage'
|
|||||||
import AIVideoPage from './pages/AIVideoPage'
|
import AIVideoPage from './pages/AIVideoPage'
|
||||||
import SettingsPage from './pages/SettingsPage'
|
import SettingsPage from './pages/SettingsPage'
|
||||||
import TemplateManagePage from './pages/TemplateManagePageV2'
|
import TemplateManagePage from './pages/TemplateManagePageV2'
|
||||||
import TemplateDetailPage from './pages/TemplateDetailPage'
|
import TemplateDetailPage from './pages/TemplateDetailPageV2'
|
||||||
import ResourceCategoryPage from './pages/ResourceCategoryPage'
|
import ResourceCategoryPage from './pages/ResourceCategoryPage'
|
||||||
import ProjectManagePage from './pages/ProjectManagePage'
|
import ProjectManagePage from './pages/ProjectManagePage'
|
||||||
import ProjectDetailPage from './pages/ProjectDetailPage'
|
import ProjectDetailPage from './pages/ProjectDetailPage'
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
|
||||||
import { ArrowLeft, RotateCcw, Trash2, ZoomIn, ZoomOut } from 'lucide-react'
|
|
||||||
import { TemplateService } from '../services/templateService'
|
|
||||||
import type { TemplateInfo } from '../hooks/useProgressCommand'
|
|
||||||
import { TimelineRuler, TrackTimeline } from '../components/timeline'
|
|
||||||
import { Track } from '@/components/timeline/TrackTimeline'
|
|
||||||
|
|
||||||
interface TemplateDetail {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
canvas_config: any
|
|
||||||
tracks: Track[]
|
|
||||||
duration: number
|
|
||||||
fps: number
|
|
||||||
sample_rate?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const TemplateDetailPage: React.FC = () => {
|
|
||||||
const { templateId } = useParams<{ templateId: string }>()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const [template, setTemplate] = useState<TemplateInfo | null>(null)
|
|
||||||
const [templateDetail, setTemplateDetail] = useState<TemplateDetail | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [currentTime, setCurrentTime] = useState(0)
|
|
||||||
const [timelineScale, setTimelineScale] = useState(100) // pixels per second
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (templateId) {
|
|
||||||
loadTemplateDetail()
|
|
||||||
}
|
|
||||||
}, [templateId])
|
|
||||||
|
|
||||||
const loadTemplateDetail = async () => {
|
|
||||||
if (!templateId) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
// Load basic template info
|
|
||||||
const templates = await TemplateService.getTemplates()
|
|
||||||
const templateInfo = templates.templates?.find((t: TemplateInfo) => t.id === templateId)
|
|
||||||
setTemplate(templateInfo || null)
|
|
||||||
|
|
||||||
// Load detailed info
|
|
||||||
const detail = await TemplateService.getTemplateDetail(templateId)
|
|
||||||
setTemplateDetail(detail)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load template detail:', error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTime = (seconds: number): string => {
|
|
||||||
const minutes = Math.floor(seconds / 60)
|
|
||||||
const secs = Math.floor(seconds % 60)
|
|
||||||
const ms = Math.floor((seconds % 1) * 100)
|
|
||||||
return `${minutes}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 handleDeleteTemplate = async () => {
|
|
||||||
if (!template || !confirm('确定要删除这个模板吗?此操作不可撤销。')) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await TemplateService.deleteTemplate(template.id)
|
|
||||||
if (result.status) {
|
|
||||||
navigate('/templates')
|
|
||||||
} else {
|
|
||||||
alert('删除失败: ' + result.msg)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Delete failed:', error)
|
|
||||||
alert('删除失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSegmentNameChange = (segmentId: string, newName: string) => {
|
|
||||||
if (!templateDetail) return
|
|
||||||
|
|
||||||
// Update the segment name in the local state
|
|
||||||
const updatedDetail = {
|
|
||||||
...templateDetail,
|
|
||||||
tracks: templateDetail.tracks.map(track => ({
|
|
||||||
...track,
|
|
||||||
segments: track.segments.map(segment =>
|
|
||||||
segment.id === segmentId ? { ...segment, name: newName } : segment
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
setTemplateDetail(updatedDetail)
|
|
||||||
|
|
||||||
// TODO: Save to backend
|
|
||||||
console.log(`Segment ${segmentId} renamed to: ${newName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
||||||
<span className="ml-3 text-gray-600">加载模板详情中...</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!template || !templateDetail) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">模板未找到</h2>
|
|
||||||
<p className="text-gray-600 mb-4">请检查模板ID是否正确</p>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/templates')}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
返回模板列表
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalDuration = templateDetail.duration / 1000000 // Convert to seconds
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="bg-white border-b border-gray-200">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="flex items-center justify-between py-4">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/templates')}
|
|
||||||
className="flex items-center text-gray-600 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
<ArrowLeft size={20} />
|
|
||||||
<span className="ml-2">返回</span>
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">{template.name}</h1>
|
|
||||||
<p className="text-gray-600">{template.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<button
|
|
||||||
onClick={handleDeleteTemplate}
|
|
||||||
className="flex items-center px-4 py-2 text-red-600 border border-red-600 rounded-lg hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<Trash2 size={16} />
|
|
||||||
<span className="ml-2">删除模板</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
||||||
{/* Template Info */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">模板信息</h2>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">画布尺寸</label>
|
|
||||||
<p className="text-gray-900">
|
|
||||||
{templateDetail.canvas_config?.width || 0} × {templateDetail.canvas_config?.height || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">帧率</label>
|
|
||||||
<p className="text-gray-900">{templateDetail.fps || 30} FPS</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">时长</label>
|
|
||||||
<p className="text-gray-900">{formatDuration(templateDetail.duration)}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">轨道数量</label>
|
|
||||||
<p className="text-gray-900">{templateDetail.tracks.length}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline Controls */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">时间轴预览</h2>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setTimelineScale(Math.max(50, timelineScale - 25))}
|
|
||||||
className="p-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
|
||||||
title="缩小"
|
|
||||||
>
|
|
||||||
<ZoomOut size={16} />
|
|
||||||
</button>
|
|
||||||
<span className="text-sm text-gray-600 min-w-[60px] text-center">
|
|
||||||
{timelineScale}px/s
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setTimelineScale(Math.min(200, timelineScale + 25))}
|
|
||||||
className="p-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
|
||||||
title="放大"
|
|
||||||
>
|
|
||||||
<ZoomIn size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentTime(0)}
|
|
||||||
className="flex items-center px-3 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<RotateCcw size={14} />
|
|
||||||
<span className="ml-1">重置</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Time Ruler */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<TimelineRuler
|
|
||||||
duration={totalDuration}
|
|
||||||
currentTime={currentTime}
|
|
||||||
scale={timelineScale}
|
|
||||||
onTimeClick={setCurrentTime}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Time Display */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-sm text-gray-700">当前时间:</span>
|
|
||||||
<span className="font-mono text-lg">{formatTime(currentTime)}</span>
|
|
||||||
<span className="text-sm text-gray-500">/ {formatTime(totalDuration)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timeline Tracks */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">轨道时间轴</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{templateDetail.tracks.map((track) => (
|
|
||||||
<TrackTimeline
|
|
||||||
key={track.id}
|
|
||||||
track={track}
|
|
||||||
totalDuration={totalDuration}
|
|
||||||
currentTime={currentTime}
|
|
||||||
onSegmentClick={(segment) => {
|
|
||||||
// Jump to segment start time
|
|
||||||
setCurrentTime(segment.start_time)
|
|
||||||
}}
|
|
||||||
onSegmentHover={(segment) => {
|
|
||||||
// Could show segment details in a tooltip
|
|
||||||
console.log('Hovering segment:', segment?.name)
|
|
||||||
}}
|
|
||||||
onSegmentNameChange={handleSegmentNameChange}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TemplateDetailPage
|
|
||||||
441
src/pages/TemplateDetailPageV2.tsx
Normal file
441
src/pages/TemplateDetailPageV2.tsx
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
RotateCcw,
|
||||||
|
Trash2,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Download,
|
||||||
|
Share2,
|
||||||
|
Edit3,
|
||||||
|
Tag,
|
||||||
|
Clock,
|
||||||
|
Monitor,
|
||||||
|
Layers,
|
||||||
|
Cloud,
|
||||||
|
User,
|
||||||
|
Calendar,
|
||||||
|
BarChart3
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { TemplateServiceV2, type TemplateInfo, type TemplateDetail } from '../services/TemplateServiceV2'
|
||||||
|
import { TimelineRuler, TrackTimeline } from '../components/timeline'
|
||||||
|
import { useTemplateOperationV2 } from '../hooks/useTemplateProgressV2'
|
||||||
|
|
||||||
|
const TemplateDetailPageV2: React.FC = () => {
|
||||||
|
const { templateId } = useParams<{ templateId: string }>()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const [template, setTemplate] = useState<TemplateInfo | null>(null)
|
||||||
|
const [templateDetail, setTemplateDetail] = useState<TemplateDetail | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
|
const [timelineScale, setTimelineScale] = useState(100) // pixels per second
|
||||||
|
const [showAdvancedInfo, setShowAdvancedInfo] = useState(false)
|
||||||
|
|
||||||
|
// 使用操作Hook
|
||||||
|
const { loading: deleting, error: deleteError, execute: executeDelete } = useTemplateOperationV2()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (templateId) {
|
||||||
|
loadTemplateDetail()
|
||||||
|
}
|
||||||
|
}, [templateId])
|
||||||
|
|
||||||
|
const loadTemplateDetail = async () => {
|
||||||
|
if (!templateId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// 并行加载基本信息和详细信息
|
||||||
|
const [templateInfo, detail] = await Promise.all([
|
||||||
|
TemplateServiceV2.getTemplate(templateId),
|
||||||
|
TemplateServiceV2.getTemplateDetail(templateId)
|
||||||
|
])
|
||||||
|
|
||||||
|
setTemplate(templateInfo)
|
||||||
|
setTemplateDetail(detail)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load template detail:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (seconds: number): string => {
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const secs = Math.floor(seconds % 60)
|
||||||
|
const ms = Math.floor((seconds % 1) * 100)
|
||||||
|
return `${minutes}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDuration = (duration: number) => {
|
||||||
|
// 如果duration是微秒,转换为秒
|
||||||
|
const seconds = duration > 1000000 ? Math.floor(duration / 1000000) : duration
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteTemplate = async () => {
|
||||||
|
if (!template || !confirm('确定要删除这个模板吗?此操作不可撤销。')) return
|
||||||
|
|
||||||
|
const success = await executeDelete(() => TemplateServiceV2.deleteTemplate(template.id))
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
navigate('/templates-v2')
|
||||||
|
} else if (deleteError) {
|
||||||
|
alert('删除失败: ' + deleteError.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSegmentNameChange = (segmentId: string, newName: string) => {
|
||||||
|
if (!templateDetail) return
|
||||||
|
|
||||||
|
// 更新本地状态中的片段名称
|
||||||
|
const updatedDetail = {
|
||||||
|
...templateDetail,
|
||||||
|
tracks: templateDetail.tracks.map(track => ({
|
||||||
|
...track,
|
||||||
|
segments: track.segments.map(segment =>
|
||||||
|
segment.id === segmentId ? { ...segment, name: newName } : segment
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
setTemplateDetail(updatedDetail)
|
||||||
|
|
||||||
|
// TODO: 保存到后端
|
||||||
|
console.log(`Segment ${segmentId} renamed to: ${newName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
|
<span className="ml-3 text-gray-600">加载模板详情中...</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">模板未找到</h2>
|
||||||
|
<p className="text-gray-600 mb-4">请检查模板ID是否正确</p>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/templates-v2')}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
返回模板列表
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDuration = templateDetail ?
|
||||||
|
(templateDetail.duration > 1000000 ? templateDetail.duration / 1000000 : templateDetail.duration) :
|
||||||
|
template.duration
|
||||||
|
|
||||||
|
const isCloudTemplate = TemplateServiceV2.isCloudTemplate(template)
|
||||||
|
const isUserTemplate = TemplateServiceV2.isUserTemplate(template)
|
||||||
|
const thumbnailUrl = TemplateServiceV2.getThumbnailUrl(template)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white border-b border-gray-200">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center justify-between py-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/templates-v2')}
|
||||||
|
className="flex items-center text-gray-600 hover:text-gray-900"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
<span className="ml-2">返回</span>
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{thumbnailUrl && (
|
||||||
|
<img
|
||||||
|
src={thumbnailUrl}
|
||||||
|
alt={template.name}
|
||||||
|
className="w-12 h-8 object-cover rounded border"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">{template.name}</h1>
|
||||||
|
{isCloudTemplate && (
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
|
<Cloud size={12} className="mr-1" />
|
||||||
|
云端模板
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isUserTemplate && (
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
<User size={12} className="mr-1" />
|
||||||
|
我的模板
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600">{template.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdvancedInfo(!showAdvancedInfo)}
|
||||||
|
className="flex items-center px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<BarChart3 size={16} />
|
||||||
|
<span className="ml-2">详细信息</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteTemplate}
|
||||||
|
disabled={deleting}
|
||||||
|
className="flex items-center px-4 py-2 text-red-600 border border-red-600 rounded-lg hover:bg-red-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
<span className="ml-2">{deleting ? '删除中...' : '删除模板'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
{/* Template Info Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||||
|
{/* 基本信息卡片 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">基本信息</h3>
|
||||||
|
<Monitor className="text-blue-500" size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">画布尺寸</label>
|
||||||
|
<p className="text-gray-900">
|
||||||
|
{templateDetail?.canvas_config?.width || template.canvas_config?.width || 0} × {templateDetail?.canvas_config?.height || template.canvas_config?.height || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">帧率</label>
|
||||||
|
<p className="text-gray-900">{templateDetail?.fps || 30} FPS</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 时长信息卡片 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">时长信息</h3>
|
||||||
|
<Clock className="text-green-500" size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">总时长</label>
|
||||||
|
<p className="text-gray-900">{formatDuration(totalDuration)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">轨道数量</label>
|
||||||
|
<p className="text-gray-900">{templateDetail?.tracks.length || template.track_count}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 素材信息卡片 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">素材信息</h3>
|
||||||
|
<Layers className="text-purple-500" size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">素材数量</label>
|
||||||
|
<p className="text-gray-900">{template.material_count}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">片段总数</label>
|
||||||
|
<p className="text-gray-900">
|
||||||
|
{templateDetail?.tracks.reduce((total, track) => total + track.segments.length, 0) || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 创建信息卡片 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">创建信息</h3>
|
||||||
|
<Calendar className="text-orange-500" size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">创建时间</label>
|
||||||
|
<p className="text-gray-900">{TemplateServiceV2.formatDate(template.created_at)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">更新时间</label>
|
||||||
|
<p className="text-gray-900">{TemplateServiceV2.formatDate(template.updated_at)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标签信息 */}
|
||||||
|
{template.tags.length > 0 && (
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<Tag className="text-blue-500 mr-2" size={20} />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">标签</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{template.tags.map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 高级信息 */}
|
||||||
|
{showAdvancedInfo && (
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">高级信息</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">模板ID</label>
|
||||||
|
<p className="text-gray-900 font-mono text-sm bg-gray-50 p-2 rounded">{template.id}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">用户ID</label>
|
||||||
|
<p className="text-gray-900 font-mono text-sm bg-gray-50 p-2 rounded">{template.user_id}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">草稿路径</label>
|
||||||
|
<p className="text-gray-900 text-sm bg-gray-50 p-2 rounded break-all">{template.draft_content_path}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">资源路径</label>
|
||||||
|
<p className="text-gray-900 text-sm bg-gray-50 p-2 rounded break-all">{template.resources_path}</p>
|
||||||
|
</div>
|
||||||
|
{templateDetail?.sample_rate && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">采样率</label>
|
||||||
|
<p className="text-gray-900">{templateDetail.sample_rate} Hz</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 时间轴预览 */}
|
||||||
|
{templateDetail && (
|
||||||
|
<>
|
||||||
|
{/* Timeline Controls */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">时间轴预览</h2>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setTimelineScale(Math.max(50, timelineScale - 25))}
|
||||||
|
className="p-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
||||||
|
title="缩小"
|
||||||
|
>
|
||||||
|
<ZoomOut size={16} />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-gray-600 min-w-[60px] text-center">
|
||||||
|
{timelineScale}px/s
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setTimelineScale(Math.min(200, timelineScale + 25))}
|
||||||
|
className="p-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
||||||
|
title="放大"
|
||||||
|
>
|
||||||
|
<ZoomIn size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentTime(0)}
|
||||||
|
className="flex items-center px-3 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
<span className="ml-1">重置</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time Ruler */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<TimelineRuler
|
||||||
|
duration={totalDuration}
|
||||||
|
currentTime={currentTime}
|
||||||
|
scale={timelineScale}
|
||||||
|
onTimeClick={setCurrentTime}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current Time Display */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span className="text-sm text-gray-700">当前时间:</span>
|
||||||
|
<span className="font-mono text-lg">{formatTime(currentTime)}</span>
|
||||||
|
<span className="text-sm text-gray-500">/ {formatTime(totalDuration)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline Tracks */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">轨道时间轴</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{templateDetail.tracks.map((track) => (
|
||||||
|
<TrackTimeline
|
||||||
|
key={track.id}
|
||||||
|
track={track}
|
||||||
|
totalDuration={totalDuration}
|
||||||
|
currentTime={currentTime}
|
||||||
|
onSegmentClick={(segment) => {
|
||||||
|
// 跳转到片段开始时间
|
||||||
|
setCurrentTime(segment.start_time)
|
||||||
|
}}
|
||||||
|
onSegmentHover={(segment) => {
|
||||||
|
// 可以在工具提示中显示片段详情
|
||||||
|
console.log('Hovering segment:', segment?.name)
|
||||||
|
}}
|
||||||
|
onSegmentNameChange={handleSegmentNameChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 如果没有详细信息 */}
|
||||||
|
{!templateDetail && (
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="text-gray-400 mb-4">
|
||||||
|
<Layers size={48} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无详细信息</h3>
|
||||||
|
<p className="text-gray-600">该模板的详细轨道信息暂时无法加载</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplateDetailPageV2
|
||||||
@@ -118,7 +118,7 @@ export interface TrackSegment {
|
|||||||
export interface Track {
|
export interface Track {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'video' | 'audio' | 'subtitle'
|
type: 'video' | 'audio' | 'effect' | 'text' | 'sticker' | 'image'
|
||||||
index: number
|
index: number
|
||||||
segments: TrackSegment[]
|
segments: TrackSegment[]
|
||||||
properties?: any
|
properties?: any
|
||||||
|
|||||||
Reference in New Issue
Block a user