refactor: 步骤顺序调整 - 标题(4)→预览(5) + 预览接口增加标题参数 #1377

Merged
auto-approve-bot merged 1 commits from refactor/swap-title-preview-step-order into develop 2026-08-15 16:51:09 +08:00
11 changed files with 106 additions and 77 deletions
+9
View File
@@ -13,6 +13,15 @@ export interface CreatePreviewRequest {
video_title?: string
duration?: number
video_ratio?: string
/* 标题烧录参数(可选,传入后 ASS 渲染标题到预览视频中) */
title_text?: string
title_font?: string
title_size?: number
title_color?: string
title_position?: string
title_bold?: boolean
title_stroke?: boolean
title_shadow?: boolean
bgm_config?: {
enabled: boolean
preset_id?: string
+29 -28
View File
@@ -1,6 +1,6 @@
/**
* 智能剪辑页面 — V22 多预览 + 配音前置
* 7 步向导:选择模板 → 选择素材 → 选择配音 → 生成预览 → 选择标题 → 选择封面 → 确认生成
* 7 步向导:选择模板 → 选择素材 → 选择配音 → 选择标题 → 生成预览 → 选择封面 → 确认生成
* 左右布局:左侧 generate-form + 右侧 generate-preview
* 主组件仅保留整体布局与事件编排
* 状态管理 → hooks/useGenerateFormState
@@ -24,7 +24,7 @@ import GenerateStepActions from "./components/GenerateStepActions"
import { useGenerateFormState } from "./hooks/useGenerateFormState"
import { useStepNavigation } from "./hooks/useStepNavigation"
import { useGenerateVideo } from "./hooks/useGenerateVideo"
import { useStep4Preview } from "./hooks/useStep4Preview"
import { useStep5Preview } from "./hooks/useStep5Preview"
import "./generate.css"
const GeneratePage: React.FC = () => {
@@ -96,8 +96,8 @@ const GeneratePage: React.FC = () => {
return id ? [id] : []
}, [voiceMode, selectedVoice, selectedClonedVoice])
/* ── Step4 预览生成(多预览 + voice_ids ── */
const step4Preview = useStep4Preview({
/* ── Step5 预览生成(多预览 + voice_ids ── */
const step5Preview = useStep5Preview({
templates: userTemplates,
selectedTemplate,
materialMode,
@@ -108,6 +108,7 @@ const GeneratePage: React.FC = () => {
voiceIds: previewVoiceIds,
voiceLibraryId: selectedVoice || undefined,
previewCount,
titleSettings,
})
/* ── 步骤导航 ── */
@@ -119,7 +120,7 @@ const GeneratePage: React.FC = () => {
selectedMaterials,
smartSelectedIds,
titleSettings,
previewReady: step4Preview.canProceed,
previewReady: step5Preview.canProceed,
})
/* ── 视频生成核心逻辑 ── */
@@ -150,7 +151,7 @@ const GeneratePage: React.FC = () => {
autoSubtitles,
bgm,
generateCount,
previewTaskId: step4Preview.selectedTaskId,
previewTaskId: step5Preview.selectedTaskId,
})
/* ================================================================
@@ -207,20 +208,20 @@ const GeneratePage: React.FC = () => {
onDismissError={handleDismissError}
presetVoices={presetVoices}
videoRatio={videoRatio}
/* Step4 多预览 */
/* Step5 多预览 */
previewCount={previewCount}
onPreviewCountChange={setPreviewCount}
previewItems={step4Preview.items}
previewSelectedIndex={step4Preview.selectedIndex}
onSelectPreview={step4Preview.setSelectedIndex}
previewOverallStatus={step4Preview.previewStatus}
previewOverallError={step4Preview.previewError}
previewOverallProgress={step4Preview.progress}
previewAnyGenerating={step4Preview.anyGenerating}
previewTemplateName={step4Preview.templateName}
previewMaterialCount={step4Preview.materialCount}
onGeneratePreview={step4Preview.generatePreview}
onRegeneratePreview={step4Preview.regeneratePreview}
previewItems={step5Preview.items}
previewSelectedIndex={step5Preview.selectedIndex}
onSelectPreview={step5Preview.setSelectedIndex}
previewOverallStatus={step5Preview.previewStatus}
previewOverallError={step5Preview.previewError}
previewOverallProgress={step5Preview.progress}
previewAnyGenerating={step5Preview.anyGenerating}
previewTemplateName={step5Preview.templateName}
previewMaterialCount={step5Preview.materialCount}
onGeneratePreview={step5Preview.generatePreview}
onRegeneratePreview={step5Preview.regeneratePreview}
/>
<GenerateStepActions
@@ -236,22 +237,22 @@ const GeneratePage: React.FC = () => {
{/* ════ 右侧:预览 + 生成结果 ════ */}
<div className="xx-generate-right-col">
{/* 预览视频面板(Step4+ 常驻,展示选中的预览) */}
{currentStep >= 4 && (
{/* 预览视频面板(Step5+ 常驻,展示选中的预览) */}
{currentStep >= 5 && (
<PreviewVideoPanel
previewStatus={step4Preview.previewStatus}
previewResult={step4Preview.previewResult}
previewError={step4Preview.previewError}
progress={step4Preview.progress}
previewStatus={step5Preview.previewStatus}
previewResult={step5Preview.previewResult}
previewError={step5Preview.previewError}
progress={step5Preview.progress}
videoRatio={videoRatio}
onRegenerate={step4Preview.regeneratePreview}
onRegenerate={step5Preview.regeneratePreview}
titleText={titleSettings.title}
titleSettings={currentStep >= 5 ? titleSettings : undefined}
titleSettings={currentStep >= 4 ? titleSettings : undefined}
/>
)}
{/* 正式生成结果(Step5+ 才显示) */}
{currentStep >= 5 && (
{/* 正式生成结果(Step6+ 才显示) */}
{currentStep >= 6 && (
<GenerateResultPanel
generated={generated}
generating={generating}
@@ -1,7 +1,7 @@
/**
* GeneratePage 步骤内容渲染
* 根据当前步骤渲染对应的 Step 组件
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 预览(4) → 标题(5) → 封面(6) → 确认(7)
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 标题(4) → 预览(5) → 封面(6) → 确认(7)
*/
import React from "react"
import type { EditingTemplate } from "@/api/editing-planner"
@@ -9,12 +9,12 @@ import type { PresetVoiceItem } from "@/api/voices"
import type { VoiceClone } from "@/api/voice-clone"
import type { CoverConfig } from "../types/cover"
import type { TitleSettings } from "../types"
import type { PreviewItem, PreviewStatus } from "../hooks/useStep4Preview"
import type { PreviewItem, PreviewStatus } from "../hooks/useStep5Preview"
import Step1TemplateSelect from "../components/Step1TemplateSelect"
import Step2MaterialSelect from "../components/Step2MaterialSelect"
import Step3VoiceSelect from "../components/Step5VoiceSelect"
import Step4GeneratePreview from "../components/Step4GeneratePreview"
import Step5TitleSettings from "../components/Step4TitleSettings"
import Step5GeneratePreview from "../components/Step5GeneratePreview"
import Step4TitleSettings from "../components/Step4TitleSettings"
import Step6CoverSettings from "../components/Step6CoverSettings"
import Step7ConfirmGenerate from "../components/Step7ConfirmGenerate"
import type { GeneratedVideo } from "@/api/template-editor"
@@ -157,7 +157,14 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
)
case 4:
return (
<Step4GeneratePreview
<Step4TitleSettings
titleSettings={titleSettings}
onTitleSettingsChange={onTitleSettingsChange}
/>
)
case 5:
return (
<Step5GeneratePreview
templateName={previewTemplateName}
materialCount={previewMaterialCount}
duration={duration}
@@ -175,13 +182,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
onRegeneratePreview={onRegeneratePreview}
/>
)
case 5:
return (
<Step5TitleSettings
titleSettings={titleSettings}
onTitleSettingsChange={onTitleSettingsChange}
/>
)
case 6:
return (
<Step6CoverSettings
@@ -13,7 +13,7 @@
*/
import React, { useRef, useEffect, useCallback } from "react"
import { PlayCircleOutlined, LoadingOutlined } from "@ant-design/icons"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep4Preview"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep5Preview"
import type { TitleSettings } from "../types"
interface PreviewVideoPanelProps {
@@ -1,5 +1,5 @@
/**
* Step 4
* Step 5
*
*/
import React from "react"
@@ -12,9 +12,9 @@ import {
ClockCircleOutlined,
} from "@ant-design/icons"
import { InputNumber } from "antd"
import type { PreviewItem, PreviewStatus } from "../hooks/useStep4Preview"
import type { PreviewItem, PreviewStatus } from "../hooks/useStep5Preview"
interface Step4GeneratePreviewProps {
interface Step5GeneratePreviewProps {
templateName: string
materialCount: string
duration: number
@@ -39,7 +39,7 @@ const PREVIEW_COUNT_OPTIONS = [
{ value: 3, label: "3个" },
]
const Step4GeneratePreview: React.FC<Step4GeneratePreviewProps> = ({
const Step5GeneratePreview: React.FC<Step5GeneratePreviewProps> = ({
templateName: _templateName,
materialCount: _materialCount,
videoRatio,
@@ -291,4 +291,4 @@ const Step4GeneratePreview: React.FC<Step4GeneratePreviewProps> = ({
)
}
export default Step4GeneratePreview
export default Step5GeneratePreview
+2 -2
View File
@@ -32,8 +32,8 @@ export const STEPS = [
{ key: 1, label: "选择模板" },
{ key: 2, label: "选择素材" },
{ key: 3, label: "选择配音" },
{ key: 4, label: "生成预览" },
{ key: 5, label: "选择标题" },
{ key: 4, label: "选择标题" },
{ key: 5, label: "生成预览" },
{ key: 6, label: "选择封面" },
{ key: 7, label: "确认生成" },
]
@@ -1,5 +1,5 @@
/**
* Step 4 Hook + voice_ids
* Step 5 Hook + voice_ids
* /generation/preview
*/
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
@@ -8,6 +8,7 @@ import { updateEditPlan } from "@/api/template-editor/editPlans"
import type { PreviewTaskResponse, PreviewStatus as ApiPreviewStatus } from "@/api/generation"
import type { EditingTemplate } from "@/api/editing-planner"
import { safeExtractError } from "./generate-video/errorUtils"
import type { TitleSettings } from "../types"
/** 安全地将值转为字符串,防止对象被直接渲染导致 React Error #31 */
const safeString = (val: unknown, fallback: string): string => {
@@ -26,7 +27,7 @@ const safeNumber = (val: unknown, fallback = 0): number => {
return fallback
}
interface UseStep4PreviewProps {
interface UseStep5PreviewProps {
templates: EditingTemplate[]
selectedTemplate: string
materialMode: "manual" | "auto"
@@ -40,6 +41,8 @@ interface UseStep4PreviewProps {
voiceLibraryId?: string
/** 要生成的预览数量 */
previewCount?: number
/** 标题设置(传递给后端,让预览视频包含标题) */
titleSettings?: TitleSettings
}
export type PreviewStatus = "idle" | "pending" | "generating" | "ready" | "error"
@@ -78,7 +81,7 @@ const createInitialItem = (index: number): PreviewItem => ({
progress: 0,
})
export function useStep4Preview({
export function useStep5Preview({
templates,
selectedTemplate,
materialMode,
@@ -89,7 +92,8 @@ export function useStep4Preview({
voiceIds,
voiceLibraryId,
previewCount = 1,
}: UseStep4PreviewProps) {
titleSettings,
}: UseStep5PreviewProps) {
const templateName = useMemo(
() => templates.find((t) => t.id === selectedTemplate)?.name ?? "未选择",
[templates, selectedTemplate],
@@ -153,6 +157,7 @@ export function useStep4Preview({
duration,
videoRatio,
voiceIds: [...(voiceIds || [])].sort().join(","),
titleSettings: titleSettings?.title || "",
})
useEffect(() => {
@@ -164,6 +169,7 @@ export function useStep4Preview({
duration,
videoRatio,
[...(voiceIds || [])].sort().join(","),
titleSettings?.title || "",
].join("|")
const prevKey = [
@@ -174,6 +180,7 @@ export function useStep4Preview({
prevDepsRef.current.duration,
prevDepsRef.current.videoRatio,
prevDepsRef.current.voiceIds,
prevDepsRef.current.titleSettings,
].join("|")
if (prevKey !== currentKey && items.some((it) => it.status !== "idle")) {
@@ -191,6 +198,7 @@ export function useStep4Preview({
duration,
videoRatio,
voiceIds: [...(voiceIds || [])].sort().join(","),
titleSettings: titleSettings?.title || "",
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@@ -202,6 +210,7 @@ export function useStep4Preview({
videoRatio,
voiceIds,
previewCount,
titleSettings,
])
// 组件卸载时清理所有轮询
@@ -349,6 +358,15 @@ export function useStep4Preview({
video_ratio: videoRatio,
voice_ids: voiceIds && voiceIds.length > 0 ? voiceIds : undefined,
voice_library_id: voiceLibraryId || undefined,
// 标题信息
title_text: titleSettings?.title || undefined,
title_font: titleSettings?.font || undefined,
title_size: titleSettings?.size || undefined,
title_color: titleSettings?.color || undefined,
title_position: titleSettings?.position || undefined,
title_bold: titleSettings?.bold || undefined,
title_stroke: titleSettings?.stroke || undefined,
title_shadow: titleSettings?.shadow || undefined,
})
if (startTimeRef.current === 0) return
@@ -374,6 +392,7 @@ export function useStep4Preview({
voiceIds,
voiceLibraryId,
previewCount,
titleSettings,
clearPollTimer,
pollPreviewStatus,
])
@@ -450,4 +469,4 @@ export function useStep4Preview({
}
}
export default useStep4Preview
export default useStep5Preview
@@ -1,7 +1,7 @@
/**
* GeneratePage 步骤导航
* 管理步骤切换与各步骤的前置校验
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 预览(4) → 标题(5) → 封面(6) → 确认(7)
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 标题(4) → 预览(5) → 封面(6) → 确认(7)
*/
import { message } from "antd"
import type { TitleSettings } from "../types"
@@ -49,12 +49,12 @@ export const useStepNavigation = (options: UseStepNavigationOptions): UseStepNav
return
}
// Step3 配音:配音为可选项,不强制校验,用户可跳过
if (currentStep === 4 && !previewReady) {
message.warning("请先生成剪辑预览")
if (currentStep === 4 && !titleSettings.title.trim()) {
message.warning("请选择或输入标题")
return
}
if (currentStep === 5 && !titleSettings.title.trim()) {
message.warning("请选择或输入标题")
if (currentStep === 5 && !previewReady) {
message.warning("请先生成剪辑预览")
return
}
if (currentStep < 7) {
@@ -17,7 +17,7 @@ import "@/api/generation/types"
// 直接引入所有 Step 组件,建立完整依赖链
import "@/pages/generate/GeneratePage"
import "@/pages/generate/components/Step2MaterialSelect"
import "@/pages/generate/components/Step4GeneratePreview"
import "@/pages/generate/components/Step5GeneratePreview"
import "@/pages/generate/components/Step4TitleSettings"
import "@/pages/generate/components/Step5VoiceSelect"
import "@/pages/generate/components/PreviewVideoPanel"
@@ -47,7 +47,7 @@ describe("GeneratePage module smoke test", () => {
})
})
import "@/pages/generate/hooks/useGenerateVideo"
import "@/pages/generate/hooks/useStep4Preview"
import "@/pages/generate/hooks/useStep5Preview"
import "@/pages/generate/hooks/generate-video/useGenerationPolling"
import "@/pages/generate/hooks/useGenerateFormState"
import "@/pages/generate/hooks/useGenerateFormState/useTemplateSelection"
@@ -1,17 +0,0 @@
/**
* Step4GeneratePreview smoke test
* 确保 vitest related 模式能匹配到第4步预览生成相关文件的改动
*/
import { describe, it, expect } from "vitest"
import "@/pages/generate/components/Step4GeneratePreview"
import "@/pages/generate/hooks/useStep4Preview"
import "@/pages/generate/hooks/useStepNavigation"
import "@/pages/generate/components/GenerateStepContent"
import "@/pages/generate/GeneratePage"
describe("Step4GeneratePreview module smoke test", () => {
it("should load all step4 preview modules", () => {
expect(true).toBe(true)
})
})
@@ -0,0 +1,17 @@
/**
* Step5GeneratePreview smoke test
* 确保 vitest related 模式能匹配到第5步预览生成相关文件的改动
*/
import { describe, it, expect } from "vitest"
import "@/pages/generate/components/Step5GeneratePreview"
import "@/pages/generate/hooks/useStep5Preview"
import "@/pages/generate/hooks/useStepNavigation"
import "@/pages/generate/components/GenerateStepContent"
import "@/pages/generate/GeneratePage"
describe("Step5GeneratePreview module smoke test", () => {
it("should load all step5 preview modules", () => {
expect(true).toBe(true)
})
})