From 3cf99158770a80aff816cae3f7fb331f92634fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Sat, 20 Sep 2025 19:24:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E7=BB=88=E8=A7=86=E9=A2=91=E4=B8=8E?= =?UTF-8?q?=E5=88=86=E9=95=9C=E5=88=97=E8=A1=A8=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/work-flow.tsx | 52 ++++++++++++++++++- components/pages/work-flow/H5MediaViewer.tsx | 34 +++++++++++- components/pages/work-flow/H5TaskInfo.tsx | 28 ++++++++-- components/pages/work-flow/media-viewer.tsx | 15 ++++-- components/pages/work-flow/thumbnail-grid.tsx | 10 ++-- 5 files changed, 125 insertions(+), 14 deletions(-) diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index fe4d26a..88b3e6a 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -19,6 +19,7 @@ import { Drawer, Tooltip, notification } from 'antd'; import { showEditingNotification } from "@/components/pages/work-flow/editing-notification"; // import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; import { exportVideoWithRetry } from '@/utils/export-service'; +import { getFirstFrame } from '@/utils/tools'; // 临时禁用视频编辑功能 // import { EditPoint as EditPointType } from './work-flow/video-edit/types'; import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; @@ -51,6 +52,8 @@ const WorkFlow = React.memo(function WorkFlow() { const [previewVideoUrl, setPreviewVideoUrl] = React.useState(null); const [previewVideoId, setPreviewVideoId] = React.useState(null); const [isFocusChatInput, setIsFocusChatInput] = React.useState(false); + const [selectedView, setSelectedView] = React.useState<'final' | 'video' | null>(null); + const [isFinalBarOpen, setIsFinalBarOpen] = React.useState(true); const [aiEditingResult, setAiEditingResult] = React.useState(null); // const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise }>(null); @@ -236,6 +239,13 @@ const WorkFlow = React.memo(function WorkFlow() { console.log('changedIndex_work-flow', currentSketchIndex, taskObject); }, [currentSketchIndex, taskObject]); + // 当最终视频出现时,默认选中最终视频 + useEffect(() => { + if (taskObject?.final?.url && selectedView === null) { + setSelectedView('final'); + } + }, [taskObject?.final?.url, selectedView]); + // 监听粗剪是否完成 useEffect(() => { console.log('🎬 final video useEffect triggered:', { @@ -409,6 +419,7 @@ Please process this video editing request.`; title={taskObject.title} current={currentSketchIndex + 1} taskObject={taskObject} + selectedView={selectedView} currentLoadingText={currentLoadingText} /> ) : ( @@ -431,11 +442,44 @@ Please process this video editing request.`; > {isDesktop ? (
+ {/* 左侧最终视频缩略图栏(仅桌面) */} + {taskObject?.final?.url && ( +
+
+ {isFinalBarOpen && ( +
+ {/* 预留历史列表,目前仅展示当前最终视频 */} + +
+ )} +
+
+ )} handleRetryVideo(video_id)} + onSelectView={(view) => setSelectedView(view)} // 临时禁用视频编辑功能: enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit} /> )} @@ -484,9 +530,13 @@ Please process this video editing request.`; isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput} taskObject={taskObject} currentSketchIndex={currentSketchIndex} - onSketchSelect={setCurrentSketchIndex} + onSketchSelect={(index) => { + setSelectedView('video'); + setCurrentSketchIndex(index); + }} onRetryVideo={handleRetryVideo} className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[40%]')} + selectedView={selectedView} />
)} diff --git a/components/pages/work-flow/H5MediaViewer.tsx b/components/pages/work-flow/H5MediaViewer.tsx index 53331f0..02f6c2e 100644 --- a/components/pages/work-flow/H5MediaViewer.tsx +++ b/components/pages/work-flow/H5MediaViewer.tsx @@ -18,6 +18,8 @@ interface H5MediaViewerProps { scriptData: any; /** 当前索引(视频/分镜阶段用于定位) */ currentSketchIndex: number; + /** 选中视图:final 或 video,用于覆盖阶段渲染 */ + selectedView?: 'final' | 'video' | null; /** 渲染模式(仅剧本阶段透传) */ mode: string; /** 以下为剧本阶段透传必需项(与桌面版保持一致) */ @@ -39,6 +41,8 @@ interface H5MediaViewerProps { isSmartChatBoxOpen?: boolean; /** 失败重试生成视频 */ onRetryVideo?: (video_id: string) => void; + /** 切换选择视图(final 或 video) */ + onSelectView?: (view: 'final' | 'video') => void; } /** @@ -50,6 +54,7 @@ export function H5MediaViewer({ taskObject, scriptData, currentSketchIndex, + selectedView, mode, setIsPauseWorkFlow, setAnyAttribute, @@ -61,7 +66,8 @@ export function H5MediaViewer({ showGotoCutButton, onGotoCut, isSmartChatBoxOpen, - onRetryVideo + onRetryVideo, + onSelectView }: H5MediaViewerProps) { const carouselRef = useRef(null); const videoRefs = useRef>([]); @@ -69,9 +75,12 @@ export function H5MediaViewer({ const [activeIndex, setActiveIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [isCatalogOpen, setIsCatalogOpen] = useState(false); + const [isFinalBarOpen, setIsFinalBarOpen] = useState(true); // 计算当前阶段类型 - const stage = taskObject.currentStage; + const stage = (selectedView === 'final' && taskObject.final?.url) + ? 'final_video' + : (selectedView === 'video' ? 'video' : taskObject.currentStage); // 生成各阶段对应的 slides 数据 const videoUrls = useMemo(() => { @@ -385,6 +394,27 @@ export function H5MediaViewer({ // 其他阶段:使用 Carousel return (
+ {/* 左侧最终视频缩略图栏(H5) */} + {taskObject?.final?.url && ( +
+
+ {isFinalBarOpen && ( +
+ +
+ )} +
+
+ )} {stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()} {stage === 'video' && videoUrls.length > 0 && renderVideoSlides()} {(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()} diff --git a/components/pages/work-flow/H5TaskInfo.tsx b/components/pages/work-flow/H5TaskInfo.tsx index 541731f..db419b6 100644 --- a/components/pages/work-flow/H5TaskInfo.tsx +++ b/components/pages/work-flow/H5TaskInfo.tsx @@ -14,6 +14,8 @@ interface H5TaskInfoProps { taskObject: TaskObject className?: string currentLoadingText: string + /** 选中视图:final 或 video */ + selectedView?: 'final' | 'video' | null } const H5TaskInfo: React.FC = ({ @@ -21,7 +23,8 @@ const H5TaskInfo: React.FC = ({ current, taskObject, className, - currentLoadingText + currentLoadingText, + selectedView }) => { type StageIndex = 0 | 1 | 2 | 3 @@ -72,6 +75,25 @@ const H5TaskInfo: React.FC = ({ return 0 }, [taskObject, current, total]) + // 构造副标题文本:优先根据 selectedView 覆盖 + const subtitle = useMemo(() => { + if (selectedView === 'final' && taskObject.final?.url) { + return 'Final 1/1' + } + if (selectedView === 'video') { + const videosTotal = taskObject.videos?.total_count || taskObject.videos?.data?.length || 0 + return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}` + } + // 回退到原有逻辑 + if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') { + return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(total, 1)}` + } + if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') { + return `Roles & Scenes ${Math.max(displayCurrent, 1)}/${Math.max(total, 1)}` + } + return null + }, [selectedView, taskObject, displayCurrent, total]) + return (
= ({ > {title || '...'} - {shouldShowCount && ( + {shouldShowCount && subtitle && ( - {taskObject.currentStage === 'video' ? 'Shots' : 'Roles & Scenes '} {displayCurrent}/{Math.max(total, 1)} + {subtitle} )} diff --git a/components/pages/work-flow/media-viewer.tsx b/components/pages/work-flow/media-viewer.tsx index d07d30b..4620d27 100644 --- a/components/pages/work-flow/media-viewer.tsx +++ b/components/pages/work-flow/media-viewer.tsx @@ -21,6 +21,7 @@ interface MediaViewerProps { scriptData: any; currentSketchIndex: number; isVideoPlaying: boolean; + selectedView?: 'final' | 'video' | null; onEditModalOpen: (tab: string) => void; onToggleVideoPlay: () => void; setIsPauseWorkFlow: (isPause: boolean) => void; @@ -44,6 +45,7 @@ export const MediaViewer = React.memo(function MediaViewer({ scriptData, currentSketchIndex, isVideoPlaying, + selectedView, onEditModalOpen, onToggleVideoPlay, setIsPauseWorkFlow, @@ -734,20 +736,25 @@ export const MediaViewer = React.memo(function MediaViewer({ ); }; + // 计算生效阶段:selectedView 优先于 taskObject.currentStage + const effectiveStage = (selectedView === 'final' && taskObject.final?.url) + ? 'final_video' + : (selectedView === 'video' ? 'video' : taskObject.currentStage); + // 根据当前步骤渲染对应内容 - if (taskObject.currentStage === 'final_video') { + if (effectiveStage === 'final_video') { return renderFinalVideo(); } - if (taskObject.currentStage === 'video') { + if (effectiveStage === 'video') { return renderVideoContent(onGotoCut); } - if (taskObject.currentStage === 'script') { + if (effectiveStage === 'script') { return renderScriptContent(); } - if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') { + if (effectiveStage === 'scene' || effectiveStage === 'character') { return renderSketchContent([...taskObject.roles.data, ...taskObject.scenes.data][currentSketchIndex]); } diff --git a/components/pages/work-flow/thumbnail-grid.tsx b/components/pages/work-flow/thumbnail-grid.tsx index 8ef9e0e..3952f3f 100644 --- a/components/pages/work-flow/thumbnail-grid.tsx +++ b/components/pages/work-flow/thumbnail-grid.tsx @@ -15,6 +15,7 @@ interface ThumbnailGridProps { onSketchSelect: (index: number) => void; onRetryVideo: (video_id: string) => void; className: string; + selectedView?: 'final' | 'video' | null; } /** @@ -26,7 +27,8 @@ export function ThumbnailGrid({ currentSketchIndex, onSketchSelect, onRetryVideo, - className + className, + selectedView }: ThumbnailGridProps) { const [hoveredIndex, setHoveredIndex] = useState(null); @@ -185,7 +187,7 @@ export function ThumbnailGrid({
!isDragging && !disabled && onSketchSelect(index)} > @@ -294,6 +296,7 @@ export function ThumbnailGrid({ className="w-full h-full object-cover select-none" src={sketch.url} draggable="false" + alt={sketch.type ? String(sketch.type) : 'sketch'} />
)} @@ -343,9 +346,8 @@ export function ThumbnailGrid({ onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} > - {taskObject.currentStage === 'video' && renderVideoThumbnails()} + {(taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') && renderVideoThumbnails()} {(taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') && renderSketchThumbnails(getCurrentData())} - {taskObject.currentStage === 'final_video' && renderVideoThumbnails(true)}
); }