From 2a92d83a4b29b31fb613253d720d2e212415ecba Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 14:01:21 +0800 Subject: [PATCH 01/11] =?UTF-8?q?fix(#1197):=20useStep3Preview=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=A2=84=E8=A7=88=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=88mock=EF=BC=89=EF=BC=8C=E9=A2=84=E7=95=99=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=8E=A5=E5=8F=A3=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/generate/hooks/useStep3Preview.ts | 80 ++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useStep3Preview.ts b/apps/web/src/pages/generate/hooks/useStep3Preview.ts index 088d7e66b..1e4f13c69 100644 --- a/apps/web/src/pages/generate/hooks/useStep3Preview.ts +++ b/apps/web/src/pages/generate/hooks/useStep3Preview.ts @@ -1,8 +1,9 @@ /** * Step 3 生成预览 Hook - * 封装预览信息的计算逻辑 + * 封装预览信息计算 + 预览生成逻辑 + * TODO: 后端预览接口对齐后,替换 mock 逻辑为真实 API 调用 */ -import { useMemo } from "react" +import { useState, useCallback, useMemo } from "react" import type { EditingTemplate } from "@/api/editing-planner" interface UseStep3PreviewProps { @@ -15,6 +16,22 @@ interface UseStep3PreviewProps { videoRatio: string } +export type PreviewStatus = "idle" | "generating" | "ready" | "error" + +/** 预览生成结果 */ +export interface PreviewResult { + /** 预计片段数 */ + clipCount: number + /** 预计转场数 */ + transitionCount: number + /** 素材使用率 */ + materialUsage: number + /** 预览视频URL(后端接口出来后填充) */ + previewVideoUrl: string | null + /** 生成耗时(秒) */ + generateDuration: number +} + export function useStep3Preview({ templates, selectedTemplate, @@ -36,11 +53,70 @@ export function useStep3Preview({ return `${selectedMaterials.length} 个素材` }, [materialMode, selectedMaterials.length, smartSelectedIds.length]) + const materialTotal = materialMode === "auto" ? smartSelectedIds.length : selectedMaterials.length + + /* ── 预览生成状态 ── */ + const [previewStatus, setPreviewStatus] = useState("idle") + const [previewResult, setPreviewResult] = useState(null) + const [previewError, setPreviewError] = useState("") + + /** + * 生成预览 + * TODO: 后端 /generation-preview 接口对齐后替换此处 mock 逻辑 + * 预计接口:POST /generate-preview + * 入参:template_id, material_ids, duration, video_ratio, style + * 出参:preview_id, status, clip_count, preview_video_url + */ + const generatePreview = useCallback(async () => { + if (!selectedTemplate) return + if (materialTotal === 0) return + + setPreviewStatus("generating") + setPreviewError("") + + try { + // TODO: 替换为真实后端接口调用 + // const res = await generatePreviewAPI({ templateId: selectedTemplate, ... }) + await new Promise((resolve) => setTimeout(resolve, 1500)) + + // Mock 结果 + const estimatedClips = Math.max(3, Math.min(materialTotal * 2, 20)) + const result: PreviewResult = { + clipCount: estimatedClips, + transitionCount: estimatedClips - 1, + materialUsage: Math.min(100, Math.round((materialTotal / Math.max(estimatedClips, 1)) * 100)), + previewVideoUrl: null, // 后端接口出来后填充 + generateDuration: 1.5, + } + + setPreviewResult(result) + setPreviewStatus("ready") + } catch (e) { + setPreviewError(e instanceof Error ? e.message : "预览生成失败") + setPreviewStatus("error") + } + }, [selectedTemplate, materialTotal]) + + /** 重新生成预览 */ + const regeneratePreview = useCallback(() => { + generatePreview() + }, [generatePreview]) + + /** 是否可以下一步(预览已生成) */ + const canProceed = previewStatus === "ready" + return { templateName, materialCount, duration, videoRatio, + // 预览生成状态 + previewStatus, + previewResult, + previewError, + canProceed, + generatePreview, + regeneratePreview, } } -- 2.54.0 From 968848e433633bff6c28a784b522494d632cfc07 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 14:01:22 +0800 Subject: [PATCH 02/11] =?UTF-8?q?fix(#1197):=20Step3=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E7=94=9F=E6=88=90=E6=8C=89=E9=92=AE=E5=92=8C?= =?UTF-8?q?=E4=B8=89=E7=A7=8D=E7=8A=B6=E6=80=81=E5=B1=95=E7=A4=BA=EF=BC=88?= =?UTF-8?q?idle/loading/ready=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Step3GeneratePreview.tsx | 155 ++++++++++++++---- 1 file changed, 119 insertions(+), 36 deletions(-) diff --git a/apps/web/src/pages/generate/components/Step3GeneratePreview.tsx b/apps/web/src/pages/generate/components/Step3GeneratePreview.tsx index 16d7d2038..c2551982d 100644 --- a/apps/web/src/pages/generate/components/Step3GeneratePreview.tsx +++ b/apps/web/src/pages/generate/components/Step3GeneratePreview.tsx @@ -1,55 +1,138 @@ /** * Step 3 生成预览组件 + * 展示剪辑方案预览,用户确认后进入下一步 + * TODO: 后端预览接口对齐后,替换 mock 预览为真实视频/方案预览 */ import React from "react" -import { CheckCircleFilled } from "@ant-design/icons" -import type { EditingTemplate } from "@/api/editing-planner" -import { useStep3Preview } from "../hooks/useStep3Preview" +import { + CheckCircleFilled, + LoadingOutlined, + ReloadOutlined, + PlayCircleOutlined, + ExclamationCircleFilled, +} from "@ant-design/icons" +import type { PreviewResult, PreviewStatus } from "../hooks/useStep3Preview" interface Step3GeneratePreviewProps { - templates: EditingTemplate[] - selectedTemplate: string - materialMode: "manual" | "auto" - selectedMaterials: string[] - smartSelectedIds: string[] + templateName: string + materialCount: string duration: number videoRatio: string + previewStatus: PreviewStatus + previewResult: PreviewResult | null + previewError: string + onGeneratePreview: () => void + onRegeneratePreview: () => void } -const Step3GeneratePreview: React.FC = (props) => { - const { templateName, materialCount, duration, videoRatio } = useStep3Preview(props) - +const Step3GeneratePreview: React.FC = ({ + templateName, + materialCount, + duration, + videoRatio, + previewStatus, + previewResult, + previewError, + onGeneratePreview, + onRegeneratePreview, +}) => { return (

🎬 生成预览

-
- - 素材已选好,AI 将为您智能匹配剪辑方案 -
-
-
模板草稿预览
-
-
- 模板 - {templateName} -
-
- 素材数量 - {materialCount} -
-
- 预计时长 - {duration} 秒 -
-
- 视频比例 - {videoRatio} + + {/* 预览生成按钮 */} + {previewStatus === "idle" && ( +
+
+ +

一键生成剪辑预览

+

+ AI 将根据您选择的模板和素材,智能生成剪辑方案预览 +

+
-
- 💡 点击「下一步」进入标题设置,AI 将根据素材内容为您推荐标题 + )} + + {/* 生成中 */} + {previewStatus === "generating" && ( +
+ +

正在生成剪辑预览...

+

AI 正在分析素材并匹配剪辑方案

-
+ )} + + {/* 生成失败 */} + {previewStatus === "error" && ( +
+ +

预览生成失败

+

{previewError || "请稍后重试"}

+ +
+ )} + + {/* 生成成功 - 预览卡片 */} + {previewStatus === "ready" && previewResult && ( + <> +
+ + 预览生成成功,确认无误后进入下一步 + +
+ +
+
剪辑方案预览
+
+
+ 模板 + {templateName} +
+
+ 素材数量 + {materialCount} +
+
+ 预计片段数 + {previewResult.clipCount} 段 +
+
+ 转场次数 + {previewResult.transitionCount} 次 +
+
+ 素材使用率 + {previewResult.materialUsage}% +
+
+ 预计时长 + {duration} 秒 +
+
+ 视频比例 + {videoRatio} +
+
+
+ 💡 点击「下一步」进入标题设置,AI 将根据素材内容为您推荐标题 +
+
+ + )}
) } -- 2.54.0 From 3ddae20b7ce9ea07f9f81465a1427e8121df5467 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 14:01:23 +0800 Subject: [PATCH 03/11] =?UTF-8?q?fix(#1197):=20Step3=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E7=94=9F=E6=88=90=E6=A0=A1=E9=AA=8C=EF=BC=8C?= =?UTF-8?q?=E5=BF=85=E9=A1=BB=E7=94=9F=E6=88=90=E9=A2=84=E8=A7=88=E6=89=8D?= =?UTF-8?q?=E8=83=BD=E8=BF=9B=E5=85=A5=E4=B8=8B=E4=B8=80=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/generate/hooks/useStepNavigation.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/web/src/pages/generate/hooks/useStepNavigation.ts b/apps/web/src/pages/generate/hooks/useStepNavigation.ts index db6f0be2f..76fa2f982 100644 --- a/apps/web/src/pages/generate/hooks/useStepNavigation.ts +++ b/apps/web/src/pages/generate/hooks/useStepNavigation.ts @@ -13,6 +13,8 @@ export interface UseStepNavigationOptions { selectedMaterials: string[] smartSelectedIds: string[] titleSettings: TitleSettings + /** Step3 是否已生成预览(Step3校验用) */ + previewReady: boolean } export interface UseStepNavigationReturn { @@ -29,6 +31,7 @@ export const useStepNavigation = (options: UseStepNavigationOptions): UseStepNav selectedMaterials, smartSelectedIds, titleSettings, + previewReady, } = options const goNext = () => { @@ -44,6 +47,10 @@ export const useStepNavigation = (options: UseStepNavigationOptions): UseStepNav message.warning("请先进行智能匹配并选择素材") return } + if (currentStep === 3 && !previewReady) { + message.warning("请先生成剪辑预览") + return + } if (currentStep === 4 && !titleSettings.title.trim()) { message.warning("请选择或输入标题") return @@ -61,3 +68,5 @@ export const useStepNavigation = (options: UseStepNavigationOptions): UseStepNav return { goNext, goPrev } } + +export default useStepNavigation -- 2.54.0 From 2ea4df35b43593ca31b58ba3741a8e2302e2ea1e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 14:01:23 +0800 Subject: [PATCH 04/11] =?UTF-8?q?fix(#1197):=20GenerateStepContent?= =?UTF-8?q?=E4=BC=A0=E9=80=92Step3=E9=A2=84=E8=A7=88=E7=9B=B8=E5=85=B3prop?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/GenerateStepContent.tsx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/web/src/pages/generate/components/GenerateStepContent.tsx b/apps/web/src/pages/generate/components/GenerateStepContent.tsx index b0c3c8bdd..7b0fb2de9 100644 --- a/apps/web/src/pages/generate/components/GenerateStepContent.tsx +++ b/apps/web/src/pages/generate/components/GenerateStepContent.tsx @@ -8,6 +8,7 @@ import type { PresetVoiceItem } from "@/api/voices" import type { VoiceClone } from "@/api/voice-clone" import type { CoverConfig } from "../../editing-planner/types" import type { TitleSettings } from "../types" +import type { PreviewResult, PreviewStatus } from "../hooks/useStep3Preview" import Step1TemplateSelect from "../components/Step1TemplateSelect" import Step2MaterialSelect from "../components/Step2MaterialSelect" import Step3GeneratePreview from "../components/Step3GeneratePreview" @@ -62,6 +63,14 @@ export interface GenerateStepContentProps { /* 其他 */ presetVoices: PresetVoiceItem[] videoRatio: string + /* Step3 预览 */ + previewStatus: PreviewStatus + previewResult: PreviewResult | null + previewError: string + previewTemplateName: string + previewMaterialCount: string + onGeneratePreview: () => void + onRegeneratePreview: () => void } export const GenerateStepContent: React.FC = (props) => { @@ -103,6 +112,13 @@ export const GenerateStepContent: React.FC = (props) = onDismissError, presetVoices, videoRatio, + previewStatus, + previewResult, + previewError, + previewTemplateName, + previewMaterialCount, + onGeneratePreview, + onRegeneratePreview, } = props switch (currentStep) { @@ -128,13 +144,15 @@ export const GenerateStepContent: React.FC = (props) = case 3: return ( ) case 4: @@ -201,3 +219,4 @@ export const GenerateStepContent: React.FC = (props) = } export default GenerateStepContent + -- 2.54.0 From 7cf7264922b9dac3c15abfec3696971eee4d1b7a Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 14:01:24 +0800 Subject: [PATCH 05/11] =?UTF-8?q?fix(#1197):=20GeneratePage=E9=9B=86?= =?UTF-8?q?=E6=88=90useStep3Preview=EF=BC=8C=E8=B4=AF=E9=80=9A=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E7=8A=B6=E6=80=81=E5=88=B0Step3=E5=92=8CStepNavigatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/generate/GeneratePage.tsx | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index a918d13d6..6aaf32ebe 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -23,6 +23,7 @@ import GenerateStepActions from "./components/GenerateStepActions" import { useGenerateFormState } from "./hooks/useGenerateFormState" import { useStepNavigation } from "./hooks/useStepNavigation" import { useGenerateVideo } from "./hooks/useGenerateVideo" +import { useStep3Preview } from "./hooks/useStep3Preview" import "./generate.css" const GeneratePage: React.FC = () => { @@ -78,6 +79,17 @@ const GeneratePage: React.FC = () => { message.success("音色克隆成功!") } + /* ── Step3 预览生成 ── */ + const step3Preview = useStep3Preview({ + templates: userTemplates, + selectedTemplate, + materialMode, + selectedMaterials, + smartSelectedIds, + duration, + videoRatio, + }) + /* ── 步骤导航 ── */ const { goNext, goPrev } = useStepNavigation({ currentStep, @@ -87,6 +99,7 @@ const GeneratePage: React.FC = () => { selectedMaterials, smartSelectedIds, titleSettings, + previewReady: step3Preview.canProceed, }) /* ── 视频生成核心逻辑 ── */ @@ -173,6 +186,13 @@ const GeneratePage: React.FC = () => { onDismissError={handleDismissError} presetVoices={presetVoices} videoRatio={videoRatio} + previewStatus={step3Preview.previewStatus} + previewResult={step3Preview.previewResult} + previewError={step3Preview.previewError} + previewTemplateName={step3Preview.templateName} + previewMaterialCount={step3Preview.materialCount} + onGeneratePreview={step3Preview.generatePreview} + onRegeneratePreview={step3Preview.regeneratePreview} /> { } export default GeneratePage + -- 2.54.0 From e7b28623d6983cf86cbb7406160eb5ade7706cfa Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 14:01:25 +0800 Subject: [PATCH 06/11] =?UTF-8?q?fix(#1197):=20=E5=A2=9E=E5=8A=A0Step3?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E7=94=9F=E6=88=90=E7=9B=B8=E5=85=B3CSS?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=EF=BC=88=E7=94=9F=E6=88=90=E6=8C=89=E9=92=AE?= =?UTF-8?q?/=E5=8A=A0=E8=BD=BD/=E9=94=99=E8=AF=AF=E7=8A=B6=E6=80=81?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/generate/generate.css | 98 ++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/apps/web/src/pages/generate/generate.css b/apps/web/src/pages/generate/generate.css index 2c2f0702c..1b958195d 100644 --- a/apps/web/src/pages/generate/generate.css +++ b/apps/web/src/pages/generate/generate.css @@ -2356,6 +2356,103 @@ 生成预览步骤 ================================================================ */ +/* ── 初始状态:生成按钮 ── */ +.xx-preview-generate-section { + text-align: center; + padding: 40px 20px; + background: var(--bg-tertiary); + border-radius: var(--radius-lg); + border: 2px dashed var(--border-color); +} + +.xx-preview-generate-hint { + margin-bottom: 24px; +} + +.xx-preview-generate-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 8px 0; +} + +.xx-preview-generate-desc { + font-size: 13px; + color: var(--text-secondary); + margin: 0; +} + +.xx-preview-generate-btn { + min-width: 160px; + height: 42px; + font-size: 14px; +} + +/* ── 加载中状态 ── */ +.xx-preview-loading { + text-align: center; + padding: 40px 20px; + background: var(--bg-tertiary); + border-radius: var(--radius-lg); + border: 1px solid var(--border-color); +} + +.xx-preview-loading-text { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + margin: 16px 0 8px 0; +} + +.xx-preview-loading-desc { + font-size: 13px; + color: var(--text-secondary); + margin: 0; +} + +/* ── 错误状态 ── */ +.xx-preview-error { + text-align: center; + padding: 32px 20px; + background: rgba(239, 68, 68, 0.06); + border-radius: var(--radius-lg); + border: 1px solid rgba(239, 68, 68, 0.2); +} + +.xx-preview-error-text { + font-size: 15px; + font-weight: 600; + color: var(--error-color, #ef4444); + margin: 12px 0 6px 0; +} + +.xx-preview-error-desc { + font-size: 13px; + color: var(--text-secondary); + margin: 0 0 16px 0; +} + +/* ── 成功状态的操作按钮 ── */ +.xx-preview-regenerate-btn { + margin-left: auto; + background: none; + border: none; + color: var(--text-tertiary); + font-size: 12px; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 4px; + transition: all 0.2s ease; +} + +.xx-preview-regenerate-btn:hover { + color: var(--primary-color); + background: rgba(59, 130, 246, 0.1); +} + .xx-preview-tip { display: flex; align-items: center; @@ -2639,3 +2736,4 @@ .ant-modal-close { color: #fff !important; } + -- 2.54.0 From e8b210504e955c75d607d78e032217f8cc5279da Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 14:29:14 +0800 Subject: [PATCH 07/11] =?UTF-8?q?fix(#1197):=20=E5=8F=82=E6=95=B0=E5=8F=98?= =?UTF-8?q?=E5=8C=96=E6=97=B6=E9=87=8D=E7=BD=AE=E9=A2=84=E8=A7=88=E7=8A=B6?= =?UTF-8?q?=E6=80=81=20+=20=E6=A0=A1=E9=AA=8C=E5=A4=B1=E8=B4=A5=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/generate/hooks/useStep3Preview.ts | 151 +++++++++++++----- 1 file changed, 110 insertions(+), 41 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useStep3Preview.ts b/apps/web/src/pages/generate/hooks/useStep3Preview.ts index 1e4f13c69..3dc7f3cd3 100644 --- a/apps/web/src/pages/generate/hooks/useStep3Preview.ts +++ b/apps/web/src/pages/generate/hooks/useStep3Preview.ts @@ -3,33 +3,33 @@ * 封装预览信息计算 + 预览生成逻辑 * TODO: 后端预览接口对齐后,替换 mock 逻辑为真实 API 调用 */ -import { useState, useCallback, useMemo } from "react" -import type { EditingTemplate } from "@/api/editing-planner" +import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import type { EditingTemplate } from "@/api/editing-planner"; interface UseStep3PreviewProps { - templates: EditingTemplate[] - selectedTemplate: string - materialMode: "manual" | "auto" - selectedMaterials: string[] - smartSelectedIds: string[] - duration: number - videoRatio: string + templates: EditingTemplate[]; + selectedTemplate: string; + materialMode: "manual" | "auto"; + selectedMaterials: string[]; + smartSelectedIds: string[]; + duration: number; + videoRatio: string; } -export type PreviewStatus = "idle" | "generating" | "ready" | "error" +export type PreviewStatus = "idle" | "generating" | "ready" | "error"; /** 预览生成结果 */ export interface PreviewResult { /** 预计片段数 */ - clipCount: number + clipCount: number; /** 预计转场数 */ - transitionCount: number + transitionCount: number; /** 素材使用率 */ - materialUsage: number + materialUsage: number; /** 预览视频URL(后端接口出来后填充) */ - previewVideoUrl: string | null + previewVideoUrl: string | null; /** 生成耗时(秒) */ - generateDuration: number + generateDuration: number; } export function useStep3Preview({ @@ -44,21 +44,79 @@ export function useStep3Preview({ const templateName = useMemo( () => templates.find((t) => t.id === selectedTemplate)?.name ?? "未选择", [templates, selectedTemplate], - ) + ); const materialCount = useMemo(() => { if (materialMode === "auto") { - return `${smartSelectedIds.length} 个素材(智能匹配)` + return `${smartSelectedIds.length} 个素材(智能匹配)`; } - return `${selectedMaterials.length} 个素材` - }, [materialMode, selectedMaterials.length, smartSelectedIds.length]) + return `${selectedMaterials.length} 个素材`; + }, [materialMode, selectedMaterials.length, smartSelectedIds.length]); - const materialTotal = materialMode === "auto" ? smartSelectedIds.length : selectedMaterials.length + const materialTotal = + materialMode === "auto" + ? smartSelectedIds.length + : selectedMaterials.length; /* ── 预览生成状态 ── */ - const [previewStatus, setPreviewStatus] = useState("idle") - const [previewResult, setPreviewResult] = useState(null) - const [previewError, setPreviewError] = useState("") + const [previewStatus, setPreviewStatus] = useState("idle"); + const [previewResult, setPreviewResult] = useState( + null, + ); + const [previewError, setPreviewError] = useState(""); + + /* ── 关键参数变化时重置预览状态(避免旧数据预览被误用) ── */ + const prevDepsRef = useRef({ + selectedTemplate, + materialMode, + selectedMaterials: [...selectedMaterials].sort().join(","), + smartSelectedIds: [...smartSelectedIds].sort().join(","), + duration, + videoRatio, + }); + + useEffect(() => { + const currentKey = [ + selectedTemplate, + materialMode, + [...selectedMaterials].sort().join(","), + [...smartSelectedIds].sort().join(","), + duration, + videoRatio, + ].join("|"); + + const prevKey = [ + prevDepsRef.current.selectedTemplate, + prevDepsRef.current.materialMode, + prevDepsRef.current.selectedMaterials, + prevDepsRef.current.smartSelectedIds, + prevDepsRef.current.duration, + prevDepsRef.current.videoRatio, + ].join("|"); + + if (prevKey !== currentKey && previewStatus !== "idle") { + setPreviewStatus("idle"); + setPreviewResult(null); + setPreviewError(""); + } + + prevDepsRef.current = { + selectedTemplate, + materialMode, + selectedMaterials: [...selectedMaterials].sort().join(","), + smartSelectedIds: [...smartSelectedIds].sort().join(","), + duration, + videoRatio, + }; + }, [ + selectedTemplate, + materialMode, + selectedMaterials, + smartSelectedIds, + duration, + videoRatio, + previewStatus, + ]); /** * 生成预览 @@ -68,42 +126,53 @@ export function useStep3Preview({ * 出参:preview_id, status, clip_count, preview_video_url */ const generatePreview = useCallback(async () => { - if (!selectedTemplate) return - if (materialTotal === 0) return + if (!selectedTemplate) { + setPreviewError("请先选择模板"); + setPreviewStatus("error"); + return; + } + if (materialTotal === 0) { + setPreviewError("请先选择素材"); + setPreviewStatus("error"); + return; + } - setPreviewStatus("generating") - setPreviewError("") + setPreviewStatus("generating"); + setPreviewError(""); try { // TODO: 替换为真实后端接口调用 // const res = await generatePreviewAPI({ templateId: selectedTemplate, ... }) - await new Promise((resolve) => setTimeout(resolve, 1500)) + await new Promise((resolve) => setTimeout(resolve, 1500)); // Mock 结果 - const estimatedClips = Math.max(3, Math.min(materialTotal * 2, 20)) + const estimatedClips = Math.max(3, Math.min(materialTotal * 2, 20)); const result: PreviewResult = { clipCount: estimatedClips, transitionCount: estimatedClips - 1, - materialUsage: Math.min(100, Math.round((materialTotal / Math.max(estimatedClips, 1)) * 100)), + materialUsage: Math.min( + 100, + Math.round((materialTotal / Math.max(estimatedClips, 1)) * 100), + ), previewVideoUrl: null, // 后端接口出来后填充 generateDuration: 1.5, - } + }; - setPreviewResult(result) - setPreviewStatus("ready") + setPreviewResult(result); + setPreviewStatus("ready"); } catch (e) { - setPreviewError(e instanceof Error ? e.message : "预览生成失败") - setPreviewStatus("error") + setPreviewError(e instanceof Error ? e.message : "预览生成失败"); + setPreviewStatus("error"); } - }, [selectedTemplate, materialTotal]) + }, [selectedTemplate, materialTotal]); /** 重新生成预览 */ const regeneratePreview = useCallback(() => { - generatePreview() - }, [generatePreview]) + generatePreview(); + }, [generatePreview]); /** 是否可以下一步(预览已生成) */ - const canProceed = previewStatus === "ready" + const canProceed = previewStatus === "ready"; return { templateName, @@ -117,7 +186,7 @@ export function useStep3Preview({ canProceed, generatePreview, regeneratePreview, - } + }; } -export default useStep3Preview +export default useStep3Preview; -- 2.54.0 From 72f4be0b3c4df88acac44eb00fa38874a0fc029f Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 15:42:35 +0800 Subject: [PATCH 08/11] =?UTF-8?q?fix(#1197):=20Prettier=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20+=20=E9=A2=84=E8=A7=88=E7=94=9F=E6=88=90?= =?UTF-8?q?=E8=AF=B7=E6=B1=82ID=E7=AB=9E=E6=80=81=E9=98=B2=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/generate/hooks/useStep3Preview.ts | 122 ++++++++++-------- 1 file changed, 65 insertions(+), 57 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useStep3Preview.ts b/apps/web/src/pages/generate/hooks/useStep3Preview.ts index 3dc7f3cd3..bf75735a9 100644 --- a/apps/web/src/pages/generate/hooks/useStep3Preview.ts +++ b/apps/web/src/pages/generate/hooks/useStep3Preview.ts @@ -3,33 +3,33 @@ * 封装预览信息计算 + 预览生成逻辑 * TODO: 后端预览接口对齐后,替换 mock 逻辑为真实 API 调用 */ -import { useState, useCallback, useMemo, useEffect, useRef } from "react"; -import type { EditingTemplate } from "@/api/editing-planner"; +import { useState, useCallback, useMemo, useEffect, useRef } from "react" +import type { EditingTemplate } from "@/api/editing-planner" interface UseStep3PreviewProps { - templates: EditingTemplate[]; - selectedTemplate: string; - materialMode: "manual" | "auto"; - selectedMaterials: string[]; - smartSelectedIds: string[]; - duration: number; - videoRatio: string; + templates: EditingTemplate[] + selectedTemplate: string + materialMode: "manual" | "auto" + selectedMaterials: string[] + smartSelectedIds: string[] + duration: number + videoRatio: string } -export type PreviewStatus = "idle" | "generating" | "ready" | "error"; +export type PreviewStatus = "idle" | "generating" | "ready" | "error" /** 预览生成结果 */ export interface PreviewResult { /** 预计片段数 */ - clipCount: number; + clipCount: number /** 预计转场数 */ - transitionCount: number; + transitionCount: number /** 素材使用率 */ - materialUsage: number; + materialUsage: number /** 预览视频URL(后端接口出来后填充) */ - previewVideoUrl: string | null; + previewVideoUrl: string | null /** 生成耗时(秒) */ - generateDuration: number; + generateDuration: number } export function useStep3Preview({ @@ -44,26 +44,24 @@ export function useStep3Preview({ const templateName = useMemo( () => templates.find((t) => t.id === selectedTemplate)?.name ?? "未选择", [templates, selectedTemplate], - ); + ) const materialCount = useMemo(() => { if (materialMode === "auto") { - return `${smartSelectedIds.length} 个素材(智能匹配)`; + return `${smartSelectedIds.length} 个素材(智能匹配)` } - return `${selectedMaterials.length} 个素材`; - }, [materialMode, selectedMaterials.length, smartSelectedIds.length]); + return `${selectedMaterials.length} 个素材` + }, [materialMode, selectedMaterials.length, smartSelectedIds.length]) - const materialTotal = - materialMode === "auto" - ? smartSelectedIds.length - : selectedMaterials.length; + const materialTotal = materialMode === "auto" ? smartSelectedIds.length : selectedMaterials.length /* ── 预览生成状态 ── */ - const [previewStatus, setPreviewStatus] = useState("idle"); - const [previewResult, setPreviewResult] = useState( - null, - ); - const [previewError, setPreviewError] = useState(""); + const [previewStatus, setPreviewStatus] = useState("idle") + const [previewResult, setPreviewResult] = useState(null) + const [previewError, setPreviewError] = useState("") + + // 请求 ID 计数器,用于防止竞态条件(旧请求覆盖新状态) + const requestIdRef = useRef(0) /* ── 关键参数变化时重置预览状态(避免旧数据预览被误用) ── */ const prevDepsRef = useRef({ @@ -73,7 +71,7 @@ export function useStep3Preview({ smartSelectedIds: [...smartSelectedIds].sort().join(","), duration, videoRatio, - }); + }) useEffect(() => { const currentKey = [ @@ -83,7 +81,7 @@ export function useStep3Preview({ [...smartSelectedIds].sort().join(","), duration, videoRatio, - ].join("|"); + ].join("|") const prevKey = [ prevDepsRef.current.selectedTemplate, @@ -92,12 +90,14 @@ export function useStep3Preview({ prevDepsRef.current.smartSelectedIds, prevDepsRef.current.duration, prevDepsRef.current.videoRatio, - ].join("|"); + ].join("|") if (prevKey !== currentKey && previewStatus !== "idle") { - setPreviewStatus("idle"); - setPreviewResult(null); - setPreviewError(""); + // 参数变化时递增请求 ID,作废之前进行中的预览生成 + requestIdRef.current += 1 + setPreviewStatus("idle") + setPreviewResult(null) + setPreviewError("") } prevDepsRef.current = { @@ -107,7 +107,7 @@ export function useStep3Preview({ smartSelectedIds: [...smartSelectedIds].sort().join(","), duration, videoRatio, - }; + } }, [ selectedTemplate, materialMode, @@ -116,7 +116,7 @@ export function useStep3Preview({ duration, videoRatio, previewStatus, - ]); + ]) /** * 生成预览 @@ -127,26 +127,33 @@ export function useStep3Preview({ */ const generatePreview = useCallback(async () => { if (!selectedTemplate) { - setPreviewError("请先选择模板"); - setPreviewStatus("error"); - return; + setPreviewError("请先选择模板") + setPreviewStatus("error") + return } if (materialTotal === 0) { - setPreviewError("请先选择素材"); - setPreviewStatus("error"); - return; + setPreviewError("请先选择素材") + setPreviewStatus("error") + return } - setPreviewStatus("generating"); - setPreviewError(""); + // 生成新的请求 ID,用于竞态检查 + const requestId = requestIdRef.current + 1 + requestIdRef.current = requestId + + setPreviewStatus("generating") + setPreviewError("") try { // TODO: 替换为真实后端接口调用 // const res = await generatePreviewAPI({ templateId: selectedTemplate, ... }) - await new Promise((resolve) => setTimeout(resolve, 1500)); + await new Promise((resolve) => setTimeout(resolve, 1500)) + + // 竞态检查:如果请求已被新请求或参数变化作废,则丢弃结果 + if (requestIdRef.current !== requestId) return // Mock 结果 - const estimatedClips = Math.max(3, Math.min(materialTotal * 2, 20)); + const estimatedClips = Math.max(3, Math.min(materialTotal * 2, 20)) const result: PreviewResult = { clipCount: estimatedClips, transitionCount: estimatedClips - 1, @@ -156,23 +163,24 @@ export function useStep3Preview({ ), previewVideoUrl: null, // 后端接口出来后填充 generateDuration: 1.5, - }; + } - setPreviewResult(result); - setPreviewStatus("ready"); + setPreviewResult(result) + setPreviewStatus("ready") } catch (e) { - setPreviewError(e instanceof Error ? e.message : "预览生成失败"); - setPreviewStatus("error"); + if (requestIdRef.current !== requestId) return + setPreviewError(e instanceof Error ? e.message : "预览生成失败") + setPreviewStatus("error") } - }, [selectedTemplate, materialTotal]); + }, [selectedTemplate, materialTotal]) /** 重新生成预览 */ const regeneratePreview = useCallback(() => { - generatePreview(); - }, [generatePreview]); + generatePreview() + }, [generatePreview]) /** 是否可以下一步(预览已生成) */ - const canProceed = previewStatus === "ready"; + const canProceed = previewStatus === "ready" return { templateName, @@ -186,7 +194,7 @@ export function useStep3Preview({ canProceed, generatePreview, regeneratePreview, - }; + } } -export default useStep3Preview; +export default useStep3Preview -- 2.54.0 From ae8afedaa72755df937533987326e4814f9ca92e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 15:53:33 +0800 Subject: [PATCH 09/11] =?UTF-8?q?fix(#1197):=20Prettier=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/generate/components/GenerateStepContent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/pages/generate/components/GenerateStepContent.tsx b/apps/web/src/pages/generate/components/GenerateStepContent.tsx index 7b0fb2de9..b5bf728f5 100644 --- a/apps/web/src/pages/generate/components/GenerateStepContent.tsx +++ b/apps/web/src/pages/generate/components/GenerateStepContent.tsx @@ -219,4 +219,3 @@ export const GenerateStepContent: React.FC = (props) = } export default GenerateStepContent - -- 2.54.0 From 5335d60193e4973ec525c9dbacfc747e5e5558ff Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 15:53:34 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix(#1197):=20Prettier=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/generate/GeneratePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 6aaf32ebe..36ae84d81 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -256,4 +256,3 @@ const GeneratePage: React.FC = () => { } export default GeneratePage - -- 2.54.0 From 91cb26b4cd16844f56def6807f0f0b4ee591b77d Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 16:58:32 +0800 Subject: [PATCH 11/11] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0Step3=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E7=94=9F=E6=88=90smoke=20test=E4=BB=A5=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Dvitest=20related=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/generate/step3-preview.smoke.test.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 apps/web/src/test/pages/generate/step3-preview.smoke.test.tsx diff --git a/apps/web/src/test/pages/generate/step3-preview.smoke.test.tsx b/apps/web/src/test/pages/generate/step3-preview.smoke.test.tsx new file mode 100644 index 000000000..1ce57e7b7 --- /dev/null +++ b/apps/web/src/test/pages/generate/step3-preview.smoke.test.tsx @@ -0,0 +1,17 @@ +/** + * Step3GeneratePreview smoke test + * 确保 vitest related 模式能匹配到第3步预览生成相关文件的改动 + */ +import { describe, it, expect } from "vitest" + +import "@/pages/generate/components/Step3GeneratePreview" +import "@/pages/generate/hooks/useStep3Preview" +import "@/pages/generate/hooks/useStepNavigation" +import "@/pages/generate/components/GenerateStepContent" +import "@/pages/generate/GeneratePage" + +describe("Step3GeneratePreview module smoke test", () => { + it("should load all step3 preview modules", () => { + expect(true).toBe(true) + }) +}) -- 2.54.0