'use client'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Carousel } from 'antd'; import type { CarouselRef } from 'antd/es/carousel'; import { Play, Pause, FeatherIcon, MessageCircleMore, Download, ArrowDownWideNarrow, RotateCcw, Navigation } from 'lucide-react'; import { TaskObject } from '@/api/DTO/movieEdit'; import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer'; import ScriptLoading from './script-loading'; import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools'; import { Drawer } from 'antd'; interface H5MediaViewerProps { /** 任务对象,包含各阶段数据 */ taskObject: TaskObject; /** 剧本数据(仅剧本阶段使用) */ scriptData: any; /** 当前索引(视频/分镜阶段用于定位) */ currentSketchIndex: number; /** 选中视图:final 或 video,用于覆盖阶段渲染 */ selectedView?: 'final' | 'video' | null; /** 渲染模式(仅剧本阶段透传) */ mode: string; /** 以下为剧本阶段透传必需项(与桌面版保持一致) */ setIsPauseWorkFlow: (isPause: boolean) => void; setAnyAttribute: any; isPauseWorkFlow: boolean; applyScript: any; /** Carousel 切换时回传新的索引 */ setCurrentSketchIndex: (index: number) => void; /** 打开智能对话 */ onOpenChat?: () => void; /** 设置聊天预览视频 */ setVideoPreview?: (url: string, id: string) => void; /** 显示跳转至剪辑平台按钮 */ showGotoCutButton?: boolean; /** 跳转至剪辑平台 */ onGotoCut?: () => void; /** 智能对话是否打开(H5可忽略布局调整,仅占位) */ isSmartChatBoxOpen?: boolean; /** 失败重试生成视频 */ onRetryVideo?: (video_id: string) => void; /** 切换选择视图(final 或 video) */ onSelectView?: (view: 'final' | 'video') => void; /** 启用视频编辑功能 */ enableVideoEdit?: boolean; /** 视频编辑描述提交回调 */ onVideoEditDescriptionSubmit?: (editPoint: any, description: string) => void; /** 项目ID */ projectId?: string; } /** * 面向 H5 的媒体预览组件。 * - 除剧本阶段外,统一使用 antd Carousel 展示 图片/视频。 * - 视频仅保留中间的大号播放/暂停按钮。 */ export function H5MediaViewer({ taskObject, scriptData, currentSketchIndex, selectedView, mode, setIsPauseWorkFlow, setAnyAttribute, isPauseWorkFlow, applyScript, setCurrentSketchIndex, onOpenChat, setVideoPreview, showGotoCutButton, onGotoCut, isSmartChatBoxOpen, onRetryVideo, onSelectView, enableVideoEdit, onVideoEditDescriptionSubmit, projectId }: H5MediaViewerProps) { const carouselRef = useRef(null); const videoRefs = useRef>([]); const rootRef = useRef(null); const [activeIndex, setActiveIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [isCatalogOpen, setIsCatalogOpen] = useState(false); // 计算当前阶段类型 const stage = (selectedView === 'final' && taskObject.final?.url) ? 'final_video' : (!['script', 'character', 'scene'].includes(taskObject.currentStage) ? 'video' : taskObject.currentStage); // 生成各阶段对应的 slides 数据 const videoUrls = useMemo(() => { if (stage === 'final_video') { return taskObject.final?.url ? [taskObject.final.url] : []; } if (stage === 'video') { // 注意:不再过滤,保持与原始数组长度一致,避免索引错位 const list = (taskObject.videos?.data ?? []) as Array; return list.map(v => (Array.isArray(v?.urls) && v.urls.length > 0 ? v.urls[0] : '')) as string[]; } return []; }, [stage, taskObject.final?.url, taskObject.videos?.data]); const imageUrls = useMemo(() => { if (stage === 'scene' || stage === 'character') { const roles = (taskObject.roles?.data ?? []) as Array; const scenes = (taskObject.scenes?.data ?? []) as Array; console.log('h5-media-viewer:stage', stage); console.log('h5-media-viewer:roles', roles); console.log('h5-media-viewer:scenes', scenes); return [...roles, ...scenes].map(item => item?.url).filter(Boolean) as string[]; } return []; }, [stage, taskObject.roles?.data, taskObject.scenes?.data]); // 占位,避免未使用警告 useEffect(() => { void isSmartChatBoxOpen; }, [isSmartChatBoxOpen]); // 同步外部索引到 Carousel useEffect(() => { if (stage === 'video' || stage === 'scene' || stage === 'character') { const target = Math.max(0, currentSketchIndex); carouselRef.current?.goTo(target, false); setActiveIndex(target); setIsPlaying(false); // 切换时暂停全部视频 videoRefs.current.forEach(v => v?.pause()); } }, [currentSketchIndex, stage]); // 阶段变更时重置状态 useEffect(() => { setActiveIndex(0); setIsPlaying(false); videoRefs.current.forEach(v => v?.pause()); }, [stage]); const handleAfterChange = (index: number) => { setActiveIndex(index); setIsPlaying(false); videoRefs.current.forEach(v => v?.pause()); // 同步到父级索引 if (stage === 'video' || stage === 'scene' || stage === 'character') { setCurrentSketchIndex(index); } }; const togglePlay = () => { if (stage !== 'final_video' && stage !== 'video') return; const currentVideo = videoRefs.current[activeIndex] ?? null; if (!currentVideo) return; if (currentVideo.paused) { currentVideo.play().then(() => setIsPlaying(true)).catch(() => setIsPlaying(false)); } else { currentVideo.pause(); setIsPlaying(false); } }; // 渲染视频 slide const renderVideoSlides = () => (
{videoUrls.map((url, idx) => { const hasUrl = typeof url === 'string' && url.length > 0; const raw = (taskObject.videos?.data ?? [])[idx] as any; const status = raw?.video_status; const videoId = raw?.video_id as string | undefined; return (
{hasUrl ? ( <>
); })}
); // 渲染图片 slide const renderImageSlides = () => (
{imageUrls.map((url, idx) => (
scene
))}
); // 剧本阶段:不使用 Carousel,沿用 ScriptRenderer if (stage === 'script') { const navItems = Array.isArray(scriptData) ? (scriptData as Array).map(v => ({ id: v?.id, title: v?.title })) : []; const scrollToSection = (id?: string) => { if (!id) return; const el = document.getElementById(`section-${id}`); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; return (
{scriptData ? ( <> setIsCatalogOpen(false)} placement="right" width={'auto'} mask maskClosable={true} maskStyle={{ backgroundColor: 'rgba(0,0,0,0)' }} className="[&_.ant-drawer-content-wrapper]:w-auto [&_.ant-drawer-content-wrapper]:max-w-[80vw] backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl" rootClassName="outline-none" data-alt="catalog-drawer" closable={false} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: 10, borderTopLeftRadius: 10, overflow: 'hidden', }} styles={{ body: { backgroundColor: 'transparent', padding: 0, }, }} >
navigation
{navItems.map(item => (
{ scrollToSection(item.id); setIsCatalogOpen(false); }} onKeyDown={(e) => { if (e.key === 'Enter') { scrollToSection(item.id); setIsCatalogOpen(false); } }} className="px-3 py-2 text-white/90 text-sm cursor-pointer transition-colors" data-alt="catalog-item" > {item.title}
))}
) : ( )}
); } // 其他阶段:使用 Carousel return (
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()} {stage === 'video' && videoUrls.length > 0 && renderVideoSlides()} {(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()} {/* 全局固定操作区(右下角)视频暂停时展示 */} {(stage === 'video' || stage === 'final_video') && !isPlaying && (
{stage === 'video' && ( <> { const current = (taskObject.videos?.data ?? [])[activeIndex] as any; if (current && Array.isArray(current.urls) && current.urls.length > 0 && setVideoPreview) { setVideoPreview(current.urls[0], current.video_id); onOpenChat && onOpenChat(); } }} /> { const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); await downloadAllVideos(all); }} /> {(() => { const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status; return status === 1 ? ( { const current = (taskObject.videos?.data ?? [])[activeIndex] as any; const hasUrl = current && Array.isArray(current.urls) && current.urls.length > 0; if (hasUrl) { await downloadVideo(current.urls[0]); } }} /> ) : null; })()} {(() => { const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status; return status === 2 ? ( { const vid = (taskObject.videos?.data ?? [])[activeIndex]?.video_id; if (vid && onRetryVideo) onRetryVideo(vid); }} /> ) : null; })()} )} {stage === 'final_video' && ( <> { const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); await downloadAllVideos(all); }} /> { const url = videoUrls[0]; if (url) await downloadVideo(url); }} /> )}
)}
); } export default H5MediaViewer;