Files
xiaoxia-saas/apps/web/src/pages/generate/GeneratePage.tsx
T
灵应 5104a56578
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
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 / ACR Image Cleanup (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 / PR Build Worker Image (push) Failing after 721h11m17s
CI/CD Pipeline / PR Build API Image (push) Failing after 721h11m18s
CI/CD Pipeline / Frontend Lint (push) Failing after 721h11m18s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 721h11m19s
CI/CD Pipeline / PR Build Web Image (push) Failing after 721h44m54s
cleanup: 删除Canvas预览代码,简化PreviewVideoPanel (#1402)
2026-08-17 14:15:12 +08:00

301 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 智能剪辑页面 — V22 多预览 + 配音前置
* 7 步向导:选择模板 → 选择素材 → 选择配音 → 选择标题 → 生成预览 → 选择封面 → 确认生成
* 左右布局:左侧 generate-form + 右侧 generate-preview
* 主组件仅保留整体布局与事件编排
* 状态管理 → hooks/useGenerateFormState
* 步骤导航 → hooks/useStepNavigation
* 步骤内容 → components/GenerateStepContent
* 底部按钮 → components/GenerateStepActions
* 生成核心逻辑 → hooks/useGenerateVideo
*/
import React, { useState, useMemo } from "react"
import { Modal, message } from "antd"
import { useNavigate } from "react-router-dom"
import type { VoiceClone } from "@/api/voice-clone"
import { useCloneProgress } from "@/hooks/useCloneProgress"
import CloneModal from "@/components/voice/CloneModal"
import GenerateHeader from "./components/GenerateHeader"
import GenerateStepsBar from "./components/GenerateStepsBar"
import GenerateResultPanel from "./components/GenerateResultPanel"
import PreviewVideoPanel from "./components/PreviewVideoPanel"
import GenerateStepContent from "./components/GenerateStepContent"
import GenerateStepActions from "./components/GenerateStepActions"
import { useGenerateFormState } from "./hooks/useGenerateFormState"
import { useStepNavigation } from "./hooks/useStepNavigation"
import { useGenerateVideo } from "./hooks/useGenerateVideo"
import { useStep5Preview } from "./hooks/useStep5Preview"
import "./generate.css"
const GeneratePage: React.FC = () => {
const navigate = useNavigate()
/* ── 表单状态 ── */
const formState = useGenerateFormState()
const {
currentStep,
setCurrentStep,
selectedTemplate,
setSelectedTemplate,
userTemplates,
selectedMaterials,
setSelectedMaterials,
materialMode,
setMaterialMode,
smartSelectedIds,
setSmartSelectedIds,
titleSettings,
setTitleSettings,
coverSettings,
setCoverSettings,
selectedVoice,
setSelectedVoice,
voiceMode,
setVoiceMode,
selectedClonedVoice,
setSelectedClonedVoice,
presetVoices,
cloneModalOpen,
setCloneModalOpen,
generateCount,
setGenerateCount,
videoRatio,
duration,
style,
autoSubtitles,
bgm,
editPlanId,
previewVideo,
setPreviewVideo,
previewModalOpen,
setPreviewModalOpen,
} = formState
/* ── 克隆声音 ── */
const { clones: clonedVoices, addClone, hasProcessing } = useCloneProgress()
const handleCloneSuccess = (voice: VoiceClone) => {
addClone(voice)
setCloneModalOpen(false)
message.success("音色克隆成功!")
}
/* ── 预览数量(多预览) ── */
const [previewCount, setPreviewCount] = useState(1)
/* ── 根据 voiceMode 构建 voiceIds 传给预览接口 ── */
/* selectedVoice / selectedClonedVoice 均为 string 类型(voice ID),
见 useGenerateFormState 返回值类型定义 */
const previewVoiceIds = useMemo((): string[] => {
if (voiceMode === "clone") {
const id: string = selectedClonedVoice
return id ? [id] : []
}
// preset / custom 模式
const id: string = selectedVoice
return id ? [id] : []
}, [voiceMode, selectedVoice, selectedClonedVoice])
/* ── Step5 预览生成(多预览 + voice_ids) ── */
const step5Preview = useStep5Preview({
templates: userTemplates,
selectedTemplate,
materialMode,
selectedMaterials,
smartSelectedIds,
duration,
videoRatio,
voiceIds: previewVoiceIds,
voiceLibraryId: selectedVoice || undefined,
previewCount,
titleSettings,
})
/* ── 步骤导航 ── */
const { goNext, goPrev } = useStepNavigation({
currentStep,
setCurrentStep,
selectedTemplate,
materialMode,
selectedMaterials,
smartSelectedIds,
titleSettings,
previewReady: step5Preview.canProceed,
})
/* ── 视频生成核心逻辑 ── */
const {
generating,
progress,
generated,
generateError,
generatedVideos,
generate: handleGenerate,
retry: handleRetryGenerate,
dismissError: handleDismissError,
download: handleDownload,
share: handleShare,
} = useGenerateVideo({
titleSettings,
selectedTemplate,
selectedMaterials,
materialMode,
smartSelectedIds,
voiceMode,
selectedVoice,
selectedClonedVoice,
coverSettings,
videoRatio,
style,
duration,
autoSubtitles,
bgm,
generateCount,
previewTaskId: step5Preview.selectedTaskId,
})
/* ================================================================
渲染 — 主页面
================================================================ */
return (
<div className="xx-generate-page">
{/* ── 页头 ── */}
<GenerateHeader fromEditPlan={!!editPlanId} />
{/* ── 步骤条 ── */}
<GenerateStepsBar currentStep={currentStep} onStepClick={setCurrentStep} />
{/* ── 主布局 ── */}
<div className="xx-generate-layout">
{/* ════ 左侧:表单区 ════ */}
<div className="xx-generate-form">
<GenerateStepContent
currentStep={currentStep}
userTemplates={userTemplates}
selectedTemplate={selectedTemplate}
onSelectTemplate={setSelectedTemplate}
materialMode={materialMode}
onMaterialModeChange={setMaterialMode}
selectedMaterials={selectedMaterials}
onSelectedMaterialsChange={setSelectedMaterials}
smartSelectedIds={smartSelectedIds}
onSmartSelectedIdsChange={setSmartSelectedIds}
titleSettings={titleSettings}
onTitleSettingsChange={setTitleSettings}
coverSettings={coverSettings}
onCoverSettingsChange={setCoverSettings}
duration={duration}
selectedVoice={selectedVoice}
onSelectedVoiceChange={setSelectedVoice}
voiceMode={voiceMode}
onVoiceModeChange={setVoiceMode}
selectedClonedVoice={selectedClonedVoice}
onSelectedClonedVoiceChange={setSelectedClonedVoice}
clonedVoices={clonedVoices}
addClone={addClone}
hasProcessing={hasProcessing}
cloneModalOpen={cloneModalOpen}
onCloneModalOpenChange={setCloneModalOpen}
generateCount={generateCount}
onGenerateCountChange={setGenerateCount}
generating={generating}
generated={generated}
generateError={generateError}
progress={progress}
generatedVideos={generatedVideos}
onRetry={handleRetryGenerate}
onDismissError={handleDismissError}
presetVoices={presetVoices}
videoRatio={videoRatio}
/* Step5 多预览 */
previewCount={previewCount}
onPreviewCountChange={setPreviewCount}
previewItems={step5Preview.items}
previewSelectedIndex={step5Preview.selectedIndex}
onSelectPreview={step5Preview.setSelectedIndex}
previewOverallStatus={step5Preview.previewStatus}
previewOverallError={step5Preview.previewError}
previewOverallProgress={step5Preview.progress}
previewAnyGenerating={step5Preview.anyGenerating}
onGeneratePreview={step5Preview.generatePreview}
onRegeneratePreview={step5Preview.regeneratePreview}
/>
<GenerateStepActions
currentStep={currentStep}
onPrev={goPrev}
onNext={goNext}
onGenerate={handleGenerate}
generating={generating}
generated={generated}
generateError={generateError}
/>
</div>
{/* ════ 右侧:预览 + 生成结果 ════ */}
<div className="xx-generate-right-col">
{/* 预览视频面板(Step4+ 显示) */}
{currentStep >= 4 && (
<PreviewVideoPanel
previewStatus={step5Preview.previewStatus}
previewResult={step5Preview.previewResult}
previewError={step5Preview.previewError}
progress={step5Preview.progress}
videoRatio={videoRatio}
onRegenerate={step5Preview.regeneratePreview}
/>
)}
{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")}
/>
)}
</div>
</div>
{/* ── 视频预览弹窗 ── */}
<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}
onClose={() => setCloneModalOpen(false)}
onSuccess={handleCloneSuccess}
/>
</div>
)
}
export default GeneratePage