From c0c29bb0c2ffa874009529d5e8743385be1c6aa7 Mon Sep 17 00:00:00 2001 From: saas-backend Date: Thu, 24 Sep 2026 13:44:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(digital-human):=20=E5=AF=B9=E5=8F=A3?= =?UTF-8?q?=E5=9E=8B=E7=94=9F=E6=88=90=E5=BC=B9=E7=AA=97=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=AE=A1=E6=97=B6=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 对口型生成弹窗loading状态显示已用时(MM:SS格式,每秒更新) - 使用useState+useEffect+setInterval实现计时,组件卸载时clearInterval防内存泄漏 - 取消生成或生成完成/失败时停止并重置计时器 - 样式与现有灰色提示文字风格一致 --- apps/web/src/pages/ai-avatar/AiAvatarPage.tsx | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx index 2e2b3e293..264a8fe4f 100644 --- a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx +++ b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx @@ -61,6 +61,9 @@ const AiAvatarPage: React.FC = () => { "generating", ) const [lipsyncErrorMessage, setLipsyncErrorMessage] = useState("") + const [lipsyncElapsed, setLipsyncElapsed] = useState(0) + /* ── 对口型计时器 ── */ + const lipsyncElapsedTimerRef = useRef | null>(null) /* ── 渲染进度弹窗 ── */ const [showRenderModal, setShowRenderModal] = useState(false) const [renderStatus, setRenderStatus] = useState<"generating" | "completed" | "failed">( @@ -80,6 +83,30 @@ const AiAvatarPage: React.FC = () => { /* ── 渲染进度轮询 ── */ const renderTimerRef = useRef | null>(null) + /* ── 对口型计时器工具 ── */ + const _formatElapsed = useCallback((seconds: number) => { + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}` + }, []) + + const _startLipsyncElapsedTimer = useCallback(() => { + if (lipsyncElapsedTimerRef.current) { + clearInterval(lipsyncElapsedTimerRef.current) + } + setLipsyncElapsed(0) + lipsyncElapsedTimerRef.current = setInterval(() => { + setLipsyncElapsed((prev) => prev + 1) + }, 1000) + }, []) + + const _stopLipsyncElapsedTimer = useCallback(() => { + if (lipsyncElapsedTimerRef.current) { + clearInterval(lipsyncElapsedTimerRef.current) + lipsyncElapsedTimerRef.current = null + } + }, []) + const togglePanel = useCallback((key: PanelKey) => { setCollapsed((prev) => ({ ...prev, [key]: !prev[key] })) }, []) @@ -222,6 +249,7 @@ const AiAvatarPage: React.FC = () => { setShowLipsyncModal(true) setLipsyncStatus("generating") setLipsyncErrorMessage("") + _startLipsyncElapsedTimer() const asset = await getAssetById(video.id) const videoUrl = asset?.file_url @@ -265,6 +293,7 @@ const AiAvatarPage: React.FC = () => { state.setLipsyncJob(updated) if (updated.status === "completed") { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) + _stopLipsyncElapsedTimer() setLipsyncStatus("completed") setTimeout(() => { setShowLipsyncModal(false) @@ -272,6 +301,7 @@ const AiAvatarPage: React.FC = () => { }, 1000) } else if (updated.status === "failed") { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) + _stopLipsyncElapsedTimer() setLipsyncStatus("failed") setLipsyncErrorMessage(updated.error_message || "对口型生成失败") } @@ -285,6 +315,8 @@ const AiAvatarPage: React.FC = () => { data: (err as { response?: { data?: unknown } })?.response?.data, message: err instanceof Error ? err.message : String(err), }) + _stopLipsyncElapsedTimer() + setLipsyncElapsed(0) setShowLipsyncModal(false) message.error(err instanceof Error ? err.message : "对口型任务提交失败,请重试") } @@ -297,6 +329,8 @@ const AiAvatarPage: React.FC = () => { state.style, state.ttsPreview, + _startLipsyncElapsedTimer, + _stopLipsyncElapsedTimer, ]) // 取消对口型生成 @@ -305,16 +339,19 @@ const AiAvatarPage: React.FC = () => { clearInterval(lipsyncTimerRef.current) lipsyncTimerRef.current = null } + _stopLipsyncElapsedTimer() + setLipsyncElapsed(0) setShowLipsyncModal(false) setLipsyncStatus("generating") setLipsyncErrorMessage("") - }, []) + }, [_stopLipsyncElapsedTimer]) // 清理轮询 useEffect(() => { return () => { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) if (renderTimerRef.current) clearInterval(renderTimerRef.current) + if (lipsyncElapsedTimerRef.current) clearInterval(lipsyncElapsedTimerRef.current) } }, []) @@ -964,6 +1001,9 @@ const AiAvatarPage: React.FC = () => { 对口型视频生成中…
+ 已用时:{_formatElapsed(lipsyncElapsed)} +
+
请勿关闭页面,完成后将自动提示