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
246 lines
7.3 KiB
TypeScript
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
|