style: reformat GeneratePage.tsx with repo prettier config (#1899 #1900)

This commit is contained in:
2026-09-14 21:37:22 +08:00
parent e40175d358
commit 5a9c97d64c
+130 -163
View File
@@ -4,43 +4,37 @@
* 模板选择已删除,模板在后台自动加载/选择(后端 API 兼容)。
* 新增片段数量输入(默认3),系统按片段数智能匹配素材。
*/
import React, {
useMemo,
useState,
useEffect,
useRef,
useCallback,
} 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";
import FrontendPreviewPlayer from "./components/FrontendPreviewPlayer";
import CanvasPreviewGrid from "./components/CanvasPreviewGrid";
import PreviewCountModal from "./components/PreviewCountModal";
import GenerateStepsBar from "./components/GenerateStepsBar";
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 React, { useMemo, useState, useEffect, useRef, useCallback } 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"
import FrontendPreviewPlayer from "./components/FrontendPreviewPlayer"
import CanvasPreviewGrid from "./components/CanvasPreviewGrid"
import PreviewCountModal from "./components/PreviewCountModal"
import GenerateStepsBar from "./components/GenerateStepsBar"
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 { usePreviewAssets } from "./hooks/usePreviewAssets";
import { useBatchVariantPlans } from "./hooks/useBatchVariantPlans";
import { useVariantVoicePreview } from "./hooks/useVariantVoicePreview";
import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters";
import { getAssetsByKind } from "@/api/assets";
import { previewTts } from "@/api/tts";
import "./generate.css";
import { usePreviewAssets } from "./hooks/usePreviewAssets"
import { useBatchVariantPlans } from "./hooks/useBatchVariantPlans"
import { useVariantVoicePreview } from "./hooks/useVariantVoicePreview"
import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters"
import { getAssetsByKind } from "@/api/assets"
import { previewTts } from "@/api/tts"
import "./generate.css"
const GeneratePage: React.FC = () => {
const navigate = useNavigate();
const navigate = useNavigate()
/* ── 表单状态 ── */
const formState = useGenerateFormState();
const formState = useGenerateFormState()
const {
currentStep,
setCurrentStep,
@@ -89,97 +83,92 @@ const GeneratePage: React.FC = () => {
setPreviewCovers,
selectedVariantIds,
setSelectedVariantIds,
} = formState;
} = formState
const isBatch = previewCount > 1;
const isBatch = previewCount > 1
/* ── 配音选择同步 ── */
const prevVoiceSyncRef = useRef({
voice: selectedVoice,
batch: isBatch,
perVideo: voiceModePerVideo,
});
})
useEffect(() => {
const prev = prevVoiceSyncRef.current;
const voiceChanged = prev.voice !== selectedVoice;
const modeChanged =
prev.batch !== isBatch || prev.perVideo !== voiceModePerVideo;
const prev = prevVoiceSyncRef.current
const voiceChanged = prev.voice !== selectedVoice
const modeChanged = prev.batch !== isBatch || prev.perVideo !== voiceModePerVideo
prevVoiceSyncRef.current = {
voice: selectedVoice,
batch: isBatch,
perVideo: voiceModePerVideo,
};
if (!voiceChanged && !modeChanged) return;
if (!isBatch) return;
}
if (!voiceChanged && !modeChanged) return
if (!isBatch) return
if (!voiceModePerVideo) {
setVoiceLibraryIds((prevIds) =>
(prevIds || []).map((id) => id || selectedVoice),
);
setVoiceLibraryIds((prevIds) => (prevIds || []).map((id) => id || selectedVoice))
} else if (voiceChanged) {
setVoiceLibraryIds((prevIds) =>
(prevIds || []).map((id, i) => (i === 0 ? selectedVoice : id)),
);
)
}
}, [selectedVoice, isBatch, voiceModePerVideo, setVoiceLibraryIds]);
}, [selectedVoice, isBatch, voiceModePerVideo, setVoiceLibraryIds])
/* ── 数量选择弹窗 ── */
const [countModalOpen, setCountModalOpen] = useState(false);
const [countModalOpen, setCountModalOpen] = useState(false)
/* ── 标题样式回调 ── */
const styleUpdaters = useTitleStyleUpdaters({
titleSettings,
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);
const variant0Title = isBatch ? previewTitles?.[0] || "" : "";
const [previewVoiceAudioUrl, setPreviewVoiceAudioUrl] = useState<string | null>(null)
const ttsAbortRef = useRef<AbortController | null>(null)
const variant0Title = isBatch ? previewTitles?.[0] || "" : ""
useEffect(() => {
if (isBatch) return;
const voiceAsset = voiceMaterials.find((m) => m.id === selectedVoice);
if (isBatch) return
const voiceAsset = voiceMaterials.find((m) => m.id === selectedVoice)
if (voiceAsset?.file_url) {
setPreviewVoiceAudioUrl(voiceAsset.file_url);
return;
setPreviewVoiceAudioUrl(voiceAsset.file_url)
return
}
const ttsTitle = titleSettings.title;
const voiceId = selectedClonedVoice || selectedVoice;
const ttsTitle = titleSettings.title
const voiceId = selectedClonedVoice || selectedVoice
if (!voiceId || !ttsTitle) {
setPreviewVoiceAudioUrl(null);
return;
setPreviewVoiceAudioUrl(null)
return
}
ttsAbortRef.current?.abort();
const controller = new AbortController();
ttsAbortRef.current = controller;
let cancelled = false;
ttsAbortRef.current?.abort()
const controller = new AbortController()
ttsAbortRef.current = controller
let cancelled = false
previewTts({ text: ttsTitle, voice_id: voiceId })
.then((res) => {
if (!cancelled && res.audio_url) {
setPreviewVoiceAudioUrl(res.audio_url);
setPreviewVoiceAudioUrl(res.audio_url)
}
})
.catch((err) => {
if (!cancelled) {
console.warn("[预览配音生成失败]", err);
setPreviewVoiceAudioUrl(null);
console.warn("[预览配音生成失败]", err)
setPreviewVoiceAudioUrl(null)
}
});
})
return () => {
cancelled = true;
controller.abort();
};
cancelled = true
controller.abort()
}
}, [
selectedVoice,
selectedClonedVoice,
@@ -187,22 +176,22 @@ const GeneratePage: React.FC = () => {
variant0Title,
isBatch,
voiceMaterials,
]);
])
/* ── 克隆声音 ── */
const { addClone } = useCloneProgress();
const { addClone } = useCloneProgress()
const handleCloneSuccess = (voice: VoiceClone) => {
addClone(voice);
setCloneModalOpen(false);
message.success("音色克隆成功!");
};
addClone(voice)
setCloneModalOpen(false)
message.success("音色克隆成功!")
}
/* ── 素材 ID 列表 ── */
const previewAssetIds = useMemo(
() => (materialMode === "auto" ? smartSelectedIds : selectedMaterials),
[materialMode, smartSelectedIds, selectedMaterials],
);
)
/* ── BGM 配置 ── */
const bgmConfig = useMemo(
@@ -211,25 +200,23 @@ const GeneratePage: React.FC = () => {
music_id: "",
}),
[bgm],
);
)
/* ── 加载素材详情 ── */
const previewAssetsEnabled = previewAssetIds.length > 0;
const previewAssetsEnabled = previewAssetIds.length > 0
const {
assets: previewAssets,
ready: previewAssetsReady,
ensureAssets,
} = usePreviewAssets(previewAssetIds, previewAssetsEnabled);
} = usePreviewAssets(previewAssetIds, previewAssetsEnabled)
/* ── 预览就绪 ── */
// #1899: 不再依赖 currentTemplate,素材加载完即可预览
const previewReady = previewAssetsReady;
const previewReady = previewAssetsReady
/* ── 批量变体真实片段(#1744) ── */
const batchVoiceLibraryId =
voiceMode === "clone"
? selectedClonedVoice || selectedVoice || ""
: selectedVoice || "";
voiceMode === "clone" ? selectedClonedVoice || selectedVoice || "" : selectedVoice || ""
const {
clipsByVariant: variantClips,
planIdsByVariant: variantPlanIds,
@@ -246,7 +233,7 @@ const GeneratePage: React.FC = () => {
voiceLibraryId: batchVoiceLibraryId,
voiceLibraryIds: voiceLibraryIds || [],
voiceModePerVideo,
});
})
/* ── 批量变体配音预览 URL ── */
const variantVoiceAudioUrls = useVariantVoicePreview({
@@ -257,33 +244,29 @@ const GeneratePage: React.FC = () => {
clonedVoiceId: selectedClonedVoice || "",
variantVoiceIds: voiceLibraryIds || [],
titles: previewTitles || [],
});
})
/* ── 变体 clips 引用素材补拉 ── */
const clipAssetIds = useMemo(() => {
if (!isBatch || !variantClips?.length) return [];
const ids = new Set<string>();
variantClips.forEach((list) =>
list.forEach((c) => c.asset_id && ids.add(c.asset_id)),
);
return Array.from(ids);
}, [isBatch, variantClips]);
if (!isBatch || !variantClips?.length) return []
const ids = new Set<string>()
variantClips.forEach((list) => list.forEach((c) => c.asset_id && ids.add(c.asset_id)))
return Array.from(ids)
}, [isBatch, variantClips])
useEffect(() => {
if (clipAssetIds.length > 0) void ensureAssets(clipAssetIds);
}, [clipAssetIds, ensureAssets]);
if (clipAssetIds.length > 0) void ensureAssets(clipAssetIds)
}, [clipAssetIds, ensureAssets])
/* ── 勾选变体 ── */
const toggleVariantSelect = useCallback(
(index: number) => {
setSelectedVariantIds((prev) => {
const list = prev || [];
return list.includes(index)
? list.filter((i) => i !== index)
: [...list, index].sort();
});
const list = prev || []
return list.includes(index) ? list.filter((i) => i !== index) : [...list, index].sort()
})
},
[setSelectedVariantIds],
);
)
/* ── 视频生成核心逻辑 ── */
const {
@@ -325,37 +308,31 @@ const GeneratePage: React.FC = () => {
variantCoverUrls: previewCovers,
selectedVariantIndexes: isBatch ? selectedVariantIds : undefined,
onGenerationSuccess: () => {
setPreviewTaskId(null);
setStoredSourceEditPlanId(null);
setPreviewTaskId(null)
setStoredSourceEditPlanId(null)
},
});
})
/* ── 数量弹窗确认 ── */
const handleCountConfirm = useCallback(
(count: number) => {
setPreviewCount(count);
setCountModalOpen(false);
setPreviewCount(count)
setCountModalOpen(false)
setPreviewTitles((prev) => {
const list = prev || [];
const base = list[0] || titleSettings.title || "";
return Array.from(
{ length: count },
(_, i) => list[i] ?? (i === 0 ? base : ""),
);
});
const list = prev || []
const base = list[0] || titleSettings.title || ""
return Array.from({ length: count }, (_, i) => list[i] ?? (i === 0 ? base : ""))
})
setVoiceLibraryIds((prev) => {
const list = prev || [];
return Array.from(
{ length: count },
(_, i) => list[i] ?? selectedVoice ?? "",
);
});
const list = prev || []
return Array.from({ length: count }, (_, i) => list[i] ?? selectedVoice ?? "")
})
setPreviewCovers((prev) => {
const list = prev || [];
return Array.from({ length: count }, (_, i) => list[i] ?? "");
});
setSelectedVariantIds(Array.from({ length: count }, (_, i) => i));
setCurrentStep(2);
const list = prev || []
return Array.from({ length: count }, (_, i) => list[i] ?? "")
})
setSelectedVariantIds(Array.from({ length: count }, (_, i) => i))
setCurrentStep(2)
},
[
setPreviewCount,
@@ -367,33 +344,33 @@ const GeneratePage: React.FC = () => {
titleSettings.title,
selectedVoice,
],
);
)
/* ── 步骤3「确认生成视频」:校验通过 → 创建正式生成任务 → 跳步骤4看实时进展 ── */
const handleConfirmGenerate = useCallback(async () => {
if (isBatch) {
if (selectedVariantIds.length === 0) {
message.warning("请至少勾选一个视频");
return;
message.warning("请至少勾选一个视频")
return
}
const missing = selectedVariantIds.some((i) => !previewTitles[i]?.trim());
const missing = selectedVariantIds.some((i) => !previewTitles[i]?.trim())
if (missing) {
message.warning("请为每个勾选的视频输入标题");
return;
message.warning("请为每个勾选的视频输入标题")
return
}
} else if (!titleSettings.title?.trim()) {
if (!titleSettings.aiAutoSelect) {
message.warning("请先选择或输入标题");
return;
message.warning("请先选择或输入标题")
return
}
}
if (!previewReady) {
message.warning("预览素材正在加载,请稍候");
return;
message.warning("预览素材正在加载,请稍候")
return
}
const ok = await handleGenerate();
const ok = await handleGenerate()
if (ok) {
setCurrentStep(4);
setCurrentStep(4)
}
}, [
isBatch,
@@ -404,7 +381,7 @@ const GeneratePage: React.FC = () => {
previewReady,
handleGenerate,
setCurrentStep,
]);
])
/* ── 步骤导航 ── */
const { goNext, goPrev } = useStepNavigation({
@@ -416,16 +393,16 @@ const GeneratePage: React.FC = () => {
titleSettings,
generated,
onOpenCountModal: () => setCountModalOpen(true),
});
})
/* ── 最终成片 ── */
const finalVideo = generatedVideos[0];
const finalVideo = generatedVideos[0]
/* ── 布局 class ── */
const layoutClassName = useMemo(() => {
if (currentStep === 3) return "xx-generate-layout step4-layout";
return "xx-generate-layout full-width";
}, [currentStep]);
if (currentStep === 3) return "xx-generate-layout step4-layout"
return "xx-generate-layout full-width"
}, [currentStep])
/* ================================================================
渲染
@@ -435,10 +412,7 @@ const GeneratePage: React.FC = () => {
<div className="xx-generate-page">
<GenerateHeader fromEditPlan={!!editPlanId} />
<GenerateStepsBar
currentStep={currentStep}
onStepClick={setCurrentStep}
/>
<GenerateStepsBar currentStep={currentStep} onStepClick={setCurrentStep} />
<div className={layoutClassName}>
{/* ════ 步骤3:左侧预览大区域 ════ */}
@@ -456,8 +430,7 @@ const GeneratePage: React.FC = () => {
size: titleSettings.size,
font: titleSettings.font,
color: titleSettings.color,
position: titleSettings.position as
"top" | "center" | "bottom" | "custom",
position: titleSettings.position as "top" | "center" | "bottom" | "custom",
bold: titleSettings.bold,
italic: titleSettings.italic,
stroke: titleSettings.stroke,
@@ -607,16 +580,10 @@ const GeneratePage: React.FC = () => {
justifyContent: "center",
}}
>
<button
className="xx-btn xx-btn-ghost xx-btn-sm"
onClick={handleDownload}
>
<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 className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleShare}>
🔗 分享
</button>
<button
@@ -658,7 +625,7 @@ const GeneratePage: React.FC = () => {
onSuccess={handleCloneSuccess}
/>
</div>
);
};
)
}
export default GeneratePage;
export default GeneratePage