fix: 恢复步骤4-6实时预览,步骤7显示最终视频 #1513
@@ -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>
|
||||
|
||||
@@ -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
|
||||
============================================================ */
|
||||
|
||||
Reference in New Issue
Block a user