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

This commit is contained in:
2026-09-15 00:47:44 +08:00
parent d8effd8e77
commit c1ad41c9f0
@@ -1,5 +1,5 @@
/**
* 批量变体真实片段 Hook(#1744 接入 / #1750 修正)
* 批量变体真实片段 Hook(#1744 接入 / #1750 修正,#1913 空模板兜底)
*
* 批量预览(N>1)进入第 4 步时,向后端轻量接口 POST /generation/variant-plans
* 申请 N 个变体的独立剪辑计划片段:
@@ -10,14 +10,16 @@
* 因此前端按这些 clips 播放的预览与最终成片一致;
* - 正式生成时把 plan_ids 回传,后端直接关联预览 plan 渲染,不再重新选片。
*
* 错误处理(#1750 基线:严禁用前端假数据冒充预览):
* 错误处理(#1750 基线:严禁用前端假数据冒充预览,#1913 加友好提示):
* - 端点 404(后端未上线)/ 400 / 网络错误 / 超时:一律置 error 状态,
* UI 显示「预览加载失败,请重试」,不再静默降级本地模拟;
* - 单个变体 clips 为空同样视为加载失败(计划数据不完整)。
*
* N=1 不调用本 hook(单视频零回归)。
* #1913:templateId 为空时不再硬拦截,仍发 variant-plans 请求(后端自动兜底)。
*/
import { useCallback, useEffect, useRef, useState } from "react"
import { message } from "antd"
import type { EditPlanClip } from "@/api/template-editor"
import { createBatchVariantPlans, type VariantPlan } from "@/api/generation/variantPlans"
@@ -42,6 +44,7 @@ interface UseBatchVariantPlansOptions {
/** 是否启用:仅批量(count>1)且素材已选时为 true */
enabled: boolean
count: number
/** 模板 ID;空字符串时由后端自动兜底默认模板(#1913) */
templateId: string
assetIds: string[]
/** 源剪辑计划 ID(草稿/预览关联),无则空串由后端兜底最新 plan */
@@ -76,11 +79,16 @@ export function useBatchVariantPlans({
// 重试计数(state):用户点重试时 +1,驱动 effect 重新发起请求(即使指纹相同);
// 用 state 而非 ref + 直接调 load,避免 retry 与 effect 双发导致 seq 竞争
const [retryNonce, setRetryNonce] = useState(0)
/** 记录后端兜底返回的新 template_id(#1913) */
const fallbackTidRef = useRef<string>("")
/** 防止错误 toast 重复弹出 */
const errorToastShownRef = useRef(false)
const load = useCallback(async () => {
const seq = ++requestSeqRef.current
setLoading(true)
setError(false)
errorToastShownRef.current = false
try {
// 配音参数:与 useGenerateVideo 保持一致的传参逻辑
// - 独立配音模式 + voiceLibraryIds 非空:传 voice_library_ids
@@ -93,8 +101,10 @@ export function useBatchVariantPlans({
voiceParam.voice_library_id = voiceLibraryId
}
// #1913:templateId 为空时使用空串,后端自动兜底默认模板
const effectiveTid = templateId || fallbackTidRef.current
const resp = await createBatchVariantPlans({
template_id: templateId,
template_id: effectiveTid,
asset_ids: assetIds,
count,
...(sourcePlanId ? { source_edit_plan_id: sourcePlanId } : {}),
@@ -102,6 +112,15 @@ export function useBatchVariantPlans({
})
if (seq !== requestSeqRef.current) return
// #1913:从响应中提取后端可能返回的 template_id(兜底创建的新模板)
const respTid =
(resp as { template_id?: string })?.template_id ||
(resp as { edit_plan_id?: string })?.edit_plan_id ||
""
if (respTid && !templateId) {
fallbackTidRef.current = respTid
}
const items: VariantPlan[] = Array.isArray(resp.items) ? resp.items : []
const clips: EditPlanClip[][] = Array.from({ length: count }, () => [])
const planIds: string[] = Array.from({ length: count }, () => "")
@@ -125,6 +144,10 @@ export function useBatchVariantPlans({
setPlanIdsByVariant([])
setVoiceDurationsByVariant([])
setError(true)
if (!errorToastShownRef.current) {
errorToastShownRef.current = true
message.error("预览加载失败,素材可能不足,请尝试选择更多素材后重试")
}
} else {
setClipsByVariant(clips)
setPlanIdsByVariant(planIds)
@@ -139,6 +162,15 @@ export function useBatchVariantPlans({
setPlanIdsByVariant([])
setVoiceDurationsByVariant([])
setError(true)
if (!errorToastShownRef.current) {
errorToastShownRef.current = true
const code = (err as { code?: string })?.code
if (code === "ECONNABORTED" || /timeout/i.test((err as Error)?.message || "")) {
message.error("预览加载超时,请检查网络后重试")
} else {
message.error("预览加载失败,请稍后重试")
}
}
} finally {
if (seq === requestSeqRef.current) setLoading(false)
}
@@ -158,7 +190,8 @@ export function useBatchVariantPlans({
}, [])
useEffect(() => {
if (!enabled || !templateId || assetIds.length === 0 || count <= 1) {
// #1913:移除 !templateId 的硬拦截;只要 enabled=true、有素材、count>1 就发请求
if (!enabled || assetIds.length === 0 || count <= 1) {
requestSeqRef.current += 1
// 函数式更新:已是目标值时返回 prev(Object.is 相等 React 跳过渲染),
// 避免父组件传入内联字面量数组导致 effect 每次 render 触发 → 无限 setState 循环
@@ -173,7 +206,8 @@ export function useBatchVariantPlans({
const voiceKey = voiceModePerVideo
? `per:${[...voiceLibraryIds].sort().join(",")}`
: `one:${voiceLibraryId}`
const key = `${retryNonce}|${templateId}|${count}|${sourcePlanId}|${[...assetIds]
const effectiveTid = templateId || fallbackTidRef.current || "__auto__"
const key = `${retryNonce}|${effectiveTid}|${count}|${sourcePlanId}|${[...assetIds]
.sort()
.join(",")}|${voiceKey}`
if (key === lastKeyRef.current) return