From 7f4402c88f5d19f6f10eb8f48e3b6d28cd3e0697 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 17:20:51 +0800 Subject: [PATCH 1/8] =?UTF-8?q?refactor(generate):=20=E6=8B=86=E5=88=86=20?= =?UTF-8?q?useGenerateFormState=20=E4=B8=BA4=E4=B8=AA=E5=AD=90Hook?= =?UTF-8?q?=EF=BC=88309=E2=86=92196=E8=A1=8C,=20-37%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generate/hooks/useGenerateFormState.ts | 309 ------------------ .../hooks/useGenerateFormState/index.ts | 196 +++++++++++ .../usePlanConfigLoader.ts | 110 +++++++ .../useTemplateSelection.ts | 22 ++ .../useGenerateFormState/useTitleCoverSync.ts | 47 +++ .../useGenerateFormState/useVoiceState.ts | 30 ++ .../src/test/pages/generate/smoke.test.tsx | 5 + 7 files changed, 410 insertions(+), 309 deletions(-) delete mode 100644 apps/web/src/pages/generate/hooks/useGenerateFormState.ts create mode 100755 apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts create mode 100755 apps/web/src/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader.ts create mode 100755 apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts create mode 100755 apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts create mode 100755 apps/web/src/pages/generate/hooks/useGenerateFormState/useVoiceState.ts 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..127319515 --- /dev/null +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts @@ -0,0 +1,196 @@ +/** + * GeneratePage 表单状态管理 + * 集中管理 7 步向导的所有共享状态、API 加载、URL 参数解析 + */ +import { useState } from "react" +import { useSearchParams } from "react-router-dom" +import type { GeneratedVideo } from "@/api/template-editor" +import type { CoverConfig } from "../../editing-planner/types" +import { DEFAULT_COVER_SETTINGS } from "../constants" +import type { TitleSettings } from "../types" +import { useTemplateSelection } from "./useGenerateFormState/useTemplateSelection" +import { useTitleCoverSync } from "./useGenerateFormState/useTitleCoverSync" +import { useVoiceState } from "./useGenerateFormState/useVoiceState" +import { usePlanConfigLoader } from "./useGenerateFormState/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: any[] + + /* 素材 */ + 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: any[] + + /* 克隆弹窗 */ + 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..73eee3295 --- /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) => ({ + ...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) => ({ ...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, 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..63872ad31 --- /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 type { EditingTemplate } from "@/api/editing-planner" +import { getEditingTemplates } 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..623c3ced5 --- /dev/null +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts @@ -0,0 +1,47 @@ +import { useEffect } from "react" +import type { CoverConfig } from "../../../editing-planner/types" +import type { TitleSettings } from "../../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) => ({ + ...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, 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" -- 2.54.0 From bae53fd705354d9a11f872669017041c0ca51179 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 18:31:18 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84EditingTemplate=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useGenerateFormState/useTemplateSelection.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts index 63872ad31..1d629f916 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts @@ -1,13 +1,10 @@ import { useState, useEffect } from "react" import { useQuery } from "@tanstack/react-query" -import type { EditingTemplate } from "@/api/editing-planner" -import { getEditingTemplates } from "@/api/editing-planner" export function useTemplateSelection() { const [selectedTemplate, setSelectedTemplate] = useState("") const { data: userTemplates = [] } = useQuery({ queryKey: ["generate-templates"], - queryFn: () => getEditingTemplates(), staleTime: 60_000, }) -- 2.54.0 From d5b3598e6f2c603ebf0ec505e93a2b1d24ed5dc5 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 18:31:41 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DuseTitleCoverSync?= =?UTF-8?q?=E8=AF=AD=E6=B3=95=E9=94=99=E8=AF=AF=20+=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=E9=87=8D=E5=A4=8Dimport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generate/hooks/useGenerateFormState/useTitleCoverSync.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts index 623c3ced5..d9913332d 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts @@ -1,12 +1,11 @@ import { useEffect } from "react" -import type { CoverConfig } from "../../../editing-planner/types" import type { TitleSettings } from "../../types" import type { EditingTemplate } from "@/api/editing-planner" interface UseTitleCoverSyncOptions { selectedTemplate: string userTemplates: EditingTemplate[] - setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings) => void + setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings)) => void setCoverSettings: (settings: CoverConfig | ((prev: CoverConfig) => CoverConfig)) => void } -- 2.54.0 From 322bc563508ace09bbc89551436a46b8e05cedb3 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 18:39:20 +0800 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=E6=9B=BF=E6=8D=A2any=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E4=B8=BA=E5=85=B7=E4=BD=93=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/generate/hooks/useGenerateFormState/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts index 127319515..7a53d558f 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts @@ -4,8 +4,9 @@ */ import { useState } from "react" import { useSearchParams } from "react-router-dom" -import type { GeneratedVideo } from "@/api/template-editor" +import type { GeneratedVideo, 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 "./useGenerateFormState/useTemplateSelection" @@ -34,7 +35,7 @@ export interface GenerateFormState { /* 模板 */ selectedTemplate: string setSelectedTemplate: (id: string) => void - userTemplates: any[] + userTemplates: EditingTemplate[] /* 素材 */ selectedMaterials: string[] @@ -59,7 +60,7 @@ export interface GenerateFormState { setVoiceMode: (mode: "preset" | "custom" | "clone") => void selectedClonedVoice: string setSelectedClonedVoice: (id: string) => void - presetVoices: any[] + presetVoices: PresetVoiceItem[] /* 克隆弹窗 */ cloneModalOpen: boolean -- 2.54.0 From 78c512d8a9a8228e660f6a9a476980ca047c1171 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 19:03:52 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix(generate):=20=E4=BF=AE=E5=A4=8D=20useGe?= =?UTF-8?q?nerateFormState=20=E6=8B=86=E5=88=86=E5=90=8E=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useGenerateFormState/index.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts index 7a53d558f..91de56c79 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts @@ -4,15 +4,16 @@ */ import { useState } from "react" import { useSearchParams } from "react-router-dom" -import type { GeneratedVideo, EditingTemplate } from "@/api/editing-planner" -import type { CoverConfig } from "../../editing-planner/types" +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 "./useGenerateFormState/useTemplateSelection" -import { useTitleCoverSync } from "./useGenerateFormState/useTitleCoverSync" -import { useVoiceState } from "./useGenerateFormState/useVoiceState" -import { usePlanConfigLoader } from "./useGenerateFormState/usePlanConfigLoader" +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, -- 2.54.0 From 172cf357aa83850a527fd9460b1dd312cf823697 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 19:03:52 +0800 Subject: [PATCH 6/8] =?UTF-8?q?fix(generate):=20=E4=BF=AE=E5=A4=8D=20useTe?= =?UTF-8?q?mplateSelection=20=E7=BC=BA=E5=B0=91=20queryFn=20=E5=92=8C?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useGenerateFormState/useTemplateSelection.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts index 1d629f916..1b2b64920 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTemplateSelection.ts @@ -1,10 +1,13 @@ 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({ + const { data: userTemplates = [] } = useQuery({ queryKey: ["generate-templates"], + queryFn: () => getEditingTemplates(), staleTime: 60_000, }) -- 2.54.0 From 68843d9d80ca87468b6cc5a8e254675c048d5388 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 19:03:53 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix(generate):=20=E4=BF=AE=E5=A4=8D=20useTi?= =?UTF-8?q?tleCoverSync=20=E7=BC=BA=E5=B0=91=20CoverConfig=20=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=92=8C=E9=9A=90=E5=BC=8Fany?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generate/hooks/useGenerateFormState/useTitleCoverSync.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts index d9913332d..af6bdbafa 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/useTitleCoverSync.ts @@ -1,5 +1,6 @@ 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 { @@ -21,7 +22,7 @@ export function useTitleCoverSync({ useEffect(() => { const tpl = userTemplates.find((t) => t.id === selectedTemplate) if (tpl?.title_config) { - setTitleSettings((prev) => ({ + setTitleSettings((prev: TitleSettings) => ({ ...prev, aiAutoSelect: tpl.title_config!.ai_auto_select, title: tpl.title_config!.content || prev.title, @@ -32,7 +33,7 @@ export function useTitleCoverSync({ })) } if (tpl?.cover_config) { - setCoverSettings((prev) => ({ + setCoverSettings((prev: CoverConfig) => ({ ...prev, enabled: tpl.cover_config!.enabled ?? prev.enabled, mode: (tpl.cover_config!.mode as CoverConfig["mode"]) || prev.mode, -- 2.54.0 From 9010fb62242819445837554e786f6a8fa4fd07fb Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 19:03:55 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix(generate):=20=E4=BF=AE=E5=A4=8D=20usePl?= =?UTF-8?q?anConfigLoader=20=E9=9A=90=E5=BC=8Fany=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useGenerateFormState/usePlanConfigLoader.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader.ts index 73eee3295..457716ac2 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/usePlanConfigLoader.ts @@ -44,7 +44,7 @@ export function usePlanConfigLoader({ if (config.title_config) { const tc = config.title_config as TitleConfig - setTitleSettings((prev) => ({ + setTitleSettings((prev: TitleSettings) => ({ ...prev, title: tc.content || "", aiAutoSelect: tc.ai_auto_select || false, @@ -73,10 +73,10 @@ export function usePlanConfigLoader({ const loadPlanConfig = async () => { try { const plan = await getEditPlan(editPlanId) - if (plan.name) setTitleSettings((prev) => ({ ...prev, title: plan.name })) + if (plan.name) setTitleSettings((prev: TitleSettings) => ({ ...prev, title: plan.name })) const cfg = plan.config if (cfg?.title_config) { - setTitleSettings((prev) => ({ + setTitleSettings((prev: TitleSettings) => ({ ...prev, aiAutoSelect: cfg.title_config!.ai_auto_select, title: cfg.title_config!.content || prev.title, @@ -88,7 +88,7 @@ export function usePlanConfigLoader({ } if (cfg?.cover_config) { const cc = cfg.cover_config as CoverConfig - setCoverSettings((prev) => ({ + setCoverSettings((prev: CoverConfig) => ({ ...prev, enabled: cc.enabled ?? prev.enabled, mode: cc.mode || prev.mode, -- 2.54.0