Files
xiaoxia-saas/apps/web/src/pages/generate/hooks/useGenerateVideo.ts
T
SaaS Frontend 13ca16b940
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 40s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 46s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 56s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 29s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m46s
AI Code Review / AI Code Review (pull_request) Failing after 2m14s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 1m59s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m29s
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 930h15m39s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 930h15m40s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 930h15m40s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 930h15m40s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 930h15m40s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 930h15m41s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 930h15m41s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 930h18m21s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 930h18m23s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 930h19m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 930h19m9s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 930h19m5s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 930h19m11s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 930h19m11s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 930h51m47s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 930h52m31s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 930h52m35s
feat: 前端对接确认生成 API (POST /tasks/{task_id}/confirm)
对接后端 PR #1308 新增的确认生成 API:

1. 新增 API 调用函数 confirm.ts
   - confirmGeneration(taskId, params) 调用 POST /tasks/{task_id}/confirm

2. 新增类型定义
   - ConfirmGenerationRequest: output_width, output_height, cover_url, custom_title
   - ConfirmGenerationResponse, ConfirmGenerationTaskItem

3. 修改 useGenerateVideo.ts
   - 有 previewTaskId 时使用新 API(确认生成流程)
   - 无 previewTaskId 时使用旧 API(向后兼容)
   - 传递 cover_url 和 custom_title 参数

前置条件:后端 PR #1308 同步到 develop 分支后此 PR 才可合并

关联:PR #1308 (后端确认生成 API)
2026-08-08 21:07:16 +08:00

163 lines
5.0 KiB
TypeScript
Executable File

/**
* 视频生成 Hook
* 封装视频生成的核心逻辑、状态管理、轮询等
*/
import { useState, useCallback } from "react"
import { message } from "antd"
import type { GeneratedVideo } from "@/api/template-editor"
import { generateEditPlan, updateEditPlan, getEditPlan } from "@/api/template-editor"
import { confirmGeneration } from "@/api/generation"
import type { UseGenerateVideoProps } from "./generate-video/types"
import { getGenerationPhase } from "./generate-video/phase"
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
import { buildEditPlanPayload, validateGenerateInputs } from "./generate-video/buildPayload"
import { extractBackendError, translateError } from "./generate-video/errorUtils"
export function useGenerateVideo(props: UseGenerateVideoProps) {
const { selectedTemplate } = props
/* ── 生成状态 ── */
const [generating, setGenerating] = useState(false)
const [progress, setProgress] = useState(0)
const [generated, setGenerated] = useState(false)
const [generateError, setGenerateError] = useState<string | null>(null)
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([])
const handleProgress = useCallback((p: number) => setProgress(p), [])
const handleComplete = useCallback((videos: unknown[]) => {
setGenerating(false)
setGenerated(true)
setGeneratedVideos(videos as GeneratedVideo[])
}, [])
const handleFailed = useCallback((errorMsg: string) => {
setGenerating(false)
setGenerateError(errorMsg)
}, [])
const { startPolling, clearTimer } = useGenerationPolling({
templateId: selectedTemplate,
onProgress: handleProgress,
onComplete: handleComplete,
onFailed: handleFailed,
})
/* ── 生成视频 ── */
const generate = useCallback(async () => {
const errorMsg = validateGenerateInputs(props)
if (errorMsg) {
message.warning(errorMsg)
return
}
setGenerating(true)
setProgress(0)
setGenerated(false)
setGenerateError(null)
clearTimer()
try {
// 新流程:使用确认生成 API(基于预览任务)
if (props.previewTaskId) {
// 解析分辨率
const [widthStr, heightStr] = (props.videoRatio || "1080x1920").split("x")
const outputWidth = parseInt(widthStr, 10) || 1080
const outputHeight = parseInt(heightStr, 10) || 1920
await confirmGeneration(props.previewTaskId, {
output_width: outputWidth,
output_height: outputHeight,
cover_url: props.coverSettings.imageUrl || "",
custom_title: props.titleSettings.title || "",
})
startPolling()
return
}
// 旧流程:使用 EditPlan API(向后兼容)
const payload = buildEditPlanPayload(props)
// 获取或创建草稿
await getEditPlan(selectedTemplate)
// 更新草稿内容 + 切换到 editing 状态
await updateEditPlan(selectedTemplate, payload)
await generateEditPlan(selectedTemplate)
startPolling()
} catch (err: unknown) {
console.error("[handleGenerate] 生成失败:", err)
setGenerating(false)
const backendMsg = extractBackendError(err)
console.error("[handleGenerate] 错误信息:", backendMsg, "完整错误:", err)
const finalMsg = translateError(backendMsg)
setGenerateError(finalMsg)
message.error(finalMsg)
}
}, [props, selectedTemplate, clearTimer, startPolling])
/* 重新生成(失败后重试) */
const retry = useCallback(() => {
setGenerateError(null)
generate()
}, [generate])
/* 清除错误 */
const dismissError = useCallback(() => {
setGenerateError(null)
}, [])
/* ── 下载视频 ── */
const download = useCallback(async () => {
if (!generatedVideos.length) return
const video = generatedVideos[0]
try {
const url = video.download_url || video.file_url
if (url) {
const a = document.createElement("a")
a.href = url
a.download = video.name || "generated-video.mp4"
a.target = "_blank"
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
} catch (err) {
console.error("[下载失败]", err)
message.error("下载失败,请重试")
}
}, [generatedVideos])
/* ── 分享视频 ── */
const share = useCallback(async () => {
if (!generatedVideos.length) return
const video = generatedVideos[0]
const shareUrl = video.file_url || window.location.href
try {
await navigator.clipboard.writeText(shareUrl)
message.success("视频链接已复制到剪贴板")
} catch {
message.info(`视频链接: ${shareUrl}`)
}
}, [generatedVideos])
return {
// 状态
generating,
progress,
generated,
generateError,
generatedVideos,
// 操作
generate,
retry,
dismissError,
download,
share,
// 工具
getGenerationPhase,
}
}
export default useGenerateVideo