From c1ad41c9f00e52dce063f058cdcb20017c01713f Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 15 Sep 2026 00:47:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20#1913=20=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E5=89=AA=E8=BE=91=E6=97=A0=E6=A8=A1=E6=9D=BF=E6=97=B6=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E8=87=AA=E5=8A=A8=E5=85=9C=E5=BA=95=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E7=A9=BAtemplate=E7=A1=AC=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generate/hooks/useBatchVariantPlans.ts | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useBatchVariantPlans.ts b/apps/web/src/pages/generate/hooks/useBatchVariantPlans.ts index 2be78fa8f..5581e84b9 100644 --- a/apps/web/src/pages/generate/hooks/useBatchVariantPlans.ts +++ b/apps/web/src/pages/generate/hooks/useBatchVariantPlans.ts @@ -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("") + /** 防止错误 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