diff --git a/apps/web/src/api/template-editor/types.ts b/apps/web/src/api/template-editor/types.ts index 734340628..40c806a21 100644 --- a/apps/web/src/api/template-editor/types.ts +++ b/apps/web/src/api/template-editor/types.ts @@ -114,8 +114,6 @@ export interface EditPlanConfig { auto_subtitles?: boolean /** 是否启用 BGM */ bgm?: boolean - /** 生成数量 */ - generate_count?: number /** 素材模式 */ material_mode?: string /** 前端标题设置(Step4 自动保存,与 title_config 字段分离,不影响后端渲染) */ diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 1e35f0b31..a53592397 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -4,15 +4,14 @@ * 左右布局:左侧 generate-form + 右侧 generate-preview * * 架构: - * - Step4+ 右侧预览面板使用 FrontendPreviewPlayer 实时播放素材片段 - * - 标题样式编辑时 CSS 层实时叠加预览,所见即所得 + * - 右侧展示生成的视频,内联播放 + * - * - 点"确认生成"时调用 createGenerationTask 创建一次服务器渲染任务 */ -import React, { useMemo, useState, useEffect, useRef } from "react" -import { Modal, message } from "antd" +import React, { useMemo } from "react" +import { message } from "antd" import { useNavigate } from "react-router-dom" import type { VoiceClone } from "@/api/voice-clone" -import { useQuery } from "@tanstack/react-query" import { useCloneProgress } from "@/hooks/useCloneProgress" import CloneModal from "@/components/voice/CloneModal" import GenerateHeader from "./components/GenerateHeader" @@ -21,8 +20,6 @@ import { estimateTotalVideoDuration, } from "./utils/calculateTotalVideoDuration" import GenerateStepsBar from "./components/GenerateStepsBar" -import GenerateResultPanel from "./components/GenerateResultPanel" -import FrontendPreviewPlayer from "./components/FrontendPreviewPlayer" import GenerateStepContent from "./components/GenerateStepContent" import GenerateStepActions from "./components/GenerateStepActions" import { useGenerateFormState } from "./hooks/useGenerateFormState" @@ -30,8 +27,6 @@ import { useStepNavigation } from "./hooks/useStepNavigation" import { useGenerateVideo } from "./hooks/useGenerateVideo" import { usePreviewAssets } from "./hooks/usePreviewAssets" import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters" -import { getAssetsByKind } from "@/api/assets" -import { previewTts } from "@/api/tts" import "./generate.css" const GeneratePage: React.FC = () => { @@ -64,8 +59,6 @@ const GeneratePage: React.FC = () => { presetVoices, cloneModalOpen, setCloneModalOpen, - generateCount, - setGenerateCount, videoRatio, duration, style, @@ -73,10 +66,6 @@ const GeneratePage: React.FC = () => { bgm, editPlanId, sourceEditPlanId, - previewVideo, - setPreviewVideo, - previewModalOpen, - setPreviewModalOpen, previewTaskId, setPreviewTaskId, storedSourceEditPlanId, @@ -89,59 +78,6 @@ const GeneratePage: React.FC = () => { onTitleSettingsChange: setTitleSettings, }) - /* ── 配音预览音频(TTS 试听)── */ - const { data: voiceMaterials = [] } = useQuery({ - queryKey: ["assets", "voice"], - queryFn: () => getAssetsByKind("voice", { limit: 50 }), - }) - - const [previewVoiceAudioUrl, setPreviewVoiceAudioUrl] = useState(null) - const ttsAbortRef = useRef(null) - - useEffect(() => { - // 如果 selectedVoice 是已上传的配音素材,直接用 file_url - const voiceAsset = voiceMaterials.find((m) => m.id === selectedVoice) - if (voiceAsset?.file_url) { - setPreviewVoiceAudioUrl(voiceAsset.file_url) - return - } - - // 没有选中的 voice 或标题,跳过 - const voiceId = selectedClonedVoice || selectedVoice - if (!voiceId || !titleSettings.title) { - setPreviewVoiceAudioUrl(null) - return - } - - // 预设音色 / 克隆音色 → 调 TTS 合成 - ttsAbortRef.current?.abort() - const controller = new AbortController() - ttsAbortRef.current = controller - let cancelled = false - - previewTts({ - text: titleSettings.title, - voice_id: voiceId, - }) - .then((res) => { - if (!cancelled && res.audio_url) { - setPreviewVoiceAudioUrl(res.audio_url) - } - }) - .catch((err) => { - if (!cancelled) { - console.warn("[预览配音生成失败]", err) - setPreviewVoiceAudioUrl(null) - } - }) - - return () => { - cancelled = true - controller.abort() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedVoice, selectedClonedVoice, titleSettings.title, voiceMaterials]) - /* ── 克隆声音 ── */ const { clones: clonedVoices, addClone, hasProcessing } = useCloneProgress() @@ -221,7 +157,6 @@ const GeneratePage: React.FC = () => { duration, autoSubtitles, bgm, - generateCount, sourceEditPlanId: storedSourceEditPlanId || sourceEditPlanId, previewTaskId, bgmConfig, @@ -286,8 +221,6 @@ const GeneratePage: React.FC = () => { hasProcessing={hasProcessing} cloneModalOpen={cloneModalOpen} onCloneModalOpenChange={setCloneModalOpen} - generateCount={generateCount} - onGenerateCountChange={setGenerateCount} generating={generating} generated={generated} generateError={generateError} @@ -309,70 +242,99 @@ const GeneratePage: React.FC = () => { /> - {/* ════ 右侧:预览 + 结果 ════ */} + {/* ════ 右侧:生成结果视频 ════ */}
- {currentStep >= 4 && !!currentTemplate && ( - 0} - voiceAudioUrl={previewVoiceAudioUrl || undefined} - titleSettings={{ - title: titleSettings.title, - size: titleSettings.size, - font: titleSettings.font, - color: titleSettings.color, - position: titleSettings.position as "top" | "center" | "bottom", - bold: titleSettings.bold, - italic: titleSettings.italic, - stroke: titleSettings.stroke, - shadow: titleSettings.shadow, - }} - /> - )} - {currentStep >= 6 && ( - { - setPreviewVideo(video) - setPreviewModalOpen(true) - }} - onDownload={handleDownload} - onShare={handleShare} - onGoToLibrary={() => navigate("/app/products")} - /> + {generated && generatedVideos.length > 0 ? ( +
+
+ ) : generating ? ( +
+
🎬
+
视频生成中...
+
+ 预计还需 1-2 分钟,请稍候 +
+
+
+
+
+ {Math.round(progress)}% +
+
+ ) : generateError ? ( +
+
+
+ 生成失败 +
+
+ {typeof generateError === "string" ? generateError : "请重试"} +
+ +
+ ) : ( +
+
🎥
+
+ 完成配置后点击「确认生成」 +
+
+ 生成的视频将在这里展示 +
+
)}
- {/* 视频预览弹窗 */} - setPreviewModalOpen(false)} - footer={null} - width="80vw" - centered - destroyOnClose - > - {previewVideo && ( -
-
- )} -
- {/* 音色克隆弹窗 */} void /* 生成 */ - generateCount: number - onGenerateCountChange: (n: number) => void generating: boolean generated: boolean generateError: string | null @@ -119,8 +117,6 @@ export const GenerateStepContent: React.FC = (props) = voiceMode, selectedClonedVoice, clonedVoices, - generateCount, - onGenerateCountChange, generating, generated, generateError, @@ -226,8 +222,6 @@ export const GenerateStepContent: React.FC = (props) = presetVoices={presetVoices} clonedVoices={clonedVoices} coverSettings={coverSettings} - generateCount={generateCount} - onGenerateCountChange={onGenerateCountChange} generating={generating} generated={generated} generateError={generateError} diff --git a/apps/web/src/pages/generate/components/Step7ConfirmGenerate.tsx b/apps/web/src/pages/generate/components/Step7ConfirmGenerate.tsx index 84c412098..968921d6f 100755 --- a/apps/web/src/pages/generate/components/Step7ConfirmGenerate.tsx +++ b/apps/web/src/pages/generate/components/Step7ConfirmGenerate.tsx @@ -24,8 +24,6 @@ interface Step7ConfirmGenerateProps { presetVoices: PresetVoiceItem[] clonedVoices: VoiceClone[] coverSettings: CoverConfig - generateCount: number - onGenerateCountChange: (count: number) => void generating: boolean generated: boolean generateError: string | null @@ -42,9 +40,6 @@ const Step7ConfirmGenerate: React.FC = (props) => { title, voiceName, coverSummary, - generateCount, - handleDecrement, - handleIncrement, generating, generated, generateError, @@ -65,10 +60,6 @@ const Step7ConfirmGenerate: React.FC = (props) => { title={title} voiceName={voiceName} coverSummary={coverSummary} - generateCount={generateCount} - generating={generating} - onDecrement={handleDecrement} - onIncrement={handleIncrement} /> void - onIncrement: () => void } const SummaryCard: React.FC = ({ @@ -19,10 +14,6 @@ const SummaryCard: React.FC = ({ title, voiceName, coverSummary, - generateCount, - generating, - onDecrement, - onIncrement, }) => { return (
@@ -46,29 +37,6 @@ const SummaryCard: React.FC = ({ 封面 {coverSummary}
-
- 生成数量 - -
- - {generateCount} - - 条视频 -
-
-
) } diff --git a/apps/web/src/pages/generate/generate.css b/apps/web/src/pages/generate/generate.css index 8e633dff9..efe195e65 100644 --- a/apps/web/src/pages/generate/generate.css +++ b/apps/web/src/pages/generate/generate.css @@ -1280,53 +1280,6 @@ } } -/* ── 生成数量步进器 ── */ -.xx-count-stepper { - display: inline-flex; - align-items: center; - gap: 6px; -} - -.xx-count-stepper-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border: 1px solid var(--border-primary, #e2e8f0); - border-radius: 8px; - background: var(--bg-surface, #fff); - color: var(--text-secondary, #64748b); - font-size: 13px; - cursor: pointer; - transition: all 0.15s; -} - -.xx-count-stepper-btn:hover:not(:disabled) { - border-color: var(--primary-400, #818cf8); - color: var(--primary-600, #4f46e5); - background: var(--primary-50, #eef2ff); -} - -.xx-count-stepper-btn:disabled { - opacity: 0.35; - cursor: not-allowed; -} - -.xx-count-stepper-value { - min-width: 24px; - text-align: center; - font-size: 16px; - font-weight: 600; - color: var(--text-primary, #1e293b); -} - -.xx-count-stepper-hint { - font-size: 12px; - color: var(--text-tertiary, #94a3b8); - margin-left: 2px; -} - /* ── 素材选择模式切换 Tab ── */ .xx-material-mode-tabs { display: flex; @@ -2529,6 +2482,21 @@ gap: 16px; } +/* ── 内联视频播放器(右侧) ── */ +.xx-inline-video-player { + width: 100%; + max-width: 320px; + background: var(--bg-surface, #fff); + border: 1px solid var(--border-primary, #e2e8f0); + border-radius: 16px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.xx-inline-video-player video { + background: #000; +} + .xx-preview-header { display: flex; align-items: center; diff --git a/apps/web/src/pages/generate/hooks/generate-video/types.ts b/apps/web/src/pages/generate/hooks/generate-video/types.ts index c114cd2e1..4105828ec 100755 --- a/apps/web/src/pages/generate/hooks/generate-video/types.ts +++ b/apps/web/src/pages/generate/hooks/generate-video/types.ts @@ -18,7 +18,6 @@ export interface UseGenerateVideoProps { duration: number autoSubtitles: boolean bgm: boolean - generateCount: number /** 当前草稿 ID(URL 参数 edit_plan_id,用于后端回写任务关联) */ sourceEditPlanId?: string | null /** 预览任务 ID(由 useStep6Cover 创建后写入,供 confirmGeneration 复用预览产物) */ diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts index 87983e211..7b969c13d 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts @@ -4,7 +4,6 @@ */ import { useState } from "react" import { useSearchParams } from "react-router-dom" -import type { GeneratedVideo } from "@/api/template-editor" import type { EditingTemplate } from "@/api/editing-planner" import type { CoverConfig } from "../../types/cover" import type { PresetVoiceItem } from "@/api/voices" @@ -68,10 +67,6 @@ export interface GenerateFormState { cloneModalOpen: boolean setCloneModalOpen: (open: boolean) => void - /* 生成数量 */ - generateCount: number - setGenerateCount: (n: number) => void - /* 高级设置 */ videoRatio: string duration: number @@ -91,12 +86,6 @@ export interface GenerateFormState { */ sourceEditPlanId: string | null - /* 预览弹窗 */ - previewVideo: GeneratedVideo | null - setPreviewVideo: (v: GeneratedVideo | null) => void - previewModalOpen: boolean - setPreviewModalOpen: (open: boolean) => void - /** 预览任务 ID(由 useStep6Cover 创建后写入,供 useGenerateVideo 复用) */ previewTaskId: string | null setPreviewTaskId: (id: string | null) => void @@ -155,9 +144,6 @@ export const useGenerateFormState = (): GenerateFormState => { /* ── 克隆声音弹窗 ── */ const [cloneModalOpen, setCloneModalOpen] = useState(false) - /* ── 生成数量 ── */ - const [generateCount, setGenerateCount] = useState(1) - /* ── 高级设置(隐藏但保留) ── */ const [videoRatio] = useState("9:16") const [duration] = useState(30) @@ -165,10 +151,6 @@ export const useGenerateFormState = (): GenerateFormState => { const [autoSubtitles] = useState(true) const [bgm] = useState(true) - /* ── 预览弹窗 ── */ - const [previewVideo, setPreviewVideo] = useState(null) - const [previewModalOpen, setPreviewModalOpen] = useState(false) - /* ── 预览任务 ID(useStep6Cover 创建预览时写入,useGenerateVideo 复用) ── */ // 持久化到 localStorage,key 按 editPlanId/templateId 区分,刷新页面后可恢复 const previewStorageKey = editPlanId @@ -227,8 +209,6 @@ export const useGenerateFormState = (): GenerateFormState => { presetVoices, cloneModalOpen, setCloneModalOpen, - generateCount, - setGenerateCount, videoRatio, duration, style, @@ -237,10 +217,6 @@ export const useGenerateFormState = (): GenerateFormState => { editPlanId, sourceEditPlanId, planConfigStr, - previewVideo, - setPreviewVideo, - previewModalOpen, - setPreviewModalOpen, previewTaskId, setPreviewTaskId, storedSourceEditPlanId, diff --git a/apps/web/src/pages/generate/hooks/useStep7Generate.ts b/apps/web/src/pages/generate/hooks/useStep7Generate.ts index 44ea08ad2..0cc44c1c7 100644 --- a/apps/web/src/pages/generate/hooks/useStep7Generate.ts +++ b/apps/web/src/pages/generate/hooks/useStep7Generate.ts @@ -25,8 +25,6 @@ interface UseStep7GenerateProps { presetVoices: PresetVoiceItem[] clonedVoices: VoiceClone[] coverSettings: CoverConfig - generateCount: number - onGenerateCountChange: (count: number) => void generating: boolean generated: boolean generateError: string | null @@ -47,8 +45,6 @@ export function useStep7Generate({ presetVoices: _presetVoices, clonedVoices: _clonedVoices, coverSettings, - generateCount, - onGenerateCountChange, generating, generated, generateError, @@ -90,14 +86,6 @@ export function useStep7Generate({ return { label: "即将完成", icon: "✨" } } - const handleDecrement = () => { - onGenerateCountChange(Math.max(1, generateCount - 1)) - } - - const handleIncrement = () => { - onGenerateCountChange(Math.min(10, generateCount + 1)) - } - const handleScrollToPreview = () => { const el = document.querySelector(".xx-preview-section") el?.scrollIntoView({ behavior: "smooth", block: "start" }) @@ -109,9 +97,6 @@ export function useStep7Generate({ title, voiceName, coverSummary, - generateCount, - handleDecrement, - handleIncrement, generating, generated, generateError,