d67d6eb2cd
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m40s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m40s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m3s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 4m3s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m27s
CI/CD Pipeline / Unit Tests (push) Successful in 10m29s
CI/CD Pipeline / Integration Tests (push) Successful in 3m42s
CI/CD Pipeline / CI Gate (push) Failing after 830h14m16s
CI/CD Pipeline / Canary Release to Production (push) Failing after 830h14m48s
CI/CD Pipeline / Deploy Production (push) Failing after 830h14m48s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 830h16m54s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 830h14m49s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 830h16m54s
CI/CD Pipeline / Build Production Web Image (push) Failing after 830h14m49s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 830h16m56s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 830h25m18s
CI/CD Pipeline / PR Build Web Image (push) Failing after 830h25m20s
CI/CD Pipeline / Frontend Lint (push) Failing after 830h25m20s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 830h25m24s
CI/CD Pipeline / Production Browser E2E (push) Failing after 830h48m19s
CI/CD Pipeline / Build Production API Image (push) Failing after 830h48m20s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 830h50m25s
CI/CD Pipeline / PR Build API Image (push) Failing after 830h58m51s
219 lines
6.4 KiB
TypeScript
219 lines
6.4 KiB
TypeScript
/**
|
|
* Step 6 封面设置 Hook
|
|
* 封装封面设置的交互逻辑,对接后端封面模板 CRUD API
|
|
*/
|
|
import { useCallback, useEffect, useState } from "react"
|
|
import { message } from "antd"
|
|
import type { CoverConfig, CoverTemplate } from "../types/cover"
|
|
import { generateCover } from "@/api/template-editor"
|
|
import {
|
|
fetchCoverTemplates,
|
|
createCoverTemplate,
|
|
updateCoverTemplate,
|
|
deleteCoverTemplate,
|
|
} from "@/api/cover-templates"
|
|
|
|
interface UseStep6CoverProps {
|
|
coverSettings: CoverConfig
|
|
onCoverSettingsChange: (settings: CoverConfig) => void
|
|
duration: number
|
|
/** 当前素材 ID 列表,用于智能封面生成 */
|
|
assetIds?: string[]
|
|
/** 当前选中的模板 ID */
|
|
selectedTemplate?: string
|
|
}
|
|
|
|
export function useStep6Cover({
|
|
coverSettings,
|
|
onCoverSettingsChange,
|
|
duration,
|
|
assetIds = [],
|
|
selectedTemplate = "",
|
|
}: 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 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 (assetIds.length === 0) {
|
|
message.error("请先选择素材")
|
|
return
|
|
}
|
|
|
|
setGenerating(true)
|
|
// 超时保护:300 秒后强制重置,防止 state 卡死导致按钮永久失效
|
|
const timeoutId = setTimeout(() => {
|
|
setGenerating(false)
|
|
}, 300000)
|
|
try {
|
|
const response = await generateCover(selectedTemplate, {
|
|
asset_ids: assetIds,
|
|
cover_type: "ai_frame",
|
|
})
|
|
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)
|
|
|
|
// 提取详细错误信息
|
|
let errorMsg = "封面生成失败"
|
|
const e = err 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 = "服务器无响应,请检查网络连接"
|
|
console.error("[Step6] 请求无响应:", e.request)
|
|
} else if (e.message) {
|
|
errorMsg = e.message
|
|
}
|
|
|
|
message.error(errorMsg)
|
|
} finally {
|
|
clearTimeout(timeoutId)
|
|
setGenerating(false)
|
|
}
|
|
}, [selectedTemplate, assetIds, coverSettings, onCoverSettingsChange, generating])
|
|
|
|
// ── 模板操作方法 ──
|
|
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 || "默认"
|
|
|
|
const totalDuration = duration || 30
|
|
|
|
return {
|
|
coverSettings,
|
|
generating,
|
|
generateAutoCover,
|
|
totalDuration,
|
|
showCoverSettings,
|
|
setShowCoverSettings,
|
|
showCoverEditor,
|
|
setShowCoverEditor,
|
|
selectedTemplateId,
|
|
setSelectedTemplateId,
|
|
editingTemplate,
|
|
coverTemplates,
|
|
templatesLoading,
|
|
templatesError,
|
|
selectedTemplateName,
|
|
handleSelectTemplate,
|
|
handleEditTemplate,
|
|
handleSaveTemplate,
|
|
handleDeleteTemplate,
|
|
loadTemplates,
|
|
}
|
|
}
|
|
|
|
export default useStep6Cover
|