f9daa08b2e
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 31s
CI/CD Pipeline / Build Staging API Image (push) Successful in 40s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m46s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m1s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 36s
CI/CD Pipeline / Integration Tests (push) Successful in 4m53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m36s
CI/CD Pipeline / Validate - Style (push) Failing after 5m37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m10s
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
296 lines
9.9 KiB
TypeScript
296 lines
9.9 KiB
TypeScript
/**
|
||
* GeneratePage 步骤内容渲染(#1970 流程重构)
|
||
* 步骤顺序:选择模式(1) → 选择素材(2) → 选择标题(3) → 确认生成(4) → 选择封面(5)
|
||
* 原步骤"选择配音"已从主流程移除,改为 Step1 下一步分支弹窗(VoiceSelectModal / ScriptSelectModal → TtsVoiceModal)。
|
||
*/
|
||
import React from "react"
|
||
import type { EditPlanClip } from "@/api/template-editor"
|
||
import type { CoverConfig } from "../types/cover"
|
||
import type { TitleSettings } from "../types"
|
||
import type { ScriptItem } from "@/api/scripts"
|
||
import Step1EditMode from "./Step1EditMode"
|
||
import type { EditMode } from "./Step1EditMode"
|
||
import Step2MaterialSelect from "../components/Step2MaterialSelect"
|
||
import Step4TitleSettings from "../components/Step4TitleSettings"
|
||
import Step6CoverSettings from "../components/Step6CoverSettings"
|
||
import BatchGenerationGrid from "./BatchGenerationGrid"
|
||
import type { BatchTaskState } from "../hooks/generate-video/useGenerationPolling"
|
||
import type { GeneratedVideo } from "@/api/template-editor"
|
||
|
||
export interface GenerateStepContentProps {
|
||
currentStep: number
|
||
/* Step1:剪辑模式 + 生成设置 */
|
||
editMode: EditMode
|
||
onEditModeChange: (m: EditMode) => void
|
||
dedupEnabled: boolean
|
||
onDedupEnabledChange: (v: boolean) => void
|
||
/* ── 片段数量(#1899) ── */
|
||
clipCount: number
|
||
onClipCountChange: (n: number) => void
|
||
/* ── 生成数量/比例(Step1 设置) ── */
|
||
previewCount: number
|
||
onPreviewCountChange: (n: number) => void
|
||
videoRatio: "9:16" | "16:9"
|
||
onVideoRatioChange: (r: "9:16" | "16:9") => void
|
||
/* ── 素材 ── */
|
||
materialMode: "manual" | "auto"
|
||
onMaterialModeChange: (mode: "manual" | "auto") => void
|
||
selectedMaterials: string[]
|
||
onSelectedMaterialsChange: (ids: string[]) => void
|
||
smartSelectedIds: string[]
|
||
onSmartSelectedIdsChange: (ids: string[]) => void
|
||
selectedTemplate?: string
|
||
/* ── 标题 ── */
|
||
titleSettings: TitleSettings
|
||
onTitleSettingsChange: (settings: TitleSettings) => void
|
||
onUpdatePosition: (position: string) => void
|
||
onUpdateFont: (font: string) => void
|
||
onUpdateSize: (size: number) => void
|
||
onToggleBold: () => void
|
||
onToggleItalic: () => void
|
||
onToggleStroke: () => void
|
||
onToggleShadow: () => void
|
||
onApplyPreset: (presetKey: string) => void
|
||
activePreset: string | null
|
||
titlePresets: { key: string; label: string; previewStyle: React.CSSProperties }[]
|
||
/* ── 封面 ── */
|
||
coverSettings: CoverConfig
|
||
onCoverSettingsChange: (settings: CoverConfig) => void
|
||
/* ── 配音 ── */
|
||
selectedVoice: string
|
||
onSelectedVoiceChange: (id: string) => void
|
||
onServerClipsChange: (clips: EditPlanClip[]) => void
|
||
/* ── 生成 ── */
|
||
generating: boolean
|
||
generated: boolean
|
||
generateError: string | null
|
||
progress: number
|
||
generatedVideos: GeneratedVideo[]
|
||
onRetry: () => void
|
||
onRetryBatchTask: (taskId: string) => void
|
||
onDismissError: () => void
|
||
batchTasks: BatchTaskState[]
|
||
bgm: boolean
|
||
bgmConfig?: { enabled: boolean; music_id?: string }
|
||
/* ── 批量生成 ── */
|
||
previewTitles: string[]
|
||
onPreviewTitlesChange: (titles: string[]) => void
|
||
voiceModePerVideo: boolean
|
||
onVoiceModePerVideoChange: (v: boolean) => void
|
||
voiceLibraryIds: string[]
|
||
onVoiceLibraryIdsChange: (ids: string[]) => void
|
||
previewCovers: string[]
|
||
onPreviewCoversChange: (urls: string[]) => void
|
||
selectedVariantIds?: number[]
|
||
/* ── 摘要信息(#1970 Step4 展示用) ── */
|
||
selectedScript: ScriptItem | null
|
||
ttsVoiceId: string
|
||
ttsVoiceSource: "preset" | "clone"
|
||
}
|
||
|
||
export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) => {
|
||
// Only destructure props actually referenced in JSX below
|
||
const {
|
||
currentStep,
|
||
editMode,
|
||
onEditModeChange,
|
||
dedupEnabled,
|
||
onDedupEnabledChange,
|
||
clipCount,
|
||
onClipCountChange,
|
||
previewCount,
|
||
onPreviewCountChange,
|
||
videoRatio,
|
||
onVideoRatioChange,
|
||
materialMode,
|
||
onMaterialModeChange,
|
||
selectedMaterials,
|
||
onSelectedMaterialsChange,
|
||
smartSelectedIds,
|
||
onSmartSelectedIdsChange,
|
||
selectedTemplate,
|
||
titleSettings,
|
||
onTitleSettingsChange,
|
||
onUpdatePosition,
|
||
onUpdateFont,
|
||
onUpdateSize,
|
||
onToggleBold,
|
||
onToggleItalic,
|
||
onToggleStroke,
|
||
onToggleShadow,
|
||
onApplyPreset,
|
||
activePreset,
|
||
titlePresets,
|
||
coverSettings,
|
||
onCoverSettingsChange,
|
||
onServerClipsChange,
|
||
generating,
|
||
generateError,
|
||
progress,
|
||
onRetry,
|
||
generatedVideos,
|
||
batchTasks,
|
||
onRetryBatchTask,
|
||
previewTitles,
|
||
onPreviewTitlesChange,
|
||
previewCovers,
|
||
onPreviewCoversChange,
|
||
selectedVariantIds,
|
||
selectedScript,
|
||
ttsVoiceId,
|
||
ttsVoiceSource,
|
||
} = props
|
||
|
||
const handleClipsChange = React.useCallback(
|
||
(clips: EditPlanClip[], _templateId?: string) => {
|
||
onServerClipsChange(clips)
|
||
},
|
||
[onServerClipsChange],
|
||
)
|
||
|
||
switch (currentStep) {
|
||
case 1:
|
||
return (
|
||
<Step1EditMode
|
||
editMode={editMode}
|
||
onEditModeChange={onEditModeChange}
|
||
previewCount={previewCount}
|
||
onPreviewCountChange={onPreviewCountChange}
|
||
videoRatio={videoRatio}
|
||
onVideoRatioChange={onVideoRatioChange}
|
||
dedupEnabled={dedupEnabled}
|
||
onDedupEnabledChange={onDedupEnabledChange}
|
||
/>
|
||
)
|
||
case 2:
|
||
return (
|
||
<Step2MaterialSelect
|
||
editMode={editMode}
|
||
materialMode={materialMode}
|
||
onMaterialModeChange={onMaterialModeChange}
|
||
selectedMaterials={selectedMaterials}
|
||
onSelectedMaterialsChange={onSelectedMaterialsChange}
|
||
smartSelectedIds={smartSelectedIds}
|
||
onSmartSelectedIdsChange={onSmartSelectedIdsChange}
|
||
clipCount={clipCount}
|
||
onClipCountChange={onClipCountChange}
|
||
selectedTemplate={selectedTemplate}
|
||
onServerClipsChange={handleClipsChange}
|
||
/>
|
||
)
|
||
case 3:
|
||
return (
|
||
<Step4TitleSettings
|
||
titleSettings={titleSettings}
|
||
onTitleSettingsChange={onTitleSettingsChange}
|
||
onUpdatePosition={onUpdatePosition}
|
||
onUpdateFont={onUpdateFont}
|
||
onUpdateSize={onUpdateSize}
|
||
onToggleBold={onToggleBold}
|
||
onToggleItalic={onToggleItalic}
|
||
onToggleStroke={onToggleStroke}
|
||
onToggleShadow={onToggleShadow}
|
||
onApplyPreset={onApplyPreset}
|
||
activePreset={activePreset}
|
||
titlePresets={titlePresets}
|
||
previewCount={previewCount}
|
||
previewTitles={previewTitles}
|
||
onPreviewTitlesChange={onPreviewTitlesChange}
|
||
/>
|
||
)
|
||
case 4:
|
||
return (
|
||
<div className="xx-form-section">
|
||
{/* 配置摘要(#1970) */}
|
||
<div
|
||
style={{
|
||
padding: 14,
|
||
background: "#f9fafb",
|
||
borderRadius: 8,
|
||
marginBottom: 16,
|
||
fontSize: 13,
|
||
lineHeight: 1.8,
|
||
color: "#374151",
|
||
}}
|
||
>
|
||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 6, color: "#111" }}>
|
||
📋 生成配置
|
||
</div>
|
||
<div>🎬 剪辑模式:{editMode === "random" ? "🎲 随机混剪" : "📖 叙事剪辑"}</div>
|
||
{editMode === "random" ? (
|
||
<div>🎙️ 配音来源:配音库音频</div>
|
||
) : (
|
||
<>
|
||
<div>📝 文案:{selectedScript?.title ?? "未选择"}</div>
|
||
<div>
|
||
🎙️ 合成配音音色:
|
||
{ttsVoiceId
|
||
? `${ttsVoiceSource === "clone" ? "克隆音色" : "系统音色"}(${ttsVoiceId.slice(0, 8)}...)`
|
||
: "未选择"}
|
||
</div>
|
||
</>
|
||
)}
|
||
<div>📱 视频比例:{videoRatio}</div>
|
||
<div>🎯 智能降重:{dedupEnabled ? "已开启" : "已关闭"}</div>
|
||
{previewCount > 1 && <div>📦 生成数量:{previewCount} 个</div>}
|
||
</div>
|
||
|
||
{previewCount > 1 ? (
|
||
<BatchGenerationGrid
|
||
tasks={batchTasks}
|
||
titles={previewTitles}
|
||
onRetryTask={onRetryBatchTask}
|
||
/>
|
||
) : generating ? (
|
||
<div className="xx-gen-progress-card">
|
||
<div className="xx-gen-progress-header">
|
||
<div className="xx-gen-progress-info">
|
||
<div className="xx-gen-progress-phase">
|
||
⏳ 视频渲染中… {Math.round(progress)}%
|
||
</div>
|
||
<div className="xx-gen-progress-sub">
|
||
生成过程中可以切换到其他页面,完成后可在任务历史查看
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="xx-gen-progress-bar">
|
||
<div
|
||
className="xx-gen-progress-bar-fill"
|
||
style={{ width: `${Math.min(Math.round(progress), 100)}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : generateError ? (
|
||
<div className="xx-gen-error-card">
|
||
<div className="xx-gen-error-info">
|
||
<div className="xx-gen-error-title">生成失败</div>
|
||
<div className="xx-gen-error-msg">{generateError}</div>
|
||
</div>
|
||
<button type="button" className="xx-btn xx-btn-primary xx-btn-sm" onClick={onRetry}>
|
||
🔄 重试
|
||
</button>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
)
|
||
case 5:
|
||
return (
|
||
<Step6CoverSettings
|
||
coverSettings={coverSettings}
|
||
onCoverSettingsChange={onCoverSettingsChange}
|
||
titleSettings={titleSettings}
|
||
generatedVideos={generatedVideos}
|
||
previewCount={previewCount}
|
||
previewTitles={previewTitles}
|
||
previewCovers={previewCovers}
|
||
onPreviewCoversChange={onPreviewCoversChange}
|
||
selectedVariantIndexes={selectedVariantIds}
|
||
/>
|
||
)
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
export default GenerateStepContent
|