fix: template page
This commit is contained in:
@@ -8,7 +8,7 @@ import EditorPage from './pages/EditorPage'
|
||||
import AIVideoPage from './pages/AIVideoPage'
|
||||
import SettingsPage from './pages/SettingsPage'
|
||||
import TemplateManagePage from './pages/TemplateManagePageV2'
|
||||
import TemplateDetailPage from './pages/TemplateDetailPage'
|
||||
import TemplateDetailPage from './pages/TemplateDetailPageV2'
|
||||
import ResourceCategoryPage from './pages/ResourceCategoryPage'
|
||||
import ProjectManagePage from './pages/ProjectManagePage'
|
||||
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 {
|
||||
id: string
|
||||
name: string
|
||||
type: 'video' | 'audio' | 'subtitle'
|
||||
type: 'video' | 'audio' | 'effect' | 'text' | 'sticker' | 'image'
|
||||
index: number
|
||||
segments: TrackSegment[]
|
||||
properties?: any
|
||||
|
||||
Reference in New Issue
Block a user