From b50cdb5943f848c294c50498edba6f6a2ada5cb9 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 24 Jul 2026 00:45:16 +0800 Subject: [PATCH] feat(generate): add Step4-7 components and hooks - Step4TitleSettings + useStep4Title: title settings with AI generation and styling - Step5VoiceSelect + useStep5Voice: voice selection with preset/TTS/clone modes - Step6CoverSettings + useStep6Cover: cover settings with auto/frame/upload modes - Step7ConfirmGenerate + useStep7Generate: generation confirmation and progress Part of GeneratePage Phase 2 refactoring. --- .../components/Step4TitleSettings.tsx | 298 ++++++++++ .../generate/components/Step5VoiceSelect.tsx | 533 ++++++++++++++++++ .../components/Step6CoverSettings.tsx | 191 +++++++ .../components/Step7ConfirmGenerate.tsx | 198 +++++++ 4 files changed, 1220 insertions(+) create mode 100644 apps/web/src/pages/generate/components/Step4TitleSettings.tsx create mode 100644 apps/web/src/pages/generate/components/Step5VoiceSelect.tsx create mode 100644 apps/web/src/pages/generate/components/Step6CoverSettings.tsx create mode 100644 apps/web/src/pages/generate/components/Step7ConfirmGenerate.tsx diff --git a/apps/web/src/pages/generate/components/Step4TitleSettings.tsx b/apps/web/src/pages/generate/components/Step4TitleSettings.tsx new file mode 100644 index 000000000..11125beed --- /dev/null +++ b/apps/web/src/pages/generate/components/Step4TitleSettings.tsx @@ -0,0 +1,298 @@ +/** + * Step 4 标题设置组件 + */ +import React from "react" +import { Select } from "antd" +import { LoadingOutlined, CheckCircleFilled } from "@ant-design/icons" +import { POSITION_OPTIONS, FONT_OPTIONS } from "../constants" +import type { TitleSettings } from "../types" +import { useStep4Title } from "../hooks/useStep4Title" + +interface Step4TitleSettingsProps { + titleSettings: TitleSettings + onTitleSettingsChange: (settings: TitleSettings) => void +} + +const Step4TitleSettings: React.FC = (props) => { + const { + userTitles, + titleSettings, + aiTitleInput, + setAiTitleInput, + aiTitleGenerating, + aiTitleResults, + hasGeneratedTitles, + activePreset, + titlePresets, + handleGenerateAiTitles, + handleSelectAiTitle, + handleRefreshAiTitles, + updateTitle, + toggleAiAutoSelect, + updatePosition, + updateFont, + updateSize, + toggleBold, + toggleItalic, + toggleStroke, + toggleShadow, + applyPreset, + } = useStep4Title(props) + + return ( +
+

📝 选择标题

+ + {/* AI 智能生成标题 */} +
+
+ ✨ AI 智能生成标题 +
+
+ setAiTitleInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleGenerateAiTitles() + }} + /> + +
+ + {/* 生成结果 */} + {hasGeneratedTitles && !aiTitleGenerating && aiTitleResults.length > 0 && ( +
+
+ + 为你生成 {aiTitleResults.length} 个标题 + + +
+
+ {aiTitleResults.map((item, idx) => { + const isSelected = titleSettings.title === item.title + return ( +
handleSelectAiTitle(item.title)} + > +
{item.title}
+
{item.highlight}
+ {isSelected && ( +
+ +
+ )} +
+ ) + })} +
+
+ )} + + {/* 生成中 */} + {aiTitleGenerating && ( +
+ + AI 正在为你创作标题… +
+ )} +
+ +
+ 或手动选择 +
+ + {/* AI 自动选择开关 */} +
+ AI 自动选择标题 +
+
+
+
+ + {!titleSettings.aiAutoSelect && ( + <> + {/* 标题内容选择 */} +
+ + updateTitle(e.target.value)} + maxLength={50} + /> +
+ + {/* 标题样式设置区 */} +
+

标题样式

+ + {/* 位置 + 字体 一行 */} +
+
+ + +
+
+ + +
+
+ + {/* 字号滑块 */} +
+
+ + {titleSettings.size}px +
+ updateSize(Number(e.target.value))} + /> +
+ + {/* 预设样式 */} +
+ +
+ {titlePresets.map((p) => { + const isActive = activePreset === p.key + return ( + + ) + })} +
+
+ + {/* 样式按钮:粗体/斜体/描边/阴影 */} +
+ +
+ + + + +
+
+
+ + )} +
+ ) +} + +export default Step4TitleSettings diff --git a/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx b/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx new file mode 100644 index 000000000..c179ec3b5 --- /dev/null +++ b/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx @@ -0,0 +1,533 @@ +/** + * Step 5 配音选择组件 + */ +import React from "react" +import { Typography } from "antd" +import { + AudioOutlined, + ThunderboltOutlined, + CheckCircleFilled, + LoadingOutlined, + PlayCircleOutlined, + PauseCircleOutlined, + SaveOutlined, + PlusOutlined, + CloseOutlined, +} from "@ant-design/icons" +import type { VoiceClone } from "@/api/voice-clone" +import { useStep5Voice } from "../hooks/useStep5Voice" + +const { Text } = Typography + +interface Step5VoiceSelectProps { + selectedVoice: string + onSelectedVoiceChange: (voiceId: string) => void + voiceMode: "preset" | "custom" | "clone" + onVoiceModeChange: (mode: "preset" | "custom" | "clone") => void + selectedClonedVoice: string + onSelectedClonedVoiceChange: (voiceId: string) => void + clonedVoices: VoiceClone[] + addClone: (voice: VoiceClone) => void + hasProcessing: boolean + cloneModalOpen: boolean + onCloneModalOpenChange: (open: boolean) => void + titleText: string +} + +const Step5VoiceSelect: React.FC = (props) => { + const { + presetVoices, + presetVoicesLoading, + clonedVoices, + hasProcessing, + voiceMode, + selectedVoice, + selectedClonedVoice, + voiceRecommendLoading, + voiceRecommendations, + hasVoiceRecommend, + handleVoiceRecommend, + handleSelectRecommendedVoice, + playingVoice, + toggleVoicePlay, + handleSelectPresetVoice, + handleSelectCloneVoice, + customVoiceText, + setCustomVoiceText, + customAudioUrl, + ttsError, + completedTtsJobId, + synthesizeMutation, + handleSynthesizeVoice, + saveModalOpen, + setSaveModalOpen, + saveName, + setSaveName, + saveTagIds, + setSaveTagIds, + saveNewTag, + setSaveNewTag, + allTags, + saveToLibraryMutation, + handleOpenSaveModal, + handleConfirmSave, + handleAddTagInModal, + cloneModalOpen, + handleOpenCloneModal, + handleCloneSuccess, + handleSelectClonedVoice, + VOICE_GENDER_ICON, + CLONE_STATUS_CONFIG, + formatDuration, + } = useStep5Voice(props) + + return ( +
+

🎙️ 选择配音

+ + {/* AI 智能推荐配音 */} +
+
+ ✨ AI 智能推荐 + +
+ + {voiceRecommendLoading && ( +
+ + 根据视频内容为你匹配最合适的音色… +
+ )} + + {!voiceRecommendLoading && hasVoiceRecommend && voiceRecommendations.length > 0 && ( +
+ {voiceRecommendations.map((voiceId) => { + const v = presetVoices.find((pv) => pv.voice_id === voiceId) + if (!v) return null + const isSelected = voiceMode === "preset" && selectedVoice === v.voice_id + return ( +
handleSelectRecommendedVoice(v.voice_id)} + > +
+ {VOICE_GENDER_ICON[v.gender] ?? "✨"} +
+
+
{v.name}
+
{v.description}
+
+ {isSelected && ( +
+ +
+ )} +
+ ) + })} +
+ )} + + {!voiceRecommendLoading && !hasVoiceRecommend && ( +
+ 点击「智能推荐」,AI 根据视频内容匹配音色 +
+ )} +
+ +
+ 全部音色 +
+ + {/* 配音方式选择卡片 — 从 API 预设音色动态生成 */} +
+ {presetVoices.slice(0, 3).map((v) => ( +
handleSelectPresetVoice(v.voice_id)} + role="button" + tabIndex={0} + aria-pressed={voiceMode === "preset" && selectedVoice === v.voice_id} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + handleSelectPresetVoice(v.voice_id) + } + }} + > + ✓ +
{VOICE_GENDER_ICON[v.gender] ?? "✨"}
+
+

{v.name}

+

{v.description}

+
+
+ ))} + {presetVoicesLoading && ( +
+
⏳
+
+

加载中…

+
+
+ )} + {/* 克隆我的声音 */} +
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + handleSelectCloneVoice() + } + }} + > + ✓ +
+ 🎤 +
+
+

克隆我的声音

+

上传语音样本克隆

+
+
+
+ + {/* 预设音色详细列表(当 voiceMode === preset 时显示 API 返回的音色) */} + {voiceMode === "preset" && ( +
+
+ + +
+ {/* 试听按钮 */} + {presetVoices.length > 0 && ( +
+ {presetVoices.slice(0, 4).map((v) => ( + + ))} +
+ )} +
+ )} + + {/* 自定义录制 */} + {voiceMode === "custom" && ( +
+