Files
xiaoxia-saas/apps/web/src/pages/generate/components/GenerateStepContent.tsx
T
Coze Agent 0a004db1bd
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m52s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 28s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m20s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 46s
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 / Frontend Lint (pull_request) Successful in 1m29s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 51s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m16s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 6m33s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 6m40s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 7m43s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 14m30s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 17m40s
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 / 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 / CI Gate (pull_request) Failing after 3s
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 / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Waiting to run
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 32s
feat(generate): 批量生成支持>3视频排队提交 + 两列标题 + 独立配音 (#2096)
- P0: 批量生成由一次性提交count=N改为串行count=1逐任务提交;
  遇到 429 USER_QUEUE_FULL / 503 SYSTEM_QUEUE_FULL 自动读取 estimated_wait_seconds
  后重试,不再弹错误阻止提交;
  新增 useGenerationPolling.pollBatchTaskQueued 支持增量排队轮询(不触发整体 onComplete);
  卡片新增 queued 状态(时钟图标+排队文案);
  单个任务提交失败不阻断其余任务,失败卡片独立展示错误+重试按钮;
  generating 状态改为所有任务到达终态(completed/awaiting_cover/failed)且无queued/running才关闭。
- P1: Step4 批量标题改为 CSS Grid 两列布局(gridTemplateColumns: repeat(2,minmax(0,1fr))),
  单字段 maxWidth 由 640 改为 100%,纵向空间节省约一半。
- P1: Step3(标题页)集成 Step3VoiceWithMode,批量模式下支持共用/独立配音切换;
  独立配音时每个视频可单独选择音色,默认继承全局配音;
  GenerateStepContent props 已从 GeneratePage 透传,之前组件只定义未渲染,本次补齐 JSX 引用。
- BatchTaskState.status 新增 queued 枚举;
- useGenerateVideo 重构 generate 函数,拆分 buildBasePayload/submitOne 逻辑,
  新增 cancelledRef/queueTimersRef 做 unmount 清理。
2026-09-29 18:05:20 +08:00

299 lines
10 KiB
TypeScript

/**
* GeneratePage 步骤内容渲染(#1970 流程重构)
* 步骤顺序:选择模式(1) → 选择素材(2) → 选择标题(3) → 确认生成(4) → 选择封面(5)
* 原步骤"选择配音"已从主流程移除,改为 Step1 下一步分支弹窗(VoiceSelectModal / ScriptSelectModal → TtsVoiceModal)。
*/
import React from "react"
import type { CoverConfig } from "../types/cover"
import type { TitleSettings } from "../types"
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 Step3VoiceWithMode from "./Step3VoiceWithMode"
import type { BatchTaskState } from "../hooks/generate-video/useGenerationPolling"
import type { GeneratedVideo } from "@/api/template-editor"
import type { TitleTemplate } from "@/components/title/template-types"
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
onUpdateStyle?: (patch: Partial<TitleSettings>) => void
activePreset: string | null
titlePresets: Array<{
key: string
label: string
emoji?: string
style: Record<string, unknown>
}>
enableTemplates?: boolean
selectedTemplateId?: string | null
onApplyTemplate?: (settings: import("../types").TitleSettings, template: TitleTemplate) => void
/* ── 封面 ── */
coverSettings: CoverConfig
onCoverSettingsChange: (settings: CoverConfig) => void
/* ── 配音 ── */
selectedVoice: string
onSelectedVoiceChange: (id: string) => 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[]
selectedCoverTemplate?: string
onSelectedCoverTemplateChange?: (templateId: string) => void
/** 单视频任务 ID(兜底,awaiting_cover 状态下 results 接口未入库时用) */
currentTaskId?: string
/** Step3 右上角确认生成按钮 */
onConfirmGenerate?: () => void | Promise<void>
}
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,
onUpdateStyle,
enableTemplates,
selectedTemplateId,
onApplyTemplate,
activePreset,
titlePresets,
coverSettings,
onCoverSettingsChange,
generating,
generateError,
progress,
onRetry,
generatedVideos,
batchTasks,
onRetryBatchTask,
previewTitles,
onPreviewTitlesChange,
previewCovers,
onPreviewCoversChange,
selectedVariantIds,
selectedCoverTemplate,
onSelectedCoverTemplateChange,
onConfirmGenerate,
selectedVoice,
onSelectedVoiceChange,
voiceModePerVideo,
onVoiceModePerVideoChange,
voiceLibraryIds,
onVoiceLibraryIdsChange,
} = props
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}
/>
)
case 3:
return (
<>
<Step4TitleSettings
titleSettings={titleSettings}
onTitleSettingsChange={onTitleSettingsChange}
onUpdatePosition={onUpdatePosition}
onUpdateFont={onUpdateFont}
onUpdateSize={onUpdateSize}
onToggleBold={onToggleBold}
onToggleItalic={onToggleItalic}
onToggleStroke={onToggleStroke}
onToggleShadow={onToggleShadow}
onApplyPreset={onApplyPreset}
onUpdateStyle={onUpdateStyle}
activePreset={activePreset}
titlePresets={titlePresets}
enableTemplates={enableTemplates}
selectedTemplateId={selectedTemplateId}
onApplyTemplate={onApplyTemplate}
previewCount={previewCount}
previewTitles={previewTitles}
onPreviewTitlesChange={onPreviewTitlesChange}
onConfirmGenerate={onConfirmGenerate}
generating={props.generating}
selectedCount={
props.previewCount && props.previewCount > 1
? props.selectedVariantIds?.length || 1
: 1
}
/>
{/* 批量配音选择:共用/独立切换(#2096) */}
<Step3VoiceWithMode
previewCount={previewCount}
selectedVoice={selectedVoice}
onSelectedVoiceChange={onSelectedVoiceChange}
voiceModePerVideo={voiceModePerVideo}
onVoiceModePerVideoChange={onVoiceModePerVideoChange}
voiceLibraryIds={voiceLibraryIds}
onVoiceLibraryIdsChange={onVoiceLibraryIdsChange}
/>
</>
)
case 4:
return (
<div className="xx-form-section">
{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}
selectedTemplate={selectedCoverTemplate}
onTemplateChange={onSelectedCoverTemplateChange}
currentTaskId={props.currentTaskId}
/>
)
default:
return null
}
}
export default GenerateStepContent