From f2050530e7a1476e5511cb2abcb266f7cfb4c613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Thu, 9 Oct 2025 18:24:47 +0800 Subject: [PATCH] =?UTF-8?q?H5=E4=B8=8B=E8=BD=BD=E6=8C=89=E9=92=AE=E5=90=88?= =?UTF-8?q?=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/work-flow/H5MediaViewer.tsx | 246 ++++++++++++++---- .../pages/work-flow/use-workflow-data.tsx | 4 +- 2 files changed, 199 insertions(+), 51 deletions(-) diff --git a/components/pages/work-flow/H5MediaViewer.tsx b/components/pages/work-flow/H5MediaViewer.tsx index 737a564..751980e 100644 --- a/components/pages/work-flow/H5MediaViewer.tsx +++ b/components/pages/work-flow/H5MediaViewer.tsx @@ -11,6 +11,7 @@ import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools'; import { Drawer } from 'antd'; import error_image from '@/public/assets/error.webp'; +import { createRoot, Root } from 'react-dom/client'; interface H5MediaViewerProps { /** 任务对象,包含各阶段数据 */ @@ -54,6 +55,142 @@ interface H5MediaViewerProps { aspectRatio?: string; } +interface DownloadOptionsModalProps { + onDownloadCurrent: () => void; + onDownloadAll: () => void; + onClose: () => void; + currentVideoIndex: number; + totalVideos: number; + /** 当前视频是否生成失败 */ + isCurrentVideoFailed: boolean; + /** 是否为最终视频阶段 */ + isFinalStage?: boolean; +} + +function DownloadOptionsModal(props: DownloadOptionsModalProps) { + const { onDownloadCurrent, onDownloadAll, onClose, currentVideoIndex, totalVideos, isCurrentVideoFailed, isFinalStage = false } = props; + const containerRef = useRef(null); + + useEffect(() => { + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, []); + + return ( +
+
e.stopPropagation()} + > +
+
+ +
+

+ Download Options +

+
+ +
+

+ Choose your download preference +

+ + {!isCurrentVideoFailed && ( +
+
Current video
+
{currentVideoIndex + 1} / {totalVideos}
+
+ )} +
+ +
+ {!isCurrentVideoFailed && ( + + )} + + +
+
+
+ ); +} + +/** + * Opens a download options modal with glass morphism style. + * @param {DownloadOptionsModalProps} options - download options and callbacks. + */ +function showDownloadOptionsModal(options: Omit): void { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return; + } + + const mount = document.createElement('div'); + mount.setAttribute('data-alt', 'download-options-modal-root'); + document.body.appendChild(mount); + + let root: Root | null = null; + try { + root = createRoot(mount); + } catch { + if (mount.parentNode) { + mount.parentNode.removeChild(mount); + } + return; + } + + const close = () => { + try { + root?.unmount(); + } finally { + if (mount.parentNode) { + mount.parentNode.removeChild(mount); + } + } + }; + + root.render( + + ); +} + /** * 面向 H5 的媒体预览组件。 * - 除剧本阶段外,统一使用 antd Carousel 展示 图片/视频。 @@ -455,35 +592,40 @@ export function H5MediaViewer({ }} /> { - const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); - await downloadAllVideos(all); + aria-label="download" + onClick={() => { + const current = (taskObject.videos?.data ?? [])[activeIndex] as any; + const status = current?.video_status; + const hasUrl = current && Array.isArray(current.urls) && current.urls.length > 0; + const hasFinalVideo = taskObject.final?.url; + const baseVideoCount = (taskObject.videos?.data ?? []).length; + const totalVideos = hasFinalVideo ? baseVideoCount + 1 : baseVideoCount; + const isCurrentVideoFailed = status === 2; + + showDownloadOptionsModal({ + currentVideoIndex: hasFinalVideo ? activeIndex + 1 : activeIndex, + totalVideos, + isCurrentVideoFailed, + onDownloadCurrent: async () => { + if (hasUrl) { + await downloadVideo(current.urls[0]); + } + }, + onDownloadAll: async () => { + const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); + if (hasFinalVideo) { + all.push(taskObject.final.url); + } + console.log('h5-media-viewer:all', all); + 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 ? ( @@ -503,30 +645,36 @@ export function H5MediaViewer({ )} {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); - }} - /> - + { + const totalVideos = (taskObject.videos?.data ?? []).length + 1; + const finalUrl = videoUrls[0]; + + showDownloadOptionsModal({ + currentVideoIndex: 0, + totalVideos, + isCurrentVideoFailed: false, + isFinalStage: true, + onDownloadCurrent: async () => { + if (finalUrl) { + await downloadVideo(finalUrl); + } + }, + onDownloadAll: async () => { + const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); + if (finalUrl) { + all.push(finalUrl); + } + await downloadAllVideos(all); + }, + }); + }} + /> )} )} diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx index cbfc550..270c36b 100644 --- a/components/pages/work-flow/use-workflow-data.tsx +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -443,7 +443,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa if (task.task_name === 'combiner_videos') { if (task.task_status === 'COMPLETED') { taskCurrent.currentStage = 'final_video'; - taskCurrent.final.url = task.task_result.video; + taskCurrent.final.url = task.task_result.video_url; taskCurrent.final.note = 'combiner'; taskCurrent.status = 'COMPLETED'; } @@ -463,7 +463,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa if (task.task_name === 'watermark_videos') { if (task.task_status === 'COMPLETED') { taskCurrent.currentStage = 'final_video'; - taskCurrent.final.url = task.task_result.video; + taskCurrent.final.url = task.task_result.video_url; taskCurrent.final.note = 'watermark'; taskCurrent.status = 'COMPLETED'; // 停止轮询