fix: 恢复步骤4-6实时预览,步骤7显示最终视频 #1513

Closed
xiaoxia wants to merge 1 commits from fix/restore-preview-step4-5-step7-video into develop
2 changed files with 85 additions and 69 deletions
+77 -69
View File
@@ -4,14 +4,15 @@
* 左右布局:左侧 generate-form + 右侧 generate-preview
*
* 架构:
* - 右侧展示生成的视频,内联播放
* -
* - 步骤 4-6 右侧显示 FrontendPreviewPlayer 实时预览
* - 步骤 7 右侧内联播放生成的最终视频
* - 点"确认生成"时调用 createGenerationTask 创建一次服务器渲染任务
*/
import React, { useMemo } from "react"
import React, { useMemo, useState, useEffect, useRef } 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"
@@ -19,6 +20,7 @@ import {
calculateTotalVideoDuration,
estimateTotalVideoDuration,
} from "./utils/calculateTotalVideoDuration"
import FrontendPreviewPlayer from "./components/FrontendPreviewPlayer"
import GenerateStepsBar from "./components/GenerateStepsBar"
import GenerateStepContent from "./components/GenerateStepContent"
import GenerateStepActions from "./components/GenerateStepActions"
@@ -27,6 +29,8 @@ 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 = () => {
@@ -78,6 +82,53 @@ const GeneratePage: React.FC = () => {
onTitleSettingsChange: setTitleSettings,
})
/* ── 配音素材库(TTS 试听)── */
const { data: voiceMaterials = [] } = useQuery({
queryKey: ["assets", "voice"],
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
})
const [previewVoiceAudioUrl, setPreviewVoiceAudioUrl] = useState<string | null>(null)
const ttsAbortRef = useRef<AbortController | null>(null)
useEffect(() => {
const voiceAsset = voiceMaterials.find((m) => m.id === selectedVoice)
if (voiceAsset?.file_url) {
setPreviewVoiceAudioUrl(voiceAsset.file_url)
return
}
const voiceId = selectedClonedVoice || selectedVoice
if (!voiceId || !titleSettings.title) {
setPreviewVoiceAudioUrl(null)
return
}
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()
@@ -176,7 +227,7 @@ const GeneratePage: React.FC = () => {
<GenerateStepsBar currentStep={currentStep} onStepClick={setCurrentStep} />
<div className="xx-generate-layout">
<div className={`xx-generate-layout${currentStep < 4 ? " full-width" : ""}`}>
{/* ════ 左侧:表单区 ════ */}
<div className="xx-generate-form">
<GenerateStepContent
@@ -242,9 +293,29 @@ const GeneratePage: React.FC = () => {
/>
</div>
{/* ════ 右侧:生成结果视频 ════ */}
{/* ════ 右侧:步骤 4-6 实时预览,步骤 7 最终视频 ════ */}
<div className="xx-generate-right-col">
{generated && generatedVideos.length > 0 ? (
{currentStep >= 4 && currentStep <= 6 && !!currentTemplate && (
<FrontendPreviewPlayer
assets={previewAssets}
template={currentTemplate}
videoRatio={videoRatio}
ready={previewAssets.length > 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 === 7 && generated && generatedVideos.length > 0 && (
<div className="xx-inline-video-player">
<video
src={generatedVideos[0].download_url || generatedVideos[0].file_url}
@@ -268,69 +339,6 @@ const GeneratePage: React.FC = () => {
</button>
</div>
</div>
) : generating ? (
<div
className="xx-inline-video-player"
style={{ textAlign: "center", padding: "60px 20px" }}
>
<div style={{ fontSize: 48, marginBottom: 16 }}>🎬</div>
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 8 }}>...</div>
<div style={{ fontSize: 13, color: "var(--text-secondary)", marginBottom: 16 }}>
1-2
</div>
<div
style={{
maxWidth: 280,
margin: "0 auto",
height: 6,
background: "var(--bg-tertiary)",
borderRadius: 3,
overflow: "hidden",
}}
>
<div
style={{
width: `${Math.min(Math.round(progress), 100)}%`,
height: "100%",
background: "var(--primary-color, #6366f1)",
borderRadius: 3,
transition: "width 0.3s",
}}
/>
</div>
<div style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 8 }}>
{Math.round(progress)}%
</div>
</div>
) : generateError ? (
<div
className="xx-inline-video-player"
style={{ textAlign: "center", padding: "60px 20px" }}
>
<div style={{ fontSize: 48, marginBottom: 16 }}></div>
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 8, color: "#ef4444" }}>
</div>
<div style={{ fontSize: 13, color: "var(--text-secondary)", marginBottom: 16 }}>
{typeof generateError === "string" ? generateError : "请重试"}
</div>
<button className="xx-btn xx-btn-primary xx-btn-sm" onClick={handleRetryGenerate}>
🔄
</button>
</div>
) : (
<div
className="xx-inline-video-player"
style={{ textAlign: "center", padding: "80px 20px" }}
>
<div style={{ fontSize: 48, marginBottom: 16, opacity: 0.5 }}>🎥</div>
<div style={{ fontSize: 14, color: "var(--text-secondary)" }}>
</div>
<div style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 4 }}>
</div>
</div>
)}
</div>
</div>
+8
View File
@@ -113,6 +113,14 @@
align-items: start;
}
.xx-generate-layout.full-width {
grid-template-columns: 1fr;
}
.xx-generate-layout.full-width .xx-generate-right-col {
display: none;
}
/* ============================================================
左侧表单区 generate-form
============================================================ */