Compare commits

...

1 Commits

Author SHA1 Message Date
saas-backend c0c29bb0c2 feat(digital-human): 对口型生成弹窗增加计时器
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m44s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m51s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m56s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m31s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 3m36s
AI Code Review / AI Code Review (pull_request) Successful in 6m43s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 8m32s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 9m41s
- 对口型生成弹窗loading状态显示已用时(MM:SS格式,每秒更新)
- 使用useState+useEffect+setInterval实现计时,组件卸载时clearInterval防内存泄漏
- 取消生成或生成完成/失败时停止并重置计时器
- 样式与现有灰色提示文字风格一致
2026-09-24 13:44:03 +08:00
+41 -1
View File
@@ -61,6 +61,9 @@ const AiAvatarPage: React.FC = () => {
"generating", "generating",
) )
const [lipsyncErrorMessage, setLipsyncErrorMessage] = useState("") const [lipsyncErrorMessage, setLipsyncErrorMessage] = useState("")
const [lipsyncElapsed, setLipsyncElapsed] = useState(0)
/* ── 对口型计时器 ── */
const lipsyncElapsedTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
/* ── 渲染进度弹窗 ── */ /* ── 渲染进度弹窗 ── */
const [showRenderModal, setShowRenderModal] = useState(false) const [showRenderModal, setShowRenderModal] = useState(false)
const [renderStatus, setRenderStatus] = useState<"generating" | "completed" | "failed">( const [renderStatus, setRenderStatus] = useState<"generating" | "completed" | "failed">(
@@ -80,6 +83,30 @@ const AiAvatarPage: React.FC = () => {
/* ── 渲染进度轮询 ── */ /* ── 渲染进度轮询 ── */
const renderTimerRef = useRef<ReturnType<typeof setInterval> | null>(null) const renderTimerRef = useRef<ReturnType<typeof setInterval> | 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) => { const togglePanel = useCallback((key: PanelKey) => {
setCollapsed((prev) => ({ ...prev, [key]: !prev[key] })) setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }))
}, []) }, [])
@@ -222,6 +249,7 @@ const AiAvatarPage: React.FC = () => {
setShowLipsyncModal(true) setShowLipsyncModal(true)
setLipsyncStatus("generating") setLipsyncStatus("generating")
setLipsyncErrorMessage("") setLipsyncErrorMessage("")
_startLipsyncElapsedTimer()
const asset = await getAssetById(video.id) const asset = await getAssetById(video.id)
const videoUrl = asset?.file_url const videoUrl = asset?.file_url
@@ -265,6 +293,7 @@ const AiAvatarPage: React.FC = () => {
state.setLipsyncJob(updated) state.setLipsyncJob(updated)
if (updated.status === "completed") { if (updated.status === "completed") {
if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current)
_stopLipsyncElapsedTimer()
setLipsyncStatus("completed") setLipsyncStatus("completed")
setTimeout(() => { setTimeout(() => {
setShowLipsyncModal(false) setShowLipsyncModal(false)
@@ -272,6 +301,7 @@ const AiAvatarPage: React.FC = () => {
}, 1000) }, 1000)
} else if (updated.status === "failed") { } else if (updated.status === "failed") {
if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current)
_stopLipsyncElapsedTimer()
setLipsyncStatus("failed") setLipsyncStatus("failed")
setLipsyncErrorMessage(updated.error_message || "对口型生成失败") setLipsyncErrorMessage(updated.error_message || "对口型生成失败")
} }
@@ -285,6 +315,8 @@ const AiAvatarPage: React.FC = () => {
data: (err as { response?: { data?: unknown } })?.response?.data, data: (err as { response?: { data?: unknown } })?.response?.data,
message: err instanceof Error ? err.message : String(err), message: err instanceof Error ? err.message : String(err),
}) })
_stopLipsyncElapsedTimer()
setLipsyncElapsed(0)
setShowLipsyncModal(false) setShowLipsyncModal(false)
message.error(err instanceof Error ? err.message : "对口型任务提交失败,请重试") message.error(err instanceof Error ? err.message : "对口型任务提交失败,请重试")
} }
@@ -297,6 +329,8 @@ const AiAvatarPage: React.FC = () => {
state.style, state.style,
state.ttsPreview, state.ttsPreview,
_startLipsyncElapsedTimer,
_stopLipsyncElapsedTimer,
]) ])
// 取消对口型生成 // 取消对口型生成
@@ -305,16 +339,19 @@ const AiAvatarPage: React.FC = () => {
clearInterval(lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current)
lipsyncTimerRef.current = null lipsyncTimerRef.current = null
} }
_stopLipsyncElapsedTimer()
setLipsyncElapsed(0)
setShowLipsyncModal(false) setShowLipsyncModal(false)
setLipsyncStatus("generating") setLipsyncStatus("generating")
setLipsyncErrorMessage("") setLipsyncErrorMessage("")
}, []) }, [_stopLipsyncElapsedTimer])
// 清理轮询 // 清理轮询
useEffect(() => { useEffect(() => {
return () => { return () => {
if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current)
if (renderTimerRef.current) clearInterval(renderTimerRef.current) if (renderTimerRef.current) clearInterval(renderTimerRef.current)
if (lipsyncElapsedTimerRef.current) clearInterval(lipsyncElapsedTimerRef.current)
} }
}, []) }, [])
@@ -964,6 +1001,9 @@ const AiAvatarPage: React.FC = () => {
</div> </div>
<div style={{ marginTop: 8, fontSize: 13, color: "#8c8ca1" }}> <div style={{ marginTop: 8, fontSize: 13, color: "#8c8ca1" }}>
{_formatElapsed(lipsyncElapsed)}
</div>
<div style={{ marginTop: 4, fontSize: 13, color: "#8c8ca1" }}>
</div> </div>
</> </>