From cc33875d16123037a400e8e691333c6900593b57 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 12 Jul 2025 21:10:43 +0800 Subject: [PATCH] fix: template page --- src/App.tsx | 2 +- src/pages/TemplateDetailPage.tsx | 277 ------------------ src/pages/TemplateDetailPageV2.tsx | 441 +++++++++++++++++++++++++++++ src/services/TemplateServiceV2.ts | 2 +- 4 files changed, 443 insertions(+), 279 deletions(-) delete mode 100644 src/pages/TemplateDetailPage.tsx create mode 100644 src/pages/TemplateDetailPageV2.tsx diff --git a/src/App.tsx b/src/App.tsx index d4b64de..1b85d70 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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' diff --git a/src/pages/TemplateDetailPage.tsx b/src/pages/TemplateDetailPage.tsx deleted file mode 100644 index 5e3e695..0000000 --- a/src/pages/TemplateDetailPage.tsx +++ /dev/null @@ -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(null) - const [templateDetail, setTemplateDetail] = useState(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 ( -
-
- 加载模板详情中... -
- ) - } - - if (!template || !templateDetail) { - return ( -
-
-

模板未找到

-

请检查模板ID是否正确

- -
-
- ) - } - - const totalDuration = templateDetail.duration / 1000000 // Convert to seconds - - return ( -
- {/* Header */} -
-
-
-
- -
-

{template.name}

-

{template.description}

-
-
-
- -
-
-
-
- -
- {/* Template Info */} -
-

模板信息

-
-
- -

- {templateDetail.canvas_config?.width || 0} × {templateDetail.canvas_config?.height || 0} -

-
-
- -

{templateDetail.fps || 30} FPS

-
-
- -

{formatDuration(templateDetail.duration)}

-
-
- -

{templateDetail.tracks.length}

-
-
-
- - {/* Timeline Controls */} -
-
-

时间轴预览

-
-
- - - {timelineScale}px/s - - -
- -
-
- - {/* Time Ruler */} -
- -
- - {/* Current Time Display */} -
-
- 当前时间: - {formatTime(currentTime)} - / {formatTime(totalDuration)} -
-
-
- - {/* Timeline Tracks */} -
-

轨道时间轴

-
- {templateDetail.tracks.map((track) => ( - { - // 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} - /> - ))} -
-
-
-
- ) -} - -export default TemplateDetailPage diff --git a/src/pages/TemplateDetailPageV2.tsx b/src/pages/TemplateDetailPageV2.tsx new file mode 100644 index 0000000..ff64821 --- /dev/null +++ b/src/pages/TemplateDetailPageV2.tsx @@ -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(null) + const [templateDetail, setTemplateDetail] = useState(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 ( +
+
+ 加载模板详情中... +
+ ) + } + + if (!template) { + return ( +
+
+

模板未找到

+

请检查模板ID是否正确

+ +
+
+ ) + } + + 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 ( +
+ {/* Header */} +
+
+
+
+ +
+ {thumbnailUrl && ( + {template.name} + )} +
+
+

{template.name}

+ {isCloudTemplate && ( + + + 云端模板 + + )} + {isUserTemplate && ( + + + 我的模板 + + )} +
+

{template.description}

+
+
+
+
+ + +
+
+
+
+ +
+ {/* Template Info Cards */} +
+ {/* 基本信息卡片 */} +
+
+

基本信息

+ +
+
+
+ +

+ {templateDetail?.canvas_config?.width || template.canvas_config?.width || 0} × {templateDetail?.canvas_config?.height || template.canvas_config?.height || 0} +

+
+
+ +

{templateDetail?.fps || 30} FPS

+
+
+
+ + {/* 时长信息卡片 */} +
+
+

时长信息

+ +
+
+
+ +

{formatDuration(totalDuration)}

+
+
+ +

{templateDetail?.tracks.length || template.track_count}

+
+
+
+ + {/* 素材信息卡片 */} +
+
+

素材信息

+ +
+
+
+ +

{template.material_count}

+
+
+ +

+ {templateDetail?.tracks.reduce((total, track) => total + track.segments.length, 0) || 0} +

+
+
+
+ + {/* 创建信息卡片 */} +
+
+

创建信息

+ +
+
+
+ +

{TemplateServiceV2.formatDate(template.created_at)}

+
+
+ +

{TemplateServiceV2.formatDate(template.updated_at)}

+
+
+
+
+ + {/* 标签信息 */} + {template.tags.length > 0 && ( +
+
+ +

标签

+
+
+ {template.tags.map((tag) => ( + + {tag} + + ))} +
+
+ )} + + {/* 高级信息 */} + {showAdvancedInfo && ( +
+

高级信息

+
+
+ +

{template.id}

+
+
+ +

{template.user_id}

+
+
+ +

{template.draft_content_path}

+
+
+ +

{template.resources_path}

+
+ {templateDetail?.sample_rate && ( +
+ +

{templateDetail.sample_rate} Hz

+
+ )} +
+
+ )} + + {/* 时间轴预览 */} + {templateDetail && ( + <> + {/* Timeline Controls */} +
+
+

时间轴预览

+
+
+ + + {timelineScale}px/s + + +
+ +
+
+ + {/* Time Ruler */} +
+ +
+ + {/* Current Time Display */} +
+
+ 当前时间: + {formatTime(currentTime)} + / {formatTime(totalDuration)} +
+
+
+ + {/* Timeline Tracks */} +
+

轨道时间轴

+
+ {templateDetail.tracks.map((track) => ( + { + // 跳转到片段开始时间 + setCurrentTime(segment.start_time) + }} + onSegmentHover={(segment) => { + // 可以在工具提示中显示片段详情 + console.log('Hovering segment:', segment?.name) + }} + onSegmentNameChange={handleSegmentNameChange} + /> + ))} +
+
+ + )} + + {/* 如果没有详细信息 */} + {!templateDetail && ( +
+
+
+ +
+

暂无详细信息

+

该模板的详细轨道信息暂时无法加载

+
+
+ )} +
+
+ ) +} + +export default TemplateDetailPageV2 diff --git a/src/services/TemplateServiceV2.ts b/src/services/TemplateServiceV2.ts index ecaff0c..9fe3331 100644 --- a/src/services/TemplateServiceV2.ts +++ b/src/services/TemplateServiceV2.ts @@ -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