diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState.ts deleted file mode 100644 index acd8e1aec..000000000 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * GeneratePage 表单状态管理 - * 集中管理 7 步向导的所有共享状态、API 加载、URL 参数解析 - */ -import { useState, useEffect, useMemo } from "react" -import { useQuery } from "@tanstack/react-query" -import { useSearchParams } from "react-router-dom" -import type { GeneratedVideo, TitleConfig } from "@/api/template-editor" -import type { EditingTemplate } from "@/api/editing-planner" -import { getEditPlan } from "@/api/template-editor" -import type { CoverConfig } from "../../editing-planner/types" -import { getEditingTemplates } from "@/api/editing-planner" -import type { PresetVoiceItem } from "@/api/voices" -import { fetchPresetVoices } from "@/api/voices" -import { DEFAULT_COVER_SETTINGS } from "../constants" -import type { TitleSettings } from "../types" - -const DEFAULT_TITLE_SETTINGS: TitleSettings = { - aiAutoSelect: false, - title: "", - position: "bottom", - font: "思源黑体", - size: 28, - bold: true, - italic: false, - stroke: true, - shadow: false, - color: "#ffffff", -} - -export interface GenerateFormState { - /* 步骤 */ - currentStep: number - setCurrentStep: (step: number | ((prev: number) => number)) => void - - /* 模板 */ - selectedTemplate: string - setSelectedTemplate: (id: string) => void - userTemplates: EditingTemplate[] - - /* 素材 */ - selectedMaterials: string[] - setSelectedMaterials: (ids: string[]) => void - materialMode: "manual" | "auto" - setMaterialMode: (mode: "manual" | "auto") => void - smartSelectedIds: string[] - setSmartSelectedIds: (ids: string[]) => void - - /* 标题 */ - titleSettings: TitleSettings - setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings)) => void - - /* 封面 */ - coverSettings: CoverConfig - setCoverSettings: (settings: CoverConfig | ((prev: CoverConfig) => CoverConfig)) => void - - /* 配音 */ - selectedVoice: string - setSelectedVoice: (id: string) => void - voiceMode: "preset" | "custom" | "clone" - setVoiceMode: (mode: "preset" | "custom" | "clone") => void - selectedClonedVoice: string - setSelectedClonedVoice: (id: string) => void - presetVoices: PresetVoiceItem[] - - /* 克隆弹窗 */ - cloneModalOpen: boolean - setCloneModalOpen: (open: boolean) => void - - /* 生成数量 */ - generateCount: number - setGenerateCount: (n: number) => void - - /* 高级设置 */ - videoRatio: string - duration: number - style: string - autoSubtitles: boolean - bgm: boolean - - /* URL 参数 */ - editPlanId: string | null - planConfigStr: string | null - - /* 预览弹窗 */ - previewVideo: GeneratedVideo | null - setPreviewVideo: (v: GeneratedVideo | null) => void - previewModalOpen: boolean - setPreviewModalOpen: (open: boolean) => void -} - -export const useGenerateFormState = (): GenerateFormState => { - const [searchParams] = useSearchParams() - const editPlanId = searchParams.get("edit_plan_id") - const planConfigStr = searchParams.get("plan_config") - - /* ── 步骤状态 ── */ - const [currentStep, setCurrentStep] = useState(1) - - /* ── 模板(从 API 加载) ── */ - const [selectedTemplate, setSelectedTemplate] = useState("") - const { data: userTemplates = [] } = useQuery({ - queryKey: ["generate-templates"], - queryFn: () => getEditingTemplates(), - staleTime: 60_000, - }) - /* 模板加载完成后自动选中第一个 */ - useEffect(() => { - if (userTemplates.length > 0 && !selectedTemplate) { - setSelectedTemplate(userTemplates[0].id) - } - }, [userTemplates, selectedTemplate]) - - /* ── 素材 ── */ - const [selectedMaterials, setSelectedMaterials] = useState([]) - const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual") - const [smartSelectedIds, setSmartSelectedIds] = useState([]) - - /* ── 标题设置 ── */ - const [titleSettings, setTitleSettings] = useState(DEFAULT_TITLE_SETTINGS) - - /* ── 封面设置 ── */ - const [coverSettings, setCoverSettings] = useState(DEFAULT_COVER_SETTINGS) - - /* 当选中模板开启了「AI自动匹配标题」,自动填入模板预设标题 / 封面 */ - useEffect(() => { - const tpl = userTemplates.find((t) => t.id === selectedTemplate) - if (tpl?.title_config) { - setTitleSettings((prev) => ({ - ...prev, - aiAutoSelect: tpl.title_config!.ai_auto_select, - title: tpl.title_config!.content || prev.title, - position: tpl.title_config!.position || prev.position, - font: tpl.title_config!.font_preset || prev.font, - size: tpl.title_config!.font_size || prev.size, - color: tpl.title_config!.font_color || prev.color, - })) - } - if (tpl?.cover_config) { - setCoverSettings((prev) => ({ - ...prev, - enabled: tpl.cover_config!.enabled ?? prev.enabled, - mode: (tpl.cover_config!.mode as CoverConfig["mode"]) || prev.mode, - frame_time: tpl.cover_config!.frame_time ?? prev.frame_time, - upload_url: tpl.cover_config!.upload_url || prev.upload_url, - ai_suggested_time: tpl.cover_config!.ai_suggested_time ?? prev.ai_suggested_time, - thumbnail_url: tpl.cover_config!.thumbnail_url || prev.thumbnail_url, - })) - } - }, [selectedTemplate, userTemplates]) - - /* ── 配音 ── */ - const [selectedVoice, setSelectedVoice] = useState("") - const [voiceMode, setVoiceMode] = useState<"preset" | "custom" | "clone">("preset") - const [selectedClonedVoice, setSelectedClonedVoice] = useState("") - - /* ── 预置音色 API ── */ - const { data: presetVoicesData } = useQuery({ - queryKey: ["preset-voices"], - queryFn: fetchPresetVoices, - }) - const presetVoices: PresetVoiceItem[] = useMemo( - () => presetVoicesData?.items ?? [], - [presetVoicesData], - ) - - /* ── 克隆声音弹窗 ── */ - const [cloneModalOpen, setCloneModalOpen] = useState(false) - - /* ── 生成数量 ── */ - const [generateCount, setGenerateCount] = useState(1) - - /* ── 高级设置(隐藏但保留) ── */ - const [videoRatio] = useState("16:9") - const [duration] = useState(30) - const [style] = useState("business") - const [autoSubtitles] = useState(true) - const [bgm] = useState(true) - - /* ── 预览弹窗 ── */ - const [previewVideo, setPreviewVideo] = useState(null) - const [previewModalOpen, setPreviewModalOpen] = useState(false) - - /** 解析 plan_config 并自动填充表单 */ - useEffect(() => { - if (!planConfigStr) return - try { - const config = JSON.parse(planConfigStr) as { - title_config?: { - content?: string - ai_auto_select?: boolean - position?: string - font_preset?: string - font_size?: number - font_color?: string - } - subtitle_config?: { enabled?: boolean } - bgm_config?: { enabled?: boolean; music_id?: string } - mode?: string - total_duration?: number - segments?: Array<{ media_asset_id?: string; material_type?: string }> - } - - if (config.title_config) { - const tc = config.title_config as TitleConfig - setTitleSettings((prev) => ({ - ...prev, - title: tc.content || "", - aiAutoSelect: tc.ai_auto_select || false, - position: tc.position || prev.position, - font: tc.font_preset || prev.font, - size: tc.font_size || prev.size, - color: tc.font_color || prev.color, - })) - } - if (config.segments && config.segments.length > 0) { - const assetIds = config.segments - .map((s) => s.media_asset_id) - .filter((id): id is string => !!id) - if (assetIds.length > 0) { - setSelectedMaterials(assetIds) - } - } - } catch (err) { - console.warn("解析 plan_config 失败:", err) - } - }, [planConfigStr]) - - /** 如果没有 plan_config,尝试通过 edit_plan_id 从后端拉取配置 */ - useEffect(() => { - if (!editPlanId || planConfigStr) return - const loadPlanConfig = async () => { - try { - const plan = await getEditPlan(editPlanId) - if (plan.name) setTitleSettings((prev) => ({ ...prev, title: plan.name })) - const cfg = plan.config - if (cfg?.title_config) { - setTitleSettings((prev) => ({ - ...prev, - aiAutoSelect: cfg.title_config!.ai_auto_select, - title: cfg.title_config!.content || prev.title, - position: cfg.title_config!.position || prev.position, - font: cfg.title_config!.font_preset || prev.font, - size: cfg.title_config!.font_size || prev.size, - color: cfg.title_config!.font_color || prev.color, - })) - } - if (cfg?.cover_config) { - const cc = cfg.cover_config as CoverConfig - setCoverSettings((prev) => ({ - ...prev, - enabled: cc.enabled ?? prev.enabled, - mode: cc.mode || prev.mode, - frame_time: cc.frame_time ?? prev.frame_time, - upload_url: cc.upload_url || prev.upload_url, - ai_suggested_time: cc.ai_suggested_time ?? prev.ai_suggested_time, - thumbnail_url: cc.thumbnail_url || prev.thumbnail_url, - })) - } - if (cfg?.asset_ids) { - setSelectedMaterials(cfg.asset_ids.filter((v): v is string => typeof v === "string")) - } - } catch (err) { - console.warn("加载模板草稿配置失败:", err) - } - } - loadPlanConfig() - }, [editPlanId, planConfigStr]) - - return { - 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, - planConfigStr, - previewVideo, - setPreviewVideo, - previewModalOpen, - setPreviewModalOpen, - } -} diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts new file mode 100755 index 000000000..91de56c79 --- /dev/null +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts @@ -0,0 +1,198 @@ +/** + * GeneratePage 表单状态管理 + * 集中管理 7 步向导的所有共享状态、API 加载、URL 参数解析 + */ +import { useState } from "react" +import { useSearchParams } from "react-router-dom" +import type { GeneratedVideo } from "@/api/template-editor" +import type { EditingTemplate } from "@/api/editing-planner" +import type { CoverConfig } from "../../../editing-planner/types" +import type { PresetVoiceItem } from "@/api/voices" +import { DEFAULT_COVER_SETTINGS } from "../../constants" +import type { TitleSettings } from "../../types" +import { useTemplateSelection } from "./useTemplateSelection" +import { useTitleCoverSync } from "./useTitleCoverSync" +import { useVoiceState } from "./useVoiceState" +import { usePlanConfigLoader } from "./usePlanConfigLoader" + +const DEFAULT_TITLE_SETTINGS: TitleSettings = { + aiAutoSelect: false, + title: "", + position: "bottom", + font: "思源黑体", + size: 28, + bold: true, + italic: false, + stroke: true, + shadow: false, + color: "#ffffff", +} + +export interface GenerateFormState { + /* 步骤 */ + currentStep: number + setCurrentStep: (step: number | ((prev: number) => number)) => void + + /* 模板 */ + selectedTemplate: string + setSelectedTemplate: (id: string) => void + userTemplates: EditingTemplate[] + + /* 素材 */ + selectedMaterials: string[] + setSelectedMaterials: (ids: string[]) => void + materialMode: "manual" | "auto" + setMaterialMode: (mode: "manual" | "auto") => void + smartSelectedIds: string[] + setSmartSelectedIds: (ids: string[]) => void + + /* 标题 */ + titleSettings: TitleSettings + setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings)) => void + + /* 封面 */ + coverSettings: CoverConfig + setCoverSettings: (settings: CoverConfig | ((prev: CoverConfig) => CoverConfig)) => void + + /* 配音 */ + selectedVoice: string + setSelectedVoice: (id: string) => void + voiceMode: "preset" | "custom" | "clone" + setVoiceMode: (mode: "preset" | "custom" | "clone") => void + selectedClonedVoice: string + setSelectedClonedVoice: (id: string) => void + presetVoices: PresetVoiceItem[] + + /* 克隆弹窗 */ + cloneModalOpen: boolean + setCloneModalOpen: (open: boolean) => void + + /* 生成数量 */ + generateCount: number + setGenerateCount: (n: number) => void + + /* 高级设置 */ + videoRatio: string + duration: number + style: string + autoSubtitles: boolean + bgm: boolean + + /* URL 参数 */ + editPlanId: string | null + planConfigStr: string | null + + /* 预览弹窗 */ + previewVideo: GeneratedVideo | null + setPreviewVideo: (v: GeneratedVideo | null) => void + previewModalOpen: boolean + setPreviewModalOpen: (open: boolean) => void +} + +export const useGenerateFormState = (): GenerateFormState => { + const [searchParams] = useSearchParams() + const editPlanId = searchParams.get("edit_plan_id") + const planConfigStr = searchParams.get("plan_config") + + /* ── 步骤状态 ── */ + const [currentStep, setCurrentStep] = useState(1) + + /* ── 模板选择 ── */ + const { selectedTemplate, setSelectedTemplate, userTemplates } = useTemplateSelection() + + /* ── 素材 ── */ + const [selectedMaterials, setSelectedMaterials] = useState([]) + const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual") + const [smartSelectedIds, setSmartSelectedIds] = useState([]) + + /* ── 标题设置 ── */ + const [titleSettings, setTitleSettings] = useState(DEFAULT_TITLE_SETTINGS) + + /* ── 封面设置 ── */ + const [coverSettings, setCoverSettings] = useState(DEFAULT_COVER_SETTINGS) + + /* ── 模板切换时同步标题/封面 ── */ + useTitleCoverSync({ + selectedTemplate, + userTemplates, + setTitleSettings, + setCoverSettings, + }) + + /* ── 配音状态 ── */ + const { + selectedVoice, + setSelectedVoice, + voiceMode, + setVoiceMode, + selectedClonedVoice, + setSelectedClonedVoice, + presetVoices, + } = useVoiceState() + + /* ── 克隆声音弹窗 ── */ + const [cloneModalOpen, setCloneModalOpen] = useState(false) + + /* ── 生成数量 ── */ + const [generateCount, setGenerateCount] = useState(1) + + /* ── 高级设置(隐藏但保留) ── */ + const [videoRatio] = useState("16:9") + const [duration] = useState(30) + const [style] = useState("business") + const [autoSubtitles] = useState(true) + const [bgm] = useState(true) + + /* ── 预览弹窗 ── */ + const [previewVideo, setPreviewVideo] = useState(null) + const [previewModalOpen, setPreviewModalOpen] = useState(false) + + /* ── 从 URL / 编辑计划加载配置 ── */ + usePlanConfigLoader({ + editPlanId, + planConfigStr, + setTitleSettings, + setCoverSettings, + setSelectedMaterials, + }) + + return { + 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, + planConfigStr, + previewVideo, + setPreviewVideo, + previewModalOpen, + setPreviewModalOpen, + } +} diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader.ts new file mode 100755 index 000000000..457716ac2 --- /dev/null +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader.ts @@ -0,0 +1,110 @@ +import { useEffect } from "react" +import type { CoverConfig } from "../../../editing-planner/types" +import type { TitleSettings } from "../../types" +import type { TitleConfig } from "@/api/template-editor" +import { getEditPlan } from "@/api/template-editor" + +interface UsePlanConfigLoaderOptions { + editPlanId: string | null + planConfigStr: string | null + setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings)) => void + setCoverSettings: (settings: CoverConfig | ((prev: CoverConfig) => CoverConfig)) => void + setSelectedMaterials: (ids: string[]) => void +} + +/** + * 从 URL 参数或编辑计划 ID 加载表单配置 + */ +export function usePlanConfigLoader({ + editPlanId, + planConfigStr, + setTitleSettings, + setCoverSettings, + setSelectedMaterials, +}: UsePlanConfigLoaderOptions) { + /** 解析 plan_config 并自动填充表单 */ + useEffect(() => { + if (!planConfigStr) return + try { + const config = JSON.parse(planConfigStr) as { + title_config?: { + content?: string + ai_auto_select?: boolean + position?: string + font_preset?: string + font_size?: number + font_color?: string + } + subtitle_config?: { enabled?: boolean } + bgm_config?: { enabled?: boolean; music_id?: string } + mode?: string + total_duration?: number + segments?: Array<{ media_asset_id?: string; material_type?: string }> + } + + if (config.title_config) { + const tc = config.title_config as TitleConfig + setTitleSettings((prev: TitleSettings) => ({ + ...prev, + title: tc.content || "", + aiAutoSelect: tc.ai_auto_select || false, + position: tc.position || prev.position, + font: tc.font_preset || prev.font, + size: tc.font_size || prev.size, + color: tc.font_color || prev.color, + })) + } + if (config.segments && config.segments.length > 0) { + const assetIds = config.segments + .map((s) => s.media_asset_id) + .filter((id): id is string => !!id) + if (assetIds.length > 0) { + setSelectedMaterials(assetIds) + } + } + } catch (err) { + console.warn("解析 plan_config 失败:", err) + } + }, [planConfigStr, setTitleSettings, setSelectedMaterials]) + + /** 如果没有 plan_config,尝试通过 edit_plan_id 从后端拉取配置 */ + useEffect(() => { + if (!editPlanId || planConfigStr) return + const loadPlanConfig = async () => { + try { + const plan = await getEditPlan(editPlanId) + if (plan.name) setTitleSettings((prev: TitleSettings) => ({ ...prev, title: plan.name })) + const cfg = plan.config + if (cfg?.title_config) { + setTitleSettings((prev: TitleSettings) => ({ + ...prev, + aiAutoSelect: cfg.title_config!.ai_auto_select, + title: cfg.title_config!.content || prev.title, + position: cfg.title_config!.position || prev.position, + font: cfg.title_config!.font_preset || prev.font, + size: cfg.title_config!.font_size || prev.size, + color: cfg.title_config!.font_color || prev.color, + })) + } + if (cfg?.cover_config) { + const cc = cfg.cover_config as CoverConfig + setCoverSettings((prev: CoverConfig) => ({ + ...prev, + enabled: cc.enabled ?? prev.enabled, + mode: cc.mode || prev.mode, + frame_time: cc.frame_time ?? prev.frame_time, + upload_url: cc.upload_url || prev.upload_url, + ai_suggested_time: cc.ai_suggested_time ?? prev.ai_suggested_time, + thumbnail_url: cc.thumbnail_url || prev.thumbnail_url, + })) + } + if (cfg?.asset_ids) { + setSelectedMaterials(cfg.asset_ids.filter((v): v is string => typeof v === "string")) + } + } catch (err) { + console.warn("加载模板草稿配置失败:", err) + } + } + loadPlanConfig() + }, [editPlanId, planConfigStr, setTitleSettings, setCoverSettings, setSelectedMaterials]) +} diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts new file mode 100755 index 000000000..1b2b64920 --- /dev/null +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts @@ -0,0 +1,22 @@ +import { useState, useEffect } from "react" +import { useQuery } from "@tanstack/react-query" +import { getEditingTemplates } from "@/api/editing-planner" +import type { EditingTemplate } from "@/api/editing-planner" + +export function useTemplateSelection() { + const [selectedTemplate, setSelectedTemplate] = useState("") + const { data: userTemplates = [] } = useQuery({ + queryKey: ["generate-templates"], + queryFn: () => getEditingTemplates(), + staleTime: 60_000, + }) + + /* 模板加载完成后自动选中第一个 */ + useEffect(() => { + if (userTemplates.length > 0 && !selectedTemplate) { + setSelectedTemplate(userTemplates[0].id) + } + }, [userTemplates, selectedTemplate]) + + return { selectedTemplate, setSelectedTemplate, userTemplates } +} diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts new file mode 100755 index 000000000..af6bdbafa --- /dev/null +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts @@ -0,0 +1,47 @@ +import { useEffect } from "react" +import type { TitleSettings } from "../../types" +import type { CoverConfig } from "../../../editing-planner/types" +import type { EditingTemplate } from "@/api/editing-planner" + +interface UseTitleCoverSyncOptions { + selectedTemplate: string + userTemplates: EditingTemplate[] + setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings)) => void + setCoverSettings: (settings: CoverConfig | ((prev: CoverConfig) => CoverConfig)) => void +} + +/** + * 当选中模板变化时,自动同步标题和封面配置 + */ +export function useTitleCoverSync({ + selectedTemplate, + userTemplates, + setTitleSettings, + setCoverSettings, +}: UseTitleCoverSyncOptions) { + useEffect(() => { + const tpl = userTemplates.find((t) => t.id === selectedTemplate) + if (tpl?.title_config) { + setTitleSettings((prev: TitleSettings) => ({ + ...prev, + aiAutoSelect: tpl.title_config!.ai_auto_select, + title: tpl.title_config!.content || prev.title, + position: tpl.title_config!.position || prev.position, + font: tpl.title_config!.font_preset || prev.font, + size: tpl.title_config!.font_size || prev.size, + color: tpl.title_config!.font_color || prev.color, + })) + } + if (tpl?.cover_config) { + setCoverSettings((prev: CoverConfig) => ({ + ...prev, + enabled: tpl.cover_config!.enabled ?? prev.enabled, + mode: (tpl.cover_config!.mode as CoverConfig["mode"]) || prev.mode, + frame_time: tpl.cover_config!.frame_time ?? prev.frame_time, + upload_url: tpl.cover_config!.upload_url || prev.upload_url, + ai_suggested_time: tpl.cover_config!.ai_suggested_time ?? prev.ai_suggested_time, + thumbnail_url: tpl.cover_config!.thumbnail_url || prev.thumbnail_url, + })) + } + }, [selectedTemplate, userTemplates, setTitleSettings, setCoverSettings]) +} diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/useVoiceState.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/useVoiceState.ts new file mode 100755 index 000000000..67b1257b4 --- /dev/null +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/useVoiceState.ts @@ -0,0 +1,30 @@ +import { useState, useMemo } from "react" +import { useQuery } from "@tanstack/react-query" +import type { PresetVoiceItem } from "@/api/voices" +import { fetchPresetVoices } from "@/api/voices" + +export function useVoiceState() { + const [selectedVoice, setSelectedVoice] = useState("") + const [voiceMode, setVoiceMode] = useState<"preset" | "custom" | "clone">("preset") + const [selectedClonedVoice, setSelectedClonedVoice] = useState("") + + /* 预置音色 API */ + const { data: presetVoicesData } = useQuery({ + queryKey: ["preset-voices"], + queryFn: fetchPresetVoices, + }) + const presetVoices: PresetVoiceItem[] = useMemo( + () => presetVoicesData?.items ?? [], + [presetVoicesData], + ) + + return { + selectedVoice, + setSelectedVoice, + voiceMode, + setVoiceMode, + selectedClonedVoice, + setSelectedClonedVoice, + presetVoices, + } +} diff --git a/apps/web/src/test/pages/generate/smoke.test.tsx b/apps/web/src/test/pages/generate/smoke.test.tsx index 784ba7b0d..2227e57da 100755 --- a/apps/web/src/test/pages/generate/smoke.test.tsx +++ b/apps/web/src/test/pages/generate/smoke.test.tsx @@ -38,6 +38,11 @@ describe("GeneratePage module smoke test", () => { }) import "@/pages/generate/hooks/useGenerateVideo" import "@/pages/generate/hooks/generate-video/useGenerationPolling" +import "@/pages/generate/hooks/useGenerateFormState" +import "@/pages/generate/hooks/useGenerateFormState/useTemplateSelection" +import "@/pages/generate/hooks/useGenerateFormState/useTitleCoverSync" +import "@/pages/generate/hooks/useGenerateFormState/useVoiceState" +import "@/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader" import "@/pages/generate/hooks/generate-video/types" import "@/pages/generate/hooks/generate-video/phase" import "@/pages/generate/hooks/generate-video/voiceConfig"