From 3275a6b8c38fde93ae9afeea45e29e8d7dc72fe3 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 15 Sep 2026 00:48:18 +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 --- .../pages/generate/hooks/useStep2Materials.ts | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useStep2Materials.ts b/apps/web/src/pages/generate/hooks/useStep2Materials.ts index 06143fa22..80a1fafc8 100644 --- a/apps/web/src/pages/generate/hooks/useStep2Materials.ts +++ b/apps/web/src/pages/generate/hooks/useStep2Materials.ts @@ -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>() const clipsAbortRef = useRef(null) @@ -95,10 +100,11 @@ export function useStep2Materials({ selectedTemplateRef.current = selectedTemplate const onServerClipsChangeRef = useRef(onServerClipsChange) onServerClipsChangeRef.current = onServerClipsChange + /** 记录后端兜底返回的新 template_id(#1913) */ + const fallbackTidRef = useRef("") 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)