forked from 77media/video-flow
H5 workflwo 页面暂存
This commit is contained in:
parent
5b7dc2fe47
commit
5efe68b63b
@ -13,7 +13,7 @@
|
||||
}
|
||||
|
||||
/* 大屏幕适配 */
|
||||
@media (width >= 1024px) {
|
||||
@media (width >=1024px) {
|
||||
.container-H2sRZG {
|
||||
width: 85%;
|
||||
}
|
||||
@ -28,19 +28,19 @@
|
||||
height: calc(100vh - 80px);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
|
||||
.splashContainer-otuV_A {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
.media-Ocdu1O {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
.videoContainer-qteKNi {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
|
||||
.imageGrid-ymZV9z {
|
||||
flex-shrink: 0;
|
||||
height: 110px;
|
||||
@ -48,17 +48,17 @@
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
|
||||
.title-JtMejk {
|
||||
font-size: 1.1rem;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
|
||||
.subtitle-had8uE {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.info-UUGkPJ {
|
||||
gap: 4px;
|
||||
}
|
||||
@ -71,34 +71,35 @@
|
||||
height: calc(100vh - 60px);
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
|
||||
.splashContainer-otuV_A {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
|
||||
.videoContainer-qteKNi {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
|
||||
.title-JtMejk {
|
||||
font-size: 1rem;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
|
||||
.subtitle-had8uE {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
|
||||
.imageGrid-ymZV9z {
|
||||
height: 90px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
||||
.info-UUGkPJ {
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 默认小屏幕布局 */
|
||||
.splashContainer-otuV_A {
|
||||
box-sizing: border-box;
|
||||
@ -110,13 +111,13 @@
|
||||
}
|
||||
|
||||
/* 大屏幕布局 */
|
||||
@media (width >= 1024px) {
|
||||
@media (width >=1024px) {
|
||||
.splashContainer-otuV_A {
|
||||
box-sizing: border-box;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
.content-vPGYx8 {
|
||||
@ -128,8 +129,7 @@
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
display: flex;
|
||||
;
|
||||
display: flex;;
|
||||
}
|
||||
.title-JtMejk {
|
||||
box-sizing: border-box;
|
||||
@ -152,8 +152,10 @@
|
||||
}
|
||||
|
||||
.media-Ocdu1O {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@ -163,6 +165,7 @@
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.videoContainer-qteKNi {
|
||||
flex: 1;
|
||||
@ -192,14 +195,15 @@
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
display: flex
|
||||
;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.secondary-_HxO1W {
|
||||
color: #fff;
|
||||
background: #1d1e23;
|
||||
}
|
||||
|
||||
.large-_aHMgD {
|
||||
letter-spacing: .01em;
|
||||
border-radius: 8px;
|
||||
@ -209,6 +213,7 @@
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.videoPlaybackButton-uFNO1b {
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
@ -220,6 +225,7 @@
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.imageGrid-ymZV9z {
|
||||
flex-shrink: 0;
|
||||
height: 140px;
|
||||
@ -228,17 +234,18 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (height >= 880px) {
|
||||
@media (height >=880px) {
|
||||
.imageGrid-ymZV9z {
|
||||
/* flex: 1; */
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(25%, 1fr);
|
||||
overflow-x: unset;
|
||||
/* flex: 1; */
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(25%, 1fr);
|
||||
overflow-x: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.image-x5Y2Sg {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
|
||||
@ -4,6 +4,8 @@ import "./style/work-flow.css";
|
||||
|
||||
import { EditModal } from "@/components/ui/edit-modal";
|
||||
import { TaskInfo } from "./work-flow/task-info";
|
||||
import H5TaskInfo from "./work-flow/H5TaskInfo";
|
||||
import H5MediaViewer from "./work-flow/H5MediaViewer";
|
||||
import { MediaViewer } from "./work-flow/media-viewer";
|
||||
import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
|
||||
import { useWorkflowData } from "./work-flow/use-workflow-data";
|
||||
@ -19,8 +21,11 @@ import { showEditingNotification } from "@/components/pages/work-flow/editing-no
|
||||
import { exportVideoWithRetry } from '@/utils/export-service';
|
||||
// 临时禁用视频编辑功能
|
||||
// import { EditPoint as EditPointType } from './work-flow/video-edit/types';
|
||||
import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
|
||||
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||
|
||||
const WorkFlow = React.memo(function WorkFlow() {
|
||||
const { isMobile, isTablet, isDesktop } = useDeviceType();
|
||||
useEffect(() => {
|
||||
console.log("init-WorkFlow");
|
||||
return () => {
|
||||
@ -377,12 +382,19 @@ Please process this video editing request.`;
|
||||
*/
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-hidden h-full px-[1rem] pb-[1rem]">
|
||||
<div className={`w-full overflow-hidden h-full ${isDesktop ? 'px-[1rem] pb-[1rem]' : ''}`}>
|
||||
<div className="w-full h-full">
|
||||
<div className="splashContainer-otuV_A">
|
||||
<div className="content-vPGYx8">
|
||||
<div className="info-UUGkPJ">
|
||||
<TaskInfo
|
||||
{isMobile || isTablet ? (
|
||||
<H5TaskInfo
|
||||
title={taskObject.title}
|
||||
current={currentSketchIndex + 1}
|
||||
taskObject={taskObject}
|
||||
/>
|
||||
) : (
|
||||
<TaskInfo
|
||||
taskObject={taskObject}
|
||||
currentLoadingText={currentLoadingText}
|
||||
roles={taskObject.roles.data}
|
||||
@ -390,27 +402,50 @@ Please process this video editing request.`;
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
onGotoCut={generateEditPlan}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
/>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="media-Ocdu1O rounded-lg">
|
||||
<div
|
||||
className="videoContainer-qteKNi"
|
||||
className={`videoContainer-qteKNi ${!isDesktop ? '!w-full' : ''}`}
|
||||
ref={containerRef}
|
||||
>
|
||||
<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}>
|
||||
<MediaViewer
|
||||
{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}>
|
||||
<MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
isVideoPlaying={isVideoPlaying}
|
||||
onEditModalOpen={handleEditModalOpen}
|
||||
onToggleVideoPlay={toggleVideoPlay}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
||||
setVideoPreview={(url, id) => {
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<H5MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
isVideoPlaying={isVideoPlaying}
|
||||
onEditModalOpen={handleEditModalOpen}
|
||||
onToggleVideoPlay={toggleVideoPlay}
|
||||
mode={mode}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
||||
setVideoPreview={(url, id) => {
|
||||
setPreviewVideoUrl(url);
|
||||
@ -422,7 +457,7 @@ Please process this video editing request.`;
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
// 临时禁用视频编辑功能: enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{taskObject.currentStage !== 'script' && (
|
||||
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
|
||||
|
||||
312
components/pages/work-flow/H5MediaViewer.tsx
Normal file
312
components/pages/work-flow/H5MediaViewer.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Carousel } from 'antd';
|
||||
import type { CarouselRef } from 'antd/es/carousel';
|
||||
import { Play, Scissors, MessageCircleMore, RotateCcw } from 'lucide-react';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||||
import ScriptLoading from './script-loading';
|
||||
import { getFirstFrame } from '@/utils/tools';
|
||||
|
||||
interface H5MediaViewerProps {
|
||||
/** 任务对象,包含各阶段数据 */
|
||||
taskObject: TaskObject;
|
||||
/** 剧本数据(仅剧本阶段使用) */
|
||||
scriptData: any;
|
||||
/** 当前索引(视频/分镜阶段用于定位) */
|
||||
currentSketchIndex: number;
|
||||
/** 渲染模式(仅剧本阶段透传) */
|
||||
mode: string;
|
||||
/** 以下为剧本阶段透传必需项(与桌面版保持一致) */
|
||||
setIsPauseWorkFlow: (isPause: boolean) => void;
|
||||
setAnyAttribute: any;
|
||||
isPauseWorkFlow: boolean;
|
||||
applyScript: any;
|
||||
/** 打开智能对话 */
|
||||
onOpenChat?: () => void;
|
||||
/** 设置聊天预览视频 */
|
||||
setVideoPreview?: (url: string, id: string) => void;
|
||||
/** 显示跳转至剪辑平台按钮 */
|
||||
showGotoCutButton?: boolean;
|
||||
/** 跳转至剪辑平台 */
|
||||
onGotoCut?: () => void;
|
||||
/** 智能对话是否打开(H5可忽略布局调整,仅占位) */
|
||||
isSmartChatBoxOpen?: boolean;
|
||||
/** 失败重试生成视频 */
|
||||
onRetryVideo?: (video_id: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 面向 H5 的媒体预览组件。
|
||||
* - 除剧本阶段外,统一使用 antd Carousel 展示 图片/视频。
|
||||
* - 视频仅保留中间的大号播放/暂停按钮。
|
||||
*/
|
||||
export function H5MediaViewer({
|
||||
taskObject,
|
||||
scriptData,
|
||||
currentSketchIndex,
|
||||
mode,
|
||||
setIsPauseWorkFlow,
|
||||
setAnyAttribute,
|
||||
isPauseWorkFlow,
|
||||
applyScript,
|
||||
onOpenChat,
|
||||
setVideoPreview,
|
||||
showGotoCutButton,
|
||||
onGotoCut,
|
||||
isSmartChatBoxOpen,
|
||||
onRetryVideo
|
||||
}: H5MediaViewerProps) {
|
||||
const carouselRef = useRef<CarouselRef>(null);
|
||||
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
const [activeIndex, setActiveIndex] = useState<number>(0);
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
|
||||
// 计算当前阶段类型
|
||||
const stage = 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<any>;
|
||||
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<any>;
|
||||
const scenes = (taskObject.scenes?.data ?? []) as Array<any>;
|
||||
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());
|
||||
};
|
||||
|
||||
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 = () => (
|
||||
<div data-alt="carousel-wrapper" className="relative w-full aspect-video min-h-[200px] overflow-hidden rounded-lg">
|
||||
<Carousel
|
||||
ref={carouselRef}
|
||||
key={`h5-carousel-video-${stage}-${videoUrls.length}`}
|
||||
arrows
|
||||
dots={false}
|
||||
infinite={false}
|
||||
afterChange={handleAfterChange}
|
||||
className="absolute inset-0"
|
||||
slidesToShow={1}
|
||||
adaptiveHeight
|
||||
>
|
||||
{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 (
|
||||
<div key={`h5-video-${idx}`} data-alt="video-slide" className="relative w-full h-full">
|
||||
{hasUrl ? (
|
||||
<>
|
||||
<video
|
||||
ref={(el) => (videoRefs.current[idx] = el)}
|
||||
className="w-full h-full object-cover [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black"
|
||||
src={url}
|
||||
preload="metadata"
|
||||
playsInline
|
||||
controls={isPlaying && activeIndex === idx}
|
||||
loop
|
||||
poster={getFirstFrame(url)}
|
||||
crossOrigin="anonymous"
|
||||
onLoadedMetadata={() => {}}
|
||||
onPlay={() => {
|
||||
if (activeIndex === idx) setIsPlaying(true);
|
||||
}}
|
||||
onPause={() => {
|
||||
if (activeIndex === idx) setIsPlaying(false);
|
||||
}}
|
||||
onCanPlay={() => {}}
|
||||
onError={() => {}}
|
||||
/>
|
||||
{activeIndex === idx && !isPlaying && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePlay}
|
||||
data-alt="play-toggle"
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 rounded-full bg-black/60 text-white flex items-center justify-center active:scale-95"
|
||||
aria-label={'Play'}
|
||||
>
|
||||
<Play className="w-8 h-8" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 顶部操作按钮 */}
|
||||
<div data-alt="video-actions" className="absolute top-2 right-2 z-10 flex items-center gap-2">
|
||||
{showGotoCutButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onGotoCut}
|
||||
data-alt="goto-cut-button"
|
||||
className="w-9 h-9 rounded-full bg-black/50 text-white flex items-center justify-center active:scale-95"
|
||||
aria-label={'Go to editing platform'}
|
||||
>
|
||||
<Scissors className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
{!!setVideoPreview && !!onOpenChat && hasUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setVideoPreview(url, videoId || '');
|
||||
onOpenChat();
|
||||
}}
|
||||
data-alt="open-chat-button"
|
||||
className="w-9 h-9 rounded-full bg-black/50 text-white flex items-center justify-center active:scale-95"
|
||||
aria-label={'Open chat for editing'}
|
||||
>
|
||||
<MessageCircleMore className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full aspect-video min-h-[200px] flex items-center justify-center bg-black/10" data-alt="video-status">
|
||||
{status === 0 && (
|
||||
<span className="text-blue-500 text-base">Generating...</span>
|
||||
)}
|
||||
{status === 2 && (
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<div className="text-4xl">⚠️</div>
|
||||
<span className="text-red-500 text-base">Generate failed</span>
|
||||
{onRetryVideo && videoId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRetryVideo(videoId)}
|
||||
data-alt="retry-button"
|
||||
className="px-3 py-1.5 rounded-full bg-black/60 text-white text-sm inline-flex items-center gap-1 active:scale-95"
|
||||
aria-label={'Retry'}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Retry
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{status !== 0 && status !== 2 && (
|
||||
<span className="text-white/70 text-base">Pending</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染图片 slide
|
||||
const renderImageSlides = () => (
|
||||
<div data-alt="carousel-wrapper" className="relative w-full aspect-video min-h-[200px] overflow-hidden rounded-lg">
|
||||
<Carousel
|
||||
ref={carouselRef}
|
||||
key={`h5-carousel-image-${stage}-${imageUrls.length}`}
|
||||
arrows
|
||||
dots={false}
|
||||
infinite={false}
|
||||
afterChange={handleAfterChange}
|
||||
className="absolute inset-0"
|
||||
slidesToShow={1}
|
||||
adaptiveHeight
|
||||
>
|
||||
{imageUrls.map((url, idx) => (
|
||||
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full">
|
||||
<img src={url} alt="scene" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 剧本阶段:不使用 Carousel,沿用 ScriptRenderer
|
||||
if (stage === 'script') {
|
||||
return (
|
||||
<div data-alt="script-content" className="w-full h-full">
|
||||
{scriptData ? (
|
||||
<ScriptRenderer
|
||||
data={scriptData}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
/>
|
||||
) : (
|
||||
<ScriptLoading isCompleted={!!scriptData} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 其他阶段:使用 Carousel
|
||||
return (
|
||||
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
|
||||
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||
{(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}
|
||||
<style jsx global>{`
|
||||
[data-alt='carousel-wrapper'] .slick-slide { display: block; }
|
||||
.slick-list { width: 100%;height: 100%; }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default H5MediaViewer;
|
||||
|
||||
|
||||
158
components/pages/work-flow/H5TaskInfo.tsx
Normal file
158
components/pages/work-flow/H5TaskInfo.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
'use client'
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import { TaskObject } from '@/api/DTO/movieEdit'
|
||||
import { GlassIconButton } from '@/components/ui/glass-icon-button'
|
||||
import { Pencil, RotateCcw, Download, ArrowDownWideNarrow, Scissors, Maximize, Minimize, MessageCircleMore } from 'lucide-react'
|
||||
|
||||
interface H5TaskInfoProps {
|
||||
/** 标题文案 */
|
||||
title: string
|
||||
/** 当前分镜序号(从1开始) */
|
||||
current: number
|
||||
/** 任务对象(用于读取总数等信息) */
|
||||
taskObject: TaskObject
|
||||
/** video:聊天编辑 */
|
||||
onEditWithChat?: () => void
|
||||
/** 下载:全部分镜 */
|
||||
onDownloadAll?: () => void
|
||||
/** 下载:最终成片 */
|
||||
onDownloadFinal?: () => void
|
||||
/** 下载:当前分镜视频 */
|
||||
onDownloadCurrent?: () => void
|
||||
/** video:失败时显示重试 */
|
||||
showRetry?: boolean
|
||||
onRetry?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
||||
title,
|
||||
current,
|
||||
taskObject,
|
||||
onEditWithChat,
|
||||
onDownloadAll,
|
||||
onDownloadFinal,
|
||||
onDownloadCurrent,
|
||||
showRetry,
|
||||
onRetry,
|
||||
className
|
||||
}) => {
|
||||
const total = useMemo(() => {
|
||||
if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') {
|
||||
return taskObject.videos?.total_count || taskObject.videos?.data?.length || 0
|
||||
}
|
||||
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
|
||||
const rolesTotal = taskObject.roles?.total_count || taskObject.roles?.data?.length || 0
|
||||
const scenesTotal = taskObject.scenes?.total_count || taskObject.scenes?.data?.length || 0
|
||||
return rolesTotal + scenesTotal
|
||||
}
|
||||
return 0
|
||||
}, [taskObject])
|
||||
|
||||
const shouldShowCount = taskObject.currentStage !== 'script'
|
||||
|
||||
const displayCurrent = useMemo(() => {
|
||||
if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') {
|
||||
return Math.max(current, 1)
|
||||
}
|
||||
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
|
||||
const bounded = Math.min(Math.max(current, 1), Math.max(total, 1))
|
||||
return bounded
|
||||
}
|
||||
return 0
|
||||
}, [taskObject, current, total])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-alt="h5-header"
|
||||
className={`absolute top-0 left-0 right-0 z-[50] pr-2 ${className || ''}`}
|
||||
>
|
||||
<div
|
||||
data-alt="h5-header-bar"
|
||||
className="flex items-start justify-between"
|
||||
>
|
||||
<div data-alt="title-area" className="flex flex-col min-w-0 bg-gradient-to-b from-slate-900/80 via-slate-900/40 to-transparent backdrop-blur-sm rounded-lg p-4">
|
||||
<h1
|
||||
data-alt="title"
|
||||
className="text-white text-lg font-bold"
|
||||
title={title}
|
||||
>
|
||||
{title || '...'}
|
||||
</h1>
|
||||
{shouldShowCount && (
|
||||
<span data-alt="shot-count" className="flex items-center gap-4 text-sm text-slate-300">
|
||||
分镜 {displayCurrent}/{Math.max(total, 1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div data-alt="actions" className="flex flex-col items-center gap-2">
|
||||
{taskObject.currentStage === 'final_video' && (
|
||||
<div data-alt="final-video-actions" className="flex flex-col items-center gap-2">
|
||||
<GlassIconButton
|
||||
data-alt="download-all-button"
|
||||
className="w-10 h-10 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl border border-purple-400/30 rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
|
||||
icon={ArrowDownWideNarrow}
|
||||
size="sm"
|
||||
aria-label="download-all"
|
||||
onClick={onDownloadAll}
|
||||
/>
|
||||
<GlassIconButton
|
||||
data-alt="download-final-button"
|
||||
className="w-10 h-10 bg-gradient-to-br from-amber-500/80 to-yellow-600/80 backdrop-blur-xl border border-amber-400/30 rounded-full flex items-center justify-center hover:from-amber-400/80 hover:to-yellow-500/80 transition-all"
|
||||
icon={Download}
|
||||
size="sm"
|
||||
aria-label="download-final"
|
||||
onClick={onDownloadFinal}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{taskObject.currentStage === 'video' && (
|
||||
<div data-alt="video-actions" className="flex flex-col items-center gap-2">
|
||||
<GlassIconButton
|
||||
data-alt="edit-with-chat-button"
|
||||
className="w-10 h-10 bg-gradient-to-br from-blue-500/80 to-blue-600/80 backdrop-blur-xl border border-blue-400/30 rounded-full flex items-center justify-center hover:from-blue-400/80 hover:to-blue-500/80 transition-all"
|
||||
icon={MessageCircleMore}
|
||||
size="sm"
|
||||
aria-label="edit-with-chat"
|
||||
onClick={onEditWithChat}
|
||||
/>
|
||||
<GlassIconButton
|
||||
data-alt="download-all-button"
|
||||
className="w-10 h-10 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl border border-purple-400/30 rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
|
||||
icon={ArrowDownWideNarrow}
|
||||
size="sm"
|
||||
aria-label="download-all"
|
||||
onClick={onDownloadAll}
|
||||
/>
|
||||
<GlassIconButton
|
||||
data-alt="download-current-button"
|
||||
className="w-10 h-10 bg-gradient-to-br from-amber-500/80 to-yellow-600/80 backdrop-blur-xl border border-amber-400/30 rounded-full flex items-center justify-center hover:from-amber-400/80 hover:to-yellow-500/80 transition-all"
|
||||
icon={Download}
|
||||
size="sm"
|
||||
aria-label="download-current"
|
||||
onClick={onDownloadCurrent}
|
||||
/>
|
||||
{showRetry && (
|
||||
<GlassIconButton
|
||||
data-alt="retry-button"
|
||||
className="w-10 h-10 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl border border-purple-400/30 rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
|
||||
icon={RotateCcw}
|
||||
size="sm"
|
||||
aria-label="retry"
|
||||
onClick={onRetry}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default H5TaskInfo
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user