fix(frontend): #1913 智能剪辑无模板时后端自动兜底,移除空template硬拦截

This commit is contained in:
2026-09-15 00:48:18 +08:00
parent 23cd65a28a
commit 3275a6b8c3
@@ -1,6 +1,7 @@
/**
* Step 1 素材选择 Hook(#1899 原 Step2 前移)
* Step 1 素材选择 Hook(#1899 原 Step2 前移,#1913 修复空模板兜底)
* 使用 clipCount 替代 templateSegments 决定片段数量
* #1913:selectedTemplate 为空时不再硬拦截,仍发 from-assets 请求(后端自动兜底创建默认模板)
*/
import { useCallback, useEffect, useRef } from "react"
import { message } from "antd"
@@ -19,10 +20,10 @@ interface UseStep2MaterialsProps {
onSmartSelectedIdsChange: (ids: string[]) => void
/** 片段数量(#1899) */
clipCount: number
/** 当前选中的模板/草稿 ID,用于自动保存 */
/** 当前选中的模板/草稿 ID;空字符串时由后端自动兜底创建默认模板(#1913) */
selectedTemplate?: string
/** 服务端 clips 创建成功后的回调,用于通知预览播放器 */
onServerClipsChange?: (clips: EditPlanClip[]) => void
/** 服务端 clips 创建成功后的回调,用于通知预览播放器;#1913 新增第二个参数回传实际使用的 template_id */
onServerClipsChange?: (clips: EditPlanClip[], templateId?: string) => void
}
export function useStep2Materials({
@@ -33,7 +34,7 @@ export function useStep2Materials({
smartSelectedIds,
onSmartSelectedIdsChange,
clipCount,
selectedTemplate,
selectedTemplate = "",
onServerClipsChange,
}: UseStep2MaterialsProps) {
const {
@@ -76,16 +77,20 @@ export function useStep2Materials({
handleSmartMatch,
])
/* ── 选择素材后自动保存草稿 asset_ids(防抖 500ms,失败静默) ── */
const { scheduleSave } = useDraftAutoSave(selectedTemplate)
/* ── 自动保存草稿 asset_ids(防抖 500ms,失败静默) ──
* #1913:selectedTemplate 为空时不发草稿保存请求(后端需要 template_id)
*/
const { scheduleSave } = useDraftAutoSave(selectedTemplate || undefined)
useEffect(() => {
if (!selectedTemplate) return
const ids = materialMode === "auto" ? smartSelectedIds : selectedMaterials
scheduleSave({ asset_ids: ids }, 500)
}, [selectedTemplate, materialMode, selectedMaterials, smartSelectedIds, scheduleSave])
/* ── 选择素材后同步写入 edit_plan_clips(防抖 800ms,失败静默)──
* 调用后端 POST /clips/from-assets,传递 clipCount 作为片段数量
/* ── 选择素材后同步写入 edit_plan_clips(防抖 800ms)──
* 调用后端 POST /clips/from-assets,传递 clipCount 作为片段数量。
* #1913:selectedTemplate 为空时仍发请求,后端自动兜底创建默认 voice_over 模板。
* 后端返回后若实际创建了新模板,通过 onServerClipsChange 回传 template_id。
*/
const clipsTimerRef = useRef<ReturnType<typeof setTimeout>>()
const clipsAbortRef = useRef<AbortController | null>(null)
@@ -95,10 +100,11 @@ export function useStep2Materials({
selectedTemplateRef.current = selectedTemplate
const onServerClipsChangeRef = useRef(onServerClipsChange)
onServerClipsChangeRef.current = onServerClipsChange
/** 记录后端兜底返回的新 template_id(#1913) */
const fallbackTidRef = useRef<string>("")
useEffect(() => {
const tid = selectedTemplateRef.current
if (!tid) return
const tid = selectedTemplateRef.current || fallbackTidRef.current
const ids = materialMode === "auto" ? smartSelectedIds : selectedMaterials
if (!ids.length) {
onServerClipsChangeRef.current?.([])
@@ -112,31 +118,59 @@ export function useStep2Materials({
clipsAbortRef.current = controller
const requiredClipsCount = clipCountRef.current
const hasTid = !!tid
const requestTid = tid // 空串时由后端兜底
try {
// 1. 清空旧片段
await updateEditPlanClips(tid, [], controller.signal, true)
// 2. 调用后端 from-assets 接口创建片段,传递 clipCount
await createClipsFromAssets(tid, ids, "main", requiredClipsCount, {
signal: controller.signal,
silentErrorToast: true,
})
// 3. 获取服务端生成的 clips
const clipList = await getEditPlanClips(tid, { limit: 500 })
const readyClips = clipList.items
.filter((c) => c.status === "ready")
.sort((a, b) => a.order - b.order)
onServerClipsChangeRef.current?.(readyClips)
if (hasTid) {
// 已有模板:先清空旧片段
await updateEditPlanClips(requestTid, [], controller.signal, true)
}
// 调用后端 from-assets 接口创建片段,传递 clipCount
// #1913:空模板时传空字符串,后端自动创建 voice_over 默认模板
const fromAssetsResp = await createClipsFromAssets(
requestTid,
ids,
"main",
requiredClipsCount,
{
signal: controller.signal,
silentErrorToast: true,
},
)
// 从响应中提取后端可能返回的 template_id(兜底创建的新模板)
const respTid =
(fromAssetsResp as { template_id?: string })?.template_id ||
(fromAssetsResp as { edit_plan_id?: string })?.edit_plan_id ||
""
const finalTid = respTid || requestTid
if (respTid && !selectedTemplateRef.current) {
fallbackTidRef.current = respTid
}
if (finalTid) {
// 获取服务端生成的 clips
const clipList = await getEditPlanClips(finalTid, { limit: 500 })
const readyClips = clipList.items
.filter((c) => c.status === "ready")
.sort((a, b) => a.order - b.order)
onServerClipsChangeRef.current?.(readyClips, finalTid)
} else {
// 无有效 template_id(后端未返回),无法获取 clips
console.warn("[useStep2Materials] 后端未返回 template_id,无法获取 clips")
message.error("素材匹配失败,请刷新页面重试")
}
} catch (err) {
const name = (err as { name?: string })?.name
if (name === "CanceledError" || name === "AbortError") return
const code = (err as { code?: string })?.code
if (code === "ECONNABORTED" || /timeout/i.test((err as Error)?.message || "")) {
console.warn("[useStep2Materials] 智能选片超时:", err)
message.error("智能选片失败,请重试")
message.error("智能选片超时,请检查网络后重试")
return
}
console.warn("[useStep2Materials] 写入 clips 失败:", err)
message.error("素材匹配失败,请稍后重试")
}
}, 800)