diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 85e764d2a..c93fc64bf 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -21,6 +21,7 @@ import GenerateStepActions from "./components/GenerateStepActions" import { useGenerateFormState } from "./hooks/useGenerateFormState" import { useStepNavigation } from "./hooks/useStepNavigation" import { useGenerateVideo } from "./hooks/useGenerateVideo" +import { confirmGeneration } from "@/api/generation/confirm" import { useBatchVariantPlans } from "./hooks/useBatchVariantPlans" import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters" @@ -139,6 +140,9 @@ const GeneratePage: React.FC = () => { /* ── 数量选择弹窗 ── */ const [countModalOpen, setCountModalOpen] = useState(false) + /* ── Step5 保存中状态 ── */ + const [finishing, setFinishing] = useState(false) + /* ── #1970 流程重构:分支弹窗 ── */ const [voiceModalOpen, setVoiceModalOpen] = useState(false) const [scriptModalOpen, setScriptModalOpen] = useState(false) @@ -404,9 +408,81 @@ const GeneratePage: React.FC = () => { }, }) - /* ── 最终成片 ── */ + /* ── 最终成片(单视频) ── */ const finalVideo = generatedVideos[0] + /* ── Step5 完成:调用 confirm 入库 + 跳转 ── */ + const handleFinish = useCallback(async () => { + if (finishing) return + // 校验:单视频必须已生成;批量必须所有已选视频有封面或确认跳过 + if (isBatch) { + if (generatedVideos.length === 0) { + message.warning("请等待视频生成完成") + return + } + } else { + if (!finalVideo) { + message.warning("请等待视频生成完成") + return + } + } + setFinishing(true) + const hide = message.loading("正在保存到视频库...", 0) + try { + const taskIds = + batchTasks && batchTasks.length > 0 + ? batchTasks.map((t) => t.taskId).filter(Boolean) + : finalVideo?.generation_task_id + ? [finalVideo.generation_task_id] + : [] + + // 单视频/批量:为每个任务调用 confirm(传入封面) + if (isBatch && previewCovers.length > 0) { + await Promise.all( + taskIds.map(async (taskId, idx) => { + const coverUrl = previewCovers[idx] || "" + return confirmGeneration(taskId, { + cover_url: coverUrl || undefined, + custom_title: previewTitles[idx] || titleSettings.title || "", + }) + }), + ) + } else if (finalVideo?.generation_task_id) { + const coverUrl = coverSettings.thumbnail_url || coverSettings.upload_url || "" + await confirmGeneration(finalVideo.generation_task_id, { + cover_url: coverUrl || undefined, + custom_title: titleSettings.title || "", + }) + } + hide() + message.success("已保存到视频库") + navigate("/app/products") + } catch (err) { + hide() + console.error("[保存失败]", err) + const detail = + (err as { response?: { data?: { detail?: string; message?: string } } })?.response?.data + ?.detail || + (err as { response?: { data?: { detail?: string; message?: string } } })?.response?.data + ?.message || + "保存失败,请稍后在任务历史查看" + message.error(detail) + } finally { + setFinishing(false) + } + }, [ + finishing, + isBatch, + finalVideo, + generatedVideos, + batchTasks, + previewCovers, + previewTitles, + titleSettings.title, + coverSettings, + navigate, + ]) + /* ── 布局 class ── */ const layoutClassName = "xx-generate-layout full-width" @@ -480,9 +556,6 @@ const GeneratePage: React.FC = () => { onPreviewCountChange={setPreviewCount} videoRatio={videoRatio as "9:16" | "16:9"} onVideoRatioChange={(r) => setVideoRatio(r)} - selectedScript={selectedScript} - ttsVoiceId={ttsVoiceId} - ttsVoiceSource={ttsVoiceSource} onSelectedVoiceChange={setSelectedVoice} generating={generating} generated={generated} @@ -565,12 +638,6 @@ const GeneratePage: React.FC = () => { - @@ -590,6 +657,8 @@ const GeneratePage: React.FC = () => { pointsInsufficient={insufficientPoints} insufficientReason={pointsEstimate.reason} onRecharge={() => navigate("/app/points/recharge")} + onFinish={handleFinish} + finishing={finishing} /> diff --git a/apps/web/src/pages/generate/components/GenerateStepActions.tsx b/apps/web/src/pages/generate/components/GenerateStepActions.tsx index 2f6c5660b..42c950c4d 100644 --- a/apps/web/src/pages/generate/components/GenerateStepActions.tsx +++ b/apps/web/src/pages/generate/components/GenerateStepActions.tsx @@ -5,7 +5,7 @@ * 步骤 1~3:上一步 / 下一步 * 步骤 4(确认生成/进度):未开始 →「✨ 确认生成视频」;生成中 →「⏳ 视频渲染中…」; * 失败 →「🔄 重新生成」;全部完成 →「下一步:选择封面 →」 - * 步骤 5(选择封面):仅上一步,无主按钮 + * 步骤 5(选择封面):上一步 + 完成按钮 */ import React from "react" import { Tooltip } from "antd" @@ -32,6 +32,10 @@ export interface GenerateStepActionsProps { freeClipsUsedThisTime?: number /** 前往充值 */ onRecharge?: () => void + /** Step5 点击完成(保存入库并跳转) */ + onFinish?: () => void | Promise + /** Step5 保存中 */ + finishing?: boolean } const GenerateStepActions: React.FC = ({ @@ -48,6 +52,8 @@ const GenerateStepActions: React.FC = ({ insufficientReason, freeClipsUsedThisTime, onRecharge, + onFinish, + finishing, }) => { const renderPrimaryButton = () => { /* 步骤 1~3:上一步 / 下一步 */ @@ -126,8 +132,18 @@ const GenerateStepActions: React.FC = ({ ) } - /* 步骤 5(封面,最后一步):无主按钮 */ - return null + /* 步骤 5(封面,最后一步):完成按钮 */ + return ( + + ) } return ( diff --git a/apps/web/src/pages/generate/components/GenerateStepContent.tsx b/apps/web/src/pages/generate/components/GenerateStepContent.tsx index f3dacfef2..cebdbd309 100644 --- a/apps/web/src/pages/generate/components/GenerateStepContent.tsx +++ b/apps/web/src/pages/generate/components/GenerateStepContent.tsx @@ -6,7 +6,6 @@ import React from "react" import type { CoverConfig } from "../types/cover" import type { TitleSettings } from "../types" -import type { ScriptItem } from "@/api/scripts" import Step1EditMode from "./Step1EditMode" import type { EditMode } from "./Step1EditMode" import Step2MaterialSelect from "../components/Step2MaterialSelect" @@ -90,10 +89,6 @@ export interface GenerateStepContentProps { previewCovers: string[] onPreviewCoversChange: (urls: string[]) => void selectedVariantIds?: number[] - /* ── 摘要信息(#1970 Step4 展示用) ── */ - selectedScript: ScriptItem | null - ttsVoiceId: string - ttsVoiceSource: "preset" | "clone" } export const GenerateStepContent: React.FC = (props) => { @@ -147,9 +142,6 @@ export const GenerateStepContent: React.FC = (props) = previewCovers, onPreviewCoversChange, selectedVariantIds, - selectedScript, - ttsVoiceId, - ttsVoiceSource, } = props switch (currentStep) { @@ -208,40 +200,6 @@ export const GenerateStepContent: React.FC = (props) = case 4: return (
- {/* 配置摘要(#1970) */} -
-
- 📋 生成配置 -
-
🎬 剪辑模式:{editMode === "random" ? "🎲 随机混剪" : "📖 叙事剪辑"}
- {editMode === "random" ? ( -
🎙️ 配音来源:配音库音频
- ) : ( - <> -
📝 文案:{selectedScript?.title ?? "未选择"}
-
- 🎙️ 合成配音音色: - {ttsVoiceId - ? `${ttsVoiceSource === "clone" ? "克隆音色" : "系统音色"}(${ttsVoiceId.slice(0, 8)}...)` - : "未选择"} -
- - )} -
📱 视频比例:{videoRatio}
-
🎯 智能降重:{dedupEnabled ? "已开启" : "已关闭"}
- {previewCount > 1 &&
📦 生成数量:{previewCount} 个
} -
- {previewCount > 1 ? ( void - /** 当前选中的模板 ID */ + /** 当前选中的视频模板 ID(封面模板由本地 state 管理) */ selectedTemplate?: string /** Step4 标题设置,用于封面叠加标题 */ titleSettings?: TitleSettings @@ -30,7 +30,7 @@ interface UseStep6CoverProps { export function useStep6Cover({ coverSettings, onCoverSettingsChange, - selectedTemplate = "", + selectedTemplate: _selectedTemplate = "", titleSettings, generatedVideos, }: UseStep6CoverProps) { @@ -79,7 +79,8 @@ export function useStep6Cover({ return } - if (!selectedTemplate) { + const activeCoverTemplateId = selectedTemplateId || "default" + if (!activeCoverTemplateId) { message.error("请先选择模板") return } @@ -95,7 +96,7 @@ export function useStep6Cover({ }, 300000) try { - const response = await generateCover(selectedTemplate, { + const response = await generateCover(activeCoverTemplateId, { generated_video_id: finalVideo.id, video_url: finalVideo.file_url || finalVideo.download_url || "", cover_type: "ai_frame", @@ -156,7 +157,7 @@ export function useStep6Cover({ setGenerating(false) } }, [ - selectedTemplate, + selectedTemplateId, finalVideo, coverSettings, onCoverSettingsChange,