fix(frontend): #1913 智能剪辑无模板时后端自动兜底,移除空template硬拦截
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user