Compare commits

...

1 Commits

Author SHA1 Message Date
xiaoxia 784a7dc939 fix: 移除预览播放器,改为内联播放生成视频;删除生成数量功能
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m58s
AI Code Review / AI Code Review (pull_request) Successful in 7m2s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m31s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m2s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m34s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 13m17s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m18s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 16m15s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 46s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 28m44s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m58s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2m28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
- 移除右侧 FrontendPreviewPlayer 实时预览组件
- 移除视频预览弹窗 Modal
- 生成的视频在右侧内联播放(HTML5 video 标签,controls + autoPlay)
- 添加生成中、失败、空状态的 UI 展示
- 删除生成数量(generateCount)全链路代码:
  - useGenerateFormState: 移除 generateCount state
  - useGenerateVideo: 移除 generateCount prop
  - GenerateStepContent: 移除 generateCount props 传递
  - Step7ConfirmGenerate: 移除 generateCount 相关逻辑
  - SummaryCard: 移除数量步进器 UI
  - useStep7Generate: 移除 handleDecrement/handleIncrement
  - generate-video/types: 移除 generateCount 字段
  - template-editor/types: 移除 generate_count API 字段
  - generate.css: 移除 .xx-count-stepper 样式
- 清理不再使用的 previewVideo/previewModalOpen 状态
- 清理不再使用的 TTS 预览音频逻辑(previewVoiceAudioUrl)
- 添加 .xx-inline-video-player 样式
2026-08-26 21:03:55 +08:00
9 changed files with 107 additions and 266 deletions
@@ -114,8 +114,6 @@ export interface EditPlanConfig {
auto_subtitles?: boolean
/** 是否启用 BGM */
bgm?: boolean
/** 生成数量 */
generate_count?: number
/** 素材模式 */
material_mode?: string
/** 前端标题设置(Step4 自动保存,与 title_config 字段分离,不影响后端渲染) */
+92 -130
View File
@@ -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<string | null>(null)
const ttsAbortRef = useRef<AbortController | null>(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 = () => {
/>
</div>
{/* ════ 右侧:预览 + 结果 ════ */}
{/* ════ 右侧:生成结果视频 ════ */}
<div className="xx-generate-right-col">
{currentStep >= 4 && !!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 >= 6 && (
<GenerateResultPanel
generated={generated}
generating={generating}
progress={progress}
generateError={generateError}
generatedVideos={generatedVideos}
onVideoPreview={(video) => {
setPreviewVideo(video)
setPreviewModalOpen(true)
}}
onDownload={handleDownload}
onShare={handleShare}
onGoToLibrary={() => navigate("/app/products")}
/>
{generated && generatedVideos.length > 0 ? (
<div className="xx-inline-video-player">
<video
src={generatedVideos[0].download_url || generatedVideos[0].file_url}
controls
autoPlay
style={{ width: "100%", maxHeight: "70vh", objectFit: "contain", borderRadius: 12 }}
poster={generatedVideos[0].thumbnail_url || undefined}
/>
<div style={{ display: "flex", gap: 8, marginTop: 12, justifyContent: "center" }}>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleDownload}>
</button>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleShare}>
🔗
</button>
<button
className="xx-btn xx-btn-ghost xx-btn-sm"
onClick={() => navigate("/app/products")}
>
📁
</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>
{/* 视频预览弹窗 */}
<Modal
className="xx-preview-modal"
open={previewModalOpen}
onCancel={() => setPreviewModalOpen(false)}
footer={null}
width="80vw"
centered
destroyOnClose
>
{previewVideo && (
<div className="xx-preview-modal-content">
<video
src={previewVideo.download_url || previewVideo.file_url}
controls
autoPlay
style={{ width: "100%", maxHeight: "70vh", objectFit: "contain" }}
poster={previewVideo.thumbnail_url || undefined}
/>
</div>
)}
</Modal>
{/* 音色克隆弹窗 */}
<CloneModal
open={cloneModalOpen}
@@ -65,8 +65,6 @@ export interface GenerateStepContentProps {
cloneModalOpen: boolean
onCloneModalOpenChange: (open: boolean) => void
/* 生成 */
generateCount: number
onGenerateCountChange: (n: number) => void
generating: boolean
generated: boolean
generateError: string | null
@@ -119,8 +117,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
voiceMode,
selectedClonedVoice,
clonedVoices,
generateCount,
onGenerateCountChange,
generating,
generated,
generateError,
@@ -226,8 +222,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
presetVoices={presetVoices}
clonedVoices={clonedVoices}
coverSettings={coverSettings}
generateCount={generateCount}
onGenerateCountChange={onGenerateCountChange}
generating={generating}
generated={generated}
generateError={generateError}
@@ -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<Step7ConfirmGenerateProps> = (props) => {
title,
voiceName,
coverSummary,
generateCount,
handleDecrement,
handleIncrement,
generating,
generated,
generateError,
@@ -65,10 +60,6 @@ const Step7ConfirmGenerate: React.FC<Step7ConfirmGenerateProps> = (props) => {
title={title}
voiceName={voiceName}
coverSummary={coverSummary}
generateCount={generateCount}
generating={generating}
onDecrement={handleDecrement}
onIncrement={handleIncrement}
/>
<GenerationStatus
generating={generating}
@@ -1,5 +1,4 @@
import React from "react"
import { MinusOutlined, PlusOutlined } from "@ant-design/icons"
interface SummaryCardProps {
templateName: string
@@ -7,10 +6,6 @@ interface SummaryCardProps {
title: string
voiceName: string
coverSummary: string
generateCount: number
generating: boolean
onDecrement: () => void
onIncrement: () => void
}
const SummaryCard: React.FC<SummaryCardProps> = ({
@@ -19,10 +14,6 @@ const SummaryCard: React.FC<SummaryCardProps> = ({
title,
voiceName,
coverSummary,
generateCount,
generating,
onDecrement,
onIncrement,
}) => {
return (
<div className="xx-summary-card">
@@ -46,29 +37,6 @@ const SummaryCard: React.FC<SummaryCardProps> = ({
<span className="xx-summary-label"></span>
<span className="xx-summary-value">{coverSummary}</span>
</div>
<div className="xx-summary-row">
<span className="xx-summary-label"></span>
<span className="xx-summary-value">
<div className="xx-count-stepper">
<button
className="xx-count-stepper-btn"
disabled={generateCount <= 1 || generating}
onClick={onDecrement}
>
<MinusOutlined />
</button>
<span className="xx-count-stepper-value">{generateCount}</span>
<button
className="xx-count-stepper-btn"
disabled={generateCount >= 10 || generating}
onClick={onIncrement}
>
<PlusOutlined />
</button>
<span className="xx-count-stepper-hint"></span>
</div>
</span>
</div>
</div>
)
}
+15 -47
View File
@@ -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;
@@ -18,7 +18,6 @@ export interface UseGenerateVideoProps {
duration: number
autoSubtitles: boolean
bgm: boolean
generateCount: number
/** 当前草稿 IDURL 参数 edit_plan_id,用于后端回写任务关联) */
sourceEditPlanId?: string | null
/** 预览任务 ID(由 useStep6Cover 创建后写入,供 confirmGeneration 复用预览产物) */
@@ -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<GeneratedVideo | null>(null)
const [previewModalOpen, setPreviewModalOpen] = useState(false)
/* ── 预览任务 IDuseStep6Cover 创建预览时写入,useGenerateVideo 复用) ── */
// 持久化到 localStoragekey 按 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,
@@ -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,