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) => ( +
+ scene +
+ ))} +
+
+ ); + + // 剧本阶段:不使用 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 + +