forked from 77media/video-flow
最终视频与分镜列表切换
This commit is contained in:
parent
5b35e274b9
commit
3cf9915877
@ -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<string | null>(null);
|
||||
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(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<any>(null);
|
||||
// const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(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 ? (
|
||||
<div className={`relative heroVideo-FIzuK1 ${['script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
|
||||
{/* 左侧最终视频缩略图栏(仅桌面) */}
|
||||
{taskObject?.final?.url && (
|
||||
<div
|
||||
className="absolute -left-36 top-0 z-[50]"
|
||||
data-alt="final-sidebar"
|
||||
>
|
||||
<div className={`flex items-start`}>
|
||||
{isFinalBarOpen && (
|
||||
<div
|
||||
className="w-28 max-h-[60vh] overflow-y-auto rounded-lg backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl p-2 mr-2"
|
||||
data-alt="final-thumbnails"
|
||||
>
|
||||
{/* 预留历史列表,目前仅展示当前最终视频 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedView('final')}
|
||||
className={`block w-full overflow-hidden rounded-md border ${selectedView === 'final' ? 'border-blue-500' : 'border-white/20'}`}
|
||||
data-alt="final-thumb-item"
|
||||
aria-label="Select final video"
|
||||
>
|
||||
<img
|
||||
src={getFirstFrame(taskObject.final.url)}
|
||||
alt="final"
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
<div className="text-xs text-white/80 text-center py-1">Final</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
isVideoPlaying={isVideoPlaying}
|
||||
selectedView={selectedView}
|
||||
onEditModalOpen={handleEditModalOpen}
|
||||
onToggleVideoPlay={toggleVideoPlay}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
@ -459,6 +503,7 @@ Please process this video editing request.`;
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
selectedView={selectedView}
|
||||
mode={mode}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
@ -474,6 +519,7 @@ Please process this video editing request.`;
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => 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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -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<CarouselRef>(null);
|
||||
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);
|
||||
@ -69,9 +75,12 @@ export function H5MediaViewer({
|
||||
const [activeIndex, setActiveIndex] = useState<number>(0);
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false);
|
||||
const [isFinalBarOpen, setIsFinalBarOpen] = useState<boolean>(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 (
|
||||
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
|
||||
{/* 左侧最终视频缩略图栏(H5) */}
|
||||
{taskObject?.final?.url && (
|
||||
<div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5">
|
||||
<div className="flex items-start">
|
||||
{isFinalBarOpen && (
|
||||
<div className="w-20 max-h-[50vh] overflow-y-auto rounded-md backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl p-1 mr-2" data-alt="final-thumbnails">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectView && onSelectView('final')}
|
||||
className={`block w-full overflow-hidden rounded border ${selectedView === 'final' ? 'border-blue-500' : 'border-white/20'}`}
|
||||
data-alt="final-thumb-item"
|
||||
aria-label="Select final video"
|
||||
>
|
||||
<img src={getFirstFrame(taskObject.final.url)} alt="final" className="w-full h-auto object-cover" />
|
||||
<div className="text-[10px] text-white/80 text-center py-0.5">Final</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||
{(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}
|
||||
|
||||
@ -14,6 +14,8 @@ interface H5TaskInfoProps {
|
||||
taskObject: TaskObject
|
||||
className?: string
|
||||
currentLoadingText: string
|
||||
/** 选中视图:final 或 video */
|
||||
selectedView?: 'final' | 'video' | null
|
||||
}
|
||||
|
||||
const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
||||
@ -21,7 +23,8 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
||||
current,
|
||||
taskObject,
|
||||
className,
|
||||
currentLoadingText
|
||||
currentLoadingText,
|
||||
selectedView
|
||||
}) => {
|
||||
type StageIndex = 0 | 1 | 2 | 3
|
||||
|
||||
@ -72,6 +75,25 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
||||
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 (
|
||||
<div
|
||||
data-alt="h5-header"
|
||||
@ -89,9 +111,9 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
||||
>
|
||||
{title || '...'}
|
||||
</h1>
|
||||
{shouldShowCount && (
|
||||
{shouldShowCount && subtitle && (
|
||||
<span data-alt="shot-count" className="flex items-center gap-4 text-sm text-slate-300">
|
||||
{taskObject.currentStage === 'video' ? 'Shots' : 'Roles & Scenes '} {displayCurrent}/{Math.max(total, 1)}
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@ -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<number | null>(null);
|
||||
|
||||
@ -185,7 +187,7 @@ export function ThumbnailGrid({
|
||||
<div
|
||||
key={`video-${urls}-${index}`}
|
||||
className={`relative aspect-video rounded-lg overflow-hidden
|
||||
${(currentSketchIndex === index && !disabled) ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||||
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||||
onClick={() => !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'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -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)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user