diff --git a/components/pages/style/work-flow.css b/components/pages/style/work-flow.css
index e551469..fe016f9 100644
--- a/components/pages/style/work-flow.css
+++ b/components/pages/style/work-flow.css
@@ -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;
diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx
index 6fd310c..f417b2f 100644
--- a/components/pages/work-flow.tsx
+++ b/components/pages/work-flow.tsx
@@ -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 (
-
+
-
-
+ setIsSmartChatBoxOpen(true)}
+ setVideoPreview={(url, id) => {
+ setPreviewVideoUrl(url);
+ setPreviewVideoId(id);
+ }}
+ showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
+ onGotoCut={generateEditPlan}
+ isSmartChatBoxOpen={isSmartChatBoxOpen}
+ onRetryVideo={(video_id) => handleRetryVideo(video_id)}
+ />
+
+ ) : (
+
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}
/>
-
+ )}
{taskObject.currentStage !== 'script' && (
diff --git a/components/pages/work-flow/H5MediaViewer.tsx b/components/pages/work-flow/H5MediaViewer.tsx
new file mode 100644
index 0000000..e96be7c
--- /dev/null
+++ b/components/pages/work-flow/H5MediaViewer.tsx
@@ -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
(null);
+ const videoRefs = useRef>([]);
+ const rootRef = useRef(null);
+ const [activeIndex, setActiveIndex] = useState(0);
+ const [isPlaying, setIsPlaying] = useState(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;
+ 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;
+ 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 = () => (
+
+
+ {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) => (
+
+

+
+ ))}
+
+
+ );
+
+ // 剧本阶段:不使用 Carousel,沿用 ScriptRenderer
+ if (stage === 'script') {
+ return (
+
+ {scriptData ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ // 其他阶段:使用 Carousel
+ return (
+
+ {stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
+ {stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
+ {(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}
+
+
+ );
+}
+
+export default H5MediaViewer;
+
+
diff --git a/components/pages/work-flow/H5TaskInfo.tsx b/components/pages/work-flow/H5TaskInfo.tsx
new file mode 100644
index 0000000..d04e6dd
--- /dev/null
+++ b/components/pages/work-flow/H5TaskInfo.tsx
@@ -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 = ({
+ 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 (
+
+
+
+
+ {title || '...'}
+
+ {shouldShowCount && (
+
+ 分镜 {displayCurrent}/{Math.max(total, 1)}
+
+ )}
+
+
+
+ {taskObject.currentStage === 'final_video' && (
+
+
+
+
+ )}
+
+ {taskObject.currentStage === 'video' && (
+
+
+
+
+ {showRetry && (
+
+ )}
+
+ )}
+
+
+
+ )
+}
+
+export default H5TaskInfo
+
+