Files
xiaoxia-saas/apps/web/src/pages/generate/hooks/useStep6Cover.ts
T
xiaoxia d512cd2ac1
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m51s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m19s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m32s
AI Code Review / AI Code Review (pull_request) Failing after 3m6s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m19s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 12m25s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m44s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 15m1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m9s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 9m55s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m11s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 12m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m48s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m52s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 37m18s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 33m46s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 448h9m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 448h9m43s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 448h9m24s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 448h11m5s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 448h11m7s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 448h13m7s
CI/CD Pipeline / Frontend Lint (push) Failing after 448h15m19s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 448h16m20s
CI/CD Pipeline / PR Build Web Image (push) Failing after 448h13m9s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 448h19m4s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 448h19m8s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 448h26m9s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 448h43m23s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 448h44m18s
CI/CD Pipeline / PR Build API Image (push) Failing after 448h47m12s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 448h53m7s
Merge PR #1533: 调换确认生成与选择封面顺序,封面从最终成片选帧
2026-08-28 22:59:19 +08:00

246 lines
7.3 KiB
TypeScript

/**
* Step 6 封面设置 Hook
* 封面候选帧从确认生成的最终视频中获取(MediaKit 选帧)
* 不再从预览片段创建预览视频
*/
import { useCallback, useEffect, useState } from "react"
import { message } from "antd"
import type { CoverConfig, CoverTemplate } from "../types/cover"
import { generateCover } from "@/api/generation"
import type { GeneratedVideo } from "@/api/template-editor"
import type { TitleSettings } from "../types"
import {
fetchCoverTemplates,
createCoverTemplate,
updateCoverTemplate,
deleteCoverTemplate,
} from "@/api/cover-templates"
interface UseStep6CoverProps {
coverSettings: CoverConfig
onCoverSettingsChange: (settings: CoverConfig) => void
/** 当前选中的模板 ID */
selectedTemplate?: string
/** Step4 标题设置,用于封面叠加标题 */
titleSettings?: TitleSettings
/** 确认生成步骤产出的最终视频列表 */
generatedVideos: GeneratedVideo[]
}
export function useStep6Cover({
coverSettings,
onCoverSettingsChange,
selectedTemplate = "",
titleSettings,
generatedVideos,
}: UseStep6CoverProps) {
const [generating, setGenerating] = useState(false)
// ── 封面设置弹窗状态 ──
const [showCoverSettings, setShowCoverSettings] = useState(false)
const [showCoverEditor, setShowCoverEditor] = useState(false)
const [selectedTemplateId, setSelectedTemplateId] = useState("default")
const [editingTemplate, setEditingTemplate] = useState<CoverTemplate | null>(null)
const [coverTemplates, setCoverTemplates] = useState<CoverTemplate[]>([])
// ── API 加载状态 ──
const [templatesLoading, setTemplatesLoading] = useState(false)
const [templatesError, setTemplatesError] = useState<string | null>(null)
/** 最终成片:取第一个已完成视频 */
const finalVideo = generatedVideos.find((v) => v.status === "completed") || generatedVideos[0]
/** 从后端加载封面模板列表 */
const loadTemplates = useCallback(async () => {
setTemplatesLoading(true)
setTemplatesError(null)
try {
const res = await fetchCoverTemplates()
setCoverTemplates(res.items || [])
} catch (err) {
console.error("[Step6] 加载封面模板失败:", err)
setTemplatesError("加载模板失败,请稍后重试")
} finally {
setTemplatesLoading(false)
}
}, [])
/** 弹窗打开时加载模板列表 */
useEffect(() => {
if (showCoverSettings) {
loadTemplates()
}
}, [showCoverSettings, loadTemplates])
/** 调用后端智能封面 API,从最终成片中抽帧 */
const generateAutoCover = useCallback(async () => {
if (generating) {
message.warning("封面正在生成中,请稍候...")
return
}
if (!selectedTemplate) {
message.error("请先选择模板")
return
}
if (!finalVideo) {
message.error("请先生成视频再选择封面")
return
}
setGenerating(true)
const timeoutId = setTimeout(() => {
setGenerating(false)
}, 300000)
try {
const response = await generateCover(selectedTemplate, {
generated_video_id: finalVideo.id,
video_url: finalVideo.file_url || finalVideo.download_url || "",
cover_type: "ai_frame",
...(titleSettings?.title
? {
title_config: {
text: titleSettings.title,
font: titleSettings.font,
font_size: titleSettings.size,
font_color: titleSettings.color,
position: titleSettings.position,
bold: titleSettings.bold,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
},
}
: {}),
})
clearTimeout(timeoutId)
const thumbnailUrl = response.cover?.image_url || ""
if (thumbnailUrl) {
onCoverSettingsChange({
...coverSettings,
thumbnail_url: thumbnailUrl,
ai_suggested_time: response.cover?.frame_time ?? null,
})
message.success("封面生成成功")
} else {
message.warning("封面生成未返回图片,请重试")
}
} catch (err) {
clearTimeout(timeoutId)
console.error("[Step6] 智能封面生成失败:", err)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyErr = err as any
if (anyErr?.__msgShown) {
// 拦截器已处理,不再重复弹出
} else {
let errorMsg = "封面生成失败"
const e = anyErr as {
response?: { data?: { detail?: string; message?: string }; status?: number }
request?: unknown
message?: string
}
if (e.response) {
const detail = e.response.data?.detail || e.response.data?.message || ""
errorMsg = detail || `后端错误 (${e.response.status})`
console.error("[Step6] 后端返回:", e.response.data)
} else if (e.request) {
errorMsg = "服务器无响应,请检查网络连接"
} else if (e.message) {
errorMsg = e.message
}
message.error(errorMsg)
}
} finally {
setGenerating(false)
}
}, [
selectedTemplate,
finalVideo,
coverSettings,
onCoverSettingsChange,
generating,
titleSettings,
])
// ── 模板操作方法 ──
const handleSelectTemplate = useCallback((id: string) => {
setSelectedTemplateId(id)
}, [])
const handleEditTemplate = useCallback((tpl: CoverTemplate) => {
setEditingTemplate(tpl)
setShowCoverEditor(true)
}, [])
/** 保存模板(创建或更新) */
const handleSaveTemplate = useCallback(
async (tpl: CoverTemplate) => {
try {
if (tpl.id && coverTemplates.some((t) => t.id === tpl.id)) {
const updated = await updateCoverTemplate(tpl.id, {
name: tpl.name,
config: tpl.config,
})
setCoverTemplates((prev) => prev.map((t) => (t.id === tpl.id ? { ...t, ...updated } : t)))
} else {
const created = await createCoverTemplate({
name: tpl.name,
config: tpl.config,
})
setCoverTemplates((prev) => [...prev, created])
}
setShowCoverEditor(false)
} catch (err) {
console.error("[Step6] 保存模板失败:", err)
}
},
[coverTemplates],
)
/** 删除模板 */
const handleDeleteTemplate = useCallback(
async (id: string) => {
try {
await deleteCoverTemplate(id)
setCoverTemplates((prev) => prev.filter((t) => t.id !== id))
if (selectedTemplateId === id) {
setSelectedTemplateId("default")
}
} catch (err) {
console.error("[Step6] 删除模板失败:", err)
}
},
[selectedTemplateId],
)
const selectedTemplateName =
coverTemplates.find((t) => t.id === selectedTemplateId)?.name || "默认"
return {
coverSettings,
generating,
generateAutoCover,
finalVideo,
showCoverSettings,
setShowCoverSettings,
showCoverEditor,
setShowCoverEditor,
selectedTemplateId,
setSelectedTemplateId,
editingTemplate,
coverTemplates,
templatesLoading,
templatesError,
selectedTemplateName,
handleSelectTemplate,
handleEditTemplate,
handleSaveTemplate,
handleDeleteTemplate,
loadTemplates,
}
}
export default useStep6Cover