From a09308e57c0152da6d2843f6fccebb7ff05be367 Mon Sep 17 00:00:00 2001 From: qikongjian Date: Wed, 17 Sep 2025 11:00:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E5=8A=A0=E8=BD=BDiframe?= =?UTF-8?q?=E7=84=B6=E5=90=8E=E7=BB=9F=E4=B8=80=E8=B5=B0exportVideoWithRet?= =?UTF-8?q?ry=E6=99=BA=E8=83=BD=E5=89=AA=E8=BE=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/work-flow.tsx | 110 +++++++++++++++++++++++++++++---- docs/导出进度.md | 15 +++++ utils/export-service.ts | 63 +++++++++++++++++-- 3 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 docs/导出进度.md diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index 2e7eb67..ce94ea0 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -15,7 +15,7 @@ import { useSearchParams } from "next/navigation"; import SmartChatBox from "@/components/SmartChatBox/SmartChatBox"; import { Drawer, Tooltip, notification } from 'antd'; import { showEditingNotification } from "@/components/pages/work-flow/editing-notification"; -import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; +// import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; import { exportVideoWithRetry } from '@/utils/export-service'; const WorkFlow = React.memo(function WorkFlow() { @@ -38,11 +38,20 @@ const WorkFlow = React.memo(function WorkFlow() { const [isFocusChatInput, setIsFocusChatInput] = React.useState(false); const [aiEditingResult, setAiEditingResult] = React.useState(null); - const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise }>(null); + // const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise }>(null); const [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial'); - const [iframeAiEditingKey, setIframeAiEditingKey] = React.useState(`iframe-ai-editing-${Date.now()}`); + // const [iframeAiEditingKey, setIframeAiEditingKey] = React.useState(`iframe-ai-editing-${Date.now()}`); const [isEditingInProgress, setIsEditingInProgress] = React.useState(false); const isEditingInProgressRef = useRef(false); + + // 导出进度状态 + const [exportProgress, setExportProgress] = React.useState<{ + status: 'processing' | 'completed' | 'failed'; + percentage: number; + message: string; + stage?: string; + taskId?: string; + } | null>(null); const searchParams = useSearchParams(); const episodeId = searchParams.get('episodeId') || ''; @@ -52,6 +61,48 @@ const WorkFlow = React.memo(function WorkFlow() { SaveEditUseCase.setProjectId(episodeId); let editingNotificationKey = useRef(`editing-${Date.now()}`); const [isHandleEdit, setIsHandleEdit] = React.useState(false); + + // 使用 ref 存储 handleTestExport 避免循环依赖 + const handleTestExportRef = useRef<(() => Promise) | null>(null); + + // 导出进度回调处理 + const handleExportProgress = useCallback((progressData: { + status: 'processing' | 'completed' | 'failed'; + percentage: number; + message: string; + stage?: string; + taskId?: string; + }) => { + console.log('📊 导出进度更新:', progressData); + setExportProgress(progressData); + + // 根据状态显示不同的通知 + if (progressData.status === 'processing') { + notification.info({ + message: '导出进度', + description: `${progressData.message} (${progressData.percentage}%)`, + placement: 'topRight', + duration: 2, + key: 'export-progress' + }); + } else if (progressData.status === 'completed') { + notification.success({ + message: '导出成功', + description: progressData.message, + placement: 'topRight', + duration: 5, + key: 'export-progress' + }); + } else if (progressData.status === 'failed') { + notification.error({ + message: '导出失败', + description: progressData.message, + placement: 'topRight', + duration: 8, + key: 'export-progress' + }); + } + }, []); // 处理编辑计划生成完成的回调 const handleEditPlanGenerated = useCallback(() => { console.log('🚀 handleEditPlanGenerated called, current ref:', isEditingInProgressRef.current); @@ -70,7 +121,10 @@ const WorkFlow = React.memo(function WorkFlow() { // 改为调用测试剪辑计划导出按钮方法 // aiEditingButtonRef.current?.handleAIEditing(); - handleTestExport(); + // 使用 ref 调用避免循环依赖 + setTimeout(() => { + handleTestExportRef.current?.(); + }, 0); editingNotificationKey.current = `editing-${Date.now()}`; showEditingNotification({ description: 'Performing intelligent editing...', @@ -89,7 +143,7 @@ const WorkFlow = React.memo(function WorkFlow() { } // 重新生成 iframeAiEditingKey 触发重新渲染 - setIframeAiEditingKey(`iframe-ai-editing-${Date.now()}`); + // setIframeAiEditingKey(`iframe-ai-editing-${Date.now()}`); // 延时200ms后显示重试通知,确保之前的通知已销毁 setTimeout(() => { @@ -118,7 +172,7 @@ const WorkFlow = React.memo(function WorkFlow() { }, 200); } }); - }, [episodeId]); // 移除 isEditingInProgress 依赖 + }, [episodeId]); // handleTestExport 在内部调用,无需作为依赖 /** 处理导出失败 */ const handleExportFailed = useCallback(() => { @@ -239,22 +293,28 @@ const WorkFlow = React.memo(function WorkFlow() { }); try { - // 使用封装的导出服务 - const result = await exportVideoWithRetry(episodeId, taskObject); + // 使用封装的导出服务,传递进度回调 + const result = await exportVideoWithRetry(episodeId, taskObject, handleExportProgress); console.log('🎉 导出服务完成,结果:', result); return result; } catch (error) { console.error('❌ 导出服务失败:', error); throw error; } - }, [episodeId, taskObject]); + }, [episodeId, taskObject, handleExportProgress]); + + // 将 handleTestExport 赋值给 ref + React.useEffect(() => { + handleTestExportRef.current = handleTestExport; + }, [handleTestExport]); - // iframe智能剪辑回调函数 + // iframe智能剪辑回调函数 - 已注释 + /* const handleIframeAIEditingComplete = useCallback((result: any) => { console.log('🎉 iframe AI剪辑完成,结果:', result); @@ -272,16 +332,21 @@ const WorkFlow = React.memo(function WorkFlow() { // setAiEditingInProgress(false); // 已移除该状态变量 }, [setAnyAttribute]); + */ + /* const handleIframeAIEditingError = useCallback((error: string) => { console.error('❌ iframe AI剪辑失败:', error); // setAiEditingInProgress(false); // 已移除该状态变量 }, []); + */ + /* const handleIframeAIEditingProgress = useCallback((progress: number, message: string) => { console.log(`📊 AI剪辑进度: ${progress}% - ${message}`); // setAiEditingInProgress(true); // 已移除该状态变量 }, []); + */ return (
@@ -346,7 +411,8 @@ const WorkFlow = React.memo(function WorkFlow() {
- {/* AI剪辑按钮 - 当可以跳转剪辑时显示 */} + {/* AI剪辑按钮 - 已注释,不加载iframe */} + {/* { isShowAutoEditing && (
@@ -367,6 +433,28 @@ const WorkFlow = React.memo(function WorkFlow() {
) } + */} + + {/* 导出进度显示 */} + {exportProgress && exportProgress.status === 'processing' && ( +
+
+
+ 导出进度: {exportProgress.percentage}% +
+
+
+
+
+ {exportProgress.message} + {exportProgress.stage && ` (${exportProgress.stage})`} +
+
+
+ )} {/* 测试导出接口按钮 - 隐藏显示(仍可通过逻辑调用) */}
void; +} + // 导出服务配置 interface ExportServiceConfig { maxRetries?: number; @@ -436,7 +450,7 @@ export class VideoExportService { * - status: 'failed' 时抛出 EXPORT_FAILED 错误,触发重新调用 api/export/stream * - 其他状态继续轮询,最多轮询10分钟(5秒间隔) */ - private async pollExportProgress(taskId: string): Promise { + private async pollExportProgress(taskId: string, progressCallback?: ExportProgressCallback['onProgress']): Promise { console.log('🔄 开始轮询导出进度,任务ID:', taskId); const maxAttempts = 120; // 最多轮询10分钟(5秒间隔) let attempts = 0; @@ -460,6 +474,18 @@ export class VideoExportService { if (status === 'completed') { console.log('🎉 导出任务完成!', progress); + + // 触发完成状态回调 + if (progressCallback) { + progressCallback({ + status: 'completed', + percentage: 100, + message: progress?.message || '导出完成', + stage: 'completed', + taskId + }); + } + return { task_id: taskId, status: status, @@ -472,6 +498,18 @@ export class VideoExportService { }; } else if (status === 'failed') { console.log('❌ 导出任务失败,需要重新调用 api/export/stream'); + + // 触发失败状态回调 + if (progressCallback) { + progressCallback({ + status: 'failed', + percentage: 0, + message: progress?.message || '导出任务失败', + stage: 'failed', + taskId + }); + } + throw new Error(`EXPORT_FAILED: ${progress?.message || '导出任务失败'}`); } else if (status === 'error') { throw new Error(`导出任务错误: ${progress?.message || '未知错误'}`); @@ -483,6 +521,17 @@ export class VideoExportService { console.log(`⏳ 导出进度: ${percentage}% - ${stage} - ${message}`); + // 触发处理中状态回调 + if (progressCallback) { + progressCallback({ + status: 'processing', + percentage, + message, + stage, + taskId + }); + } + // 等待5秒后继续轮询 await new Promise(resolve => setTimeout(resolve, this.config.pollInterval)); attempts++; @@ -504,7 +553,7 @@ export class VideoExportService { /** * 主要的导出方法 - 支持重试机制 */ - public async exportVideo(episodeId: string, taskObject: any): Promise { + public async exportVideo(episodeId: string, taskObject: any, progressCallback?: ExportProgressCallback['onProgress']): Promise { let currentAttempt = 1; try { @@ -581,7 +630,7 @@ export class VideoExportService { console.log('🔄 开始轮询导出进度,任务ID:', taskId); try { - const finalExportResult = await this.pollExportProgress(taskId); + const finalExportResult = await this.pollExportProgress(taskId, progressCallback); // 导出成功 console.log('🎉 导出成功完成!'); @@ -727,8 +776,12 @@ export const videoExportService = new VideoExportService({ /** * 便捷的导出函数 */ -export async function exportVideoWithRetry(episodeId: string, taskObject: any): Promise { - return videoExportService.exportVideo(episodeId, taskObject); +export async function exportVideoWithRetry( + episodeId: string, + taskObject: any, + progressCallback?: ExportProgressCallback['onProgress'] +): Promise { + return videoExportService.exportVideo(episodeId, taskObject, progressCallback); } /**