fix: template page

This commit is contained in:
root
2025-07-12 21:10:43 +08:00
parent 012dafcbde
commit cc33875d16
4 changed files with 443 additions and 279 deletions

View File

@@ -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'

View File

@@ -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

View 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

View File

@@ -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