style: format GeneratePage.tsx with prettier (#1899 #1900)

This commit is contained in:
2026-09-14 21:23:24 +08:00
parent d201d6fd91
commit 5d3ef34857
+188 -133
View File
@@ -4,37 +4,43 @@
* 模板选择已删除,模板在后台自动加载/选择(后端 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,
@@ -83,104 +89,120 @@ 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
prevVoiceSyncRef.current = { voice: selectedVoice, batch: isBatch, perVideo: voiceModePerVideo }
if (!voiceChanged && !modeChanged) return
if (!isBatch) return
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 (!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()
}
}, [selectedVoice, selectedClonedVoice, titleSettings.title, variant0Title, isBatch, voiceMaterials])
cancelled = true;
controller.abort();
};
}, [
selectedVoice,
selectedClonedVoice,
titleSettings.title,
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(
@@ -189,23 +211,25 @@ 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,
@@ -222,7 +246,7 @@ const GeneratePage: React.FC = () => {
voiceLibraryId: batchVoiceLibraryId,
voiceLibraryIds: voiceLibraryIds || [],
voiceModePerVideo,
})
});
/* ── 批量变体配音预览 URL ── */
const variantVoiceAudioUrls = useVariantVoicePreview({
@@ -233,29 +257,33 @@ 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 {
@@ -297,31 +325,37 @@ 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,
@@ -333,33 +367,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,
@@ -370,7 +404,7 @@ const GeneratePage: React.FC = () => {
previewReady,
handleGenerate,
setCurrentStep,
])
]);
/* ── 步骤导航 ── */
const { goNext, goPrev } = useStepNavigation({
@@ -382,16 +416,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]);
/* ================================================================
渲染
@@ -401,7 +435,10 @@ 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:左侧预览大区域 ════ */}
@@ -419,7 +456,8 @@ 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,
@@ -433,7 +471,12 @@ const GeneratePage: React.FC = () => {
<div className="xx-form-section">
<div className="xx-preview-header">
<h3>🎬 {previewCount} 个视频预览</h3>
<span style={{ fontSize: 13, color: "var(--text-secondary, #666)" }}>
<span
style={{
fontSize: 13,
color: "var(--text-secondary, #666)",
}}
>
实时预览,勾选要生成的视频
</span>
</div>
@@ -556,11 +599,24 @@ const GeneratePage: React.FC = () => {
}}
poster={finalVideo.thumbnail_url || undefined}
/>
<div style={{ display: "flex", gap: 8, marginTop: 12, justifyContent: "center" }}>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleDownload}>
<div
style={{
display: "flex",
gap: 8,
marginTop: 12,
justifyContent: "center",
}}
>
<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
@@ -602,8 +658,7 @@ const GeneratePage: React.FC = () => {
onSuccess={handleCloneSuccess}
/>
</div>
)
}
export default GeneratePage
);
};
export default GeneratePage;