diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts index 7e1cea7ad..83913ee85 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts @@ -3,21 +3,21 @@ * 集中管理 5 步向导的所有共享状态、API 加载、URL 参数解析 * 模板仍在后台自动加载/选择(后端 API 需要 edit_plan_id),但不再暴露给用户。 */ -import { useState, useMemo, useEffect, useRef, useCallback } from "react"; -import { useSearchParams } from "react-router-dom"; -import { useQuery } from "@tanstack/react-query"; -import { message } from "antd"; -import type { EditingTemplate } from "@/api/editing-planner"; -import { getEditingTemplates } from "@/api/editing-planner"; -import type { EditPlanClip } from "@/api/template-editor"; -import type { CoverConfig } from "../../types/cover"; -import type { PresetVoiceItem } from "@/api/voices"; -import { DEFAULT_COVER_SETTINGS, DEFAULT_CLIP_COUNT } from "../../constants"; -import type { TitleSettings } from "../../types"; -import { usePlanConfigLoader } from "./usePlanConfigLoader"; -import { useVoiceState } from "./useVoiceState"; -import { usePersistedState } from "../usePersistedState"; -import { isValidTemplate } from "./templateFallback"; +import { useState, useMemo, useEffect, useRef, useCallback } from "react" +import { useSearchParams } from "react-router-dom" +import { useQuery } from "@tanstack/react-query" +import { message } from "antd" +import type { EditingTemplate } from "@/api/editing-planner" +import { getEditingTemplates } from "@/api/editing-planner" +import type { EditPlanClip } from "@/api/template-editor" +import type { CoverConfig } from "../../types/cover" +import type { PresetVoiceItem } from "@/api/voices" +import { DEFAULT_COVER_SETTINGS, DEFAULT_CLIP_COUNT } from "../../constants" +import type { TitleSettings } from "../../types" +import { usePlanConfigLoader } from "./usePlanConfigLoader" +import { useVoiceState } from "./useVoiceState" +import { usePersistedState } from "../usePersistedState" +import { isValidTemplate } from "./templateFallback" const DEFAULT_TITLE_SETTINGS: TitleSettings = { aiAutoSelect: false, @@ -32,173 +32,160 @@ const DEFAULT_TITLE_SETTINGS: TitleSettings = { color: "#ffffff", posX: null, posY: null, -}; +} export interface GenerateFormState { /* 步骤 */ - currentStep: number; - setCurrentStep: (step: number | ((prev: number) => number)) => void; + currentStep: number + setCurrentStep: (step: number | ((prev: number) => number)) => void /* 片段数量(#1899) */ - clipCount: number; - setClipCount: (n: number) => void; + clipCount: number + setClipCount: (n: number) => void /* 模板(后台自动选择,不暴露 UI) */ - selectedTemplate: string; - setSelectedTemplate: (id: string) => void; - userTemplates: EditingTemplate[]; - handleInvalidTemplate: () => boolean; + selectedTemplate: string + setSelectedTemplate: (id: string) => void + userTemplates: EditingTemplate[] + handleInvalidTemplate: () => boolean /* 素材 */ - selectedMaterials: string[]; - setSelectedMaterials: (ids: string[]) => void; - materialMode: "manual" | "auto"; - setMaterialMode: (mode: "manual" | "auto") => void; - smartSelectedIds: string[]; - setSmartSelectedIds: (ids: string[]) => void; + selectedMaterials: string[] + setSelectedMaterials: (ids: string[]) => void + materialMode: "manual" | "auto" + setMaterialMode: (mode: "manual" | "auto") => void + smartSelectedIds: string[] + setSmartSelectedIds: (ids: string[]) => void /* 服务端片段 */ - serverClips: EditPlanClip[]; - setServerClips: (clips: EditPlanClip[]) => void; + serverClips: EditPlanClip[] + setServerClips: (clips: EditPlanClip[]) => void /* 标题 */ - titleSettings: TitleSettings; - setTitleSettings: ( - settings: TitleSettings | ((prev: TitleSettings) => TitleSettings), - ) => void; + titleSettings: TitleSettings + setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings)) => void /* 封面 */ - coverSettings: CoverConfig; - setCoverSettings: ( - settings: CoverConfig | ((prev: CoverConfig) => CoverConfig), - ) => 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[]; + 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; + cloneModalOpen: boolean + setCloneModalOpen: (open: boolean) => void /* 高级设置 */ - videoRatio: string; - duration: number; - style: string; - autoSubtitles: boolean; - bgm: boolean; + videoRatio: string + duration: number + style: string + autoSubtitles: boolean + bgm: boolean /* URL 参数 */ - editPlanId: string | null; - planConfigStr: string | null; - sourceEditPlanId: string | null; + editPlanId: string | null + planConfigStr: string | null + sourceEditPlanId: string | null /** 预览任务 ID */ - previewTaskId: string | null; - setPreviewTaskId: (id: string | null) => void; - storedSourceEditPlanId: string | null; - setStoredSourceEditPlanId: (planId: string | null) => void; + previewTaskId: string | null + setPreviewTaskId: (id: string | null) => void + storedSourceEditPlanId: string | null + setStoredSourceEditPlanId: (planId: string | null) => void /* ── 批量生成(Issue #1677)── */ - previewCount: number; - setPreviewCount: (n: number) => void; - previewTitles: string[]; - setPreviewTitles: (titles: string[] | ((prev: string[]) => string[])) => void; - voiceModePerVideo: boolean; - setVoiceModePerVideo: (v: boolean) => void; - voiceLibraryIds: string[]; - setVoiceLibraryIds: (ids: string[] | ((prev: string[]) => string[])) => void; - previewCovers: string[]; - setPreviewCovers: (urls: string[] | ((prev: string[]) => string[])) => void; - selectedVariantIds: number[]; - setSelectedVariantIds: ( - ids: number[] | ((prev: number[]) => number[]), - ) => void; + previewCount: number + setPreviewCount: (n: number) => void + previewTitles: string[] + setPreviewTitles: (titles: string[] | ((prev: string[]) => string[])) => void + voiceModePerVideo: boolean + setVoiceModePerVideo: (v: boolean) => void + voiceLibraryIds: string[] + setVoiceLibraryIds: (ids: string[] | ((prev: string[]) => string[])) => void + previewCovers: string[] + setPreviewCovers: (urls: string[] | ((prev: string[]) => string[])) => void + selectedVariantIds: number[] + setSelectedVariantIds: (ids: number[] | ((prev: number[]) => number[])) => void } export const useGenerateFormState = (): GenerateFormState => { - const [searchParams] = useSearchParams(); - const editPlanId = searchParams.get("edit_plan_id"); - const planConfigStr = searchParams.get("plan_config"); + const [searchParams] = useSearchParams() + const editPlanId = searchParams.get("edit_plan_id") + const planConfigStr = searchParams.get("plan_config") /* ── 步骤状态 ── */ - const [currentStep, setCurrentStep] = useState(1); + const [currentStep, setCurrentStep] = useState(1) /* ── 片段数量(#1899) ── */ - const [clipCount, setClipCount] = useState(DEFAULT_CLIP_COUNT); + const [clipCount, setClipCount] = useState(DEFAULT_CLIP_COUNT) /* ── 模板自动加载(#1899:后台静默加载,不暴露 UI) ── */ - const [selectedTemplate, setSelectedTemplateRaw] = useState(""); + const [selectedTemplate, setSelectedTemplateRaw] = useState("") const { data: allTemplates = [] } = useQuery({ queryKey: ["generate-templates"], queryFn: () => getEditingTemplates({ validOnly: true }), staleTime: 60_000, - }); - const validTemplates = useMemo( - () => allTemplates.filter(isValidTemplate), - [allTemplates], - ); - const userTemplates = validTemplates; + }) + const validTemplates = useMemo(() => allTemplates.filter(isValidTemplate), [allTemplates]) + const userTemplates = validTemplates - const templatesRef = useRef(validTemplates); - templatesRef.current = validTemplates; - const selectedRef = useRef(selectedTemplate); - selectedRef.current = selectedTemplate; - const fallbackNotifiedRef = useRef(""); + const templatesRef = useRef(validTemplates) + templatesRef.current = validTemplates + const selectedRef = useRef(selectedTemplate) + selectedRef.current = selectedTemplate + const fallbackNotifiedRef = useRef("") const setSelectedTemplate = useCallback((id: string) => { - fallbackNotifiedRef.current = ""; - setSelectedTemplateRaw(id); - }, []); + fallbackNotifiedRef.current = "" + setSelectedTemplateRaw(id) + }, []) // 自动选择第一个有效模板(静默,不弹 toast) useEffect(() => { - if (selectedTemplate) return; - const firstValid = validTemplates[0]; + if (selectedTemplate) return + const firstValid = validTemplates[0] if (firstValid) { - setSelectedTemplateRaw(firstValid.id); + setSelectedTemplateRaw(firstValid.id) } - }, [validTemplates, selectedTemplate]); + }, [validTemplates, selectedTemplate]) const handleInvalidTemplate = useCallback((): boolean => { - const current = selectedRef.current; - if (current && fallbackNotifiedRef.current === current) return true; - const fallback = validTemplates[0]; - fallbackNotifiedRef.current = current || "__empty__"; + const current = selectedRef.current + if (current && fallbackNotifiedRef.current === current) return true + const fallback = validTemplates[0] + fallbackNotifiedRef.current = current || "__empty__" if (fallback) { - setSelectedTemplateRaw(fallback.id); - message.warning("原模板已失效,已自动切换"); + setSelectedTemplateRaw(fallback.id) + message.warning("原模板已失效,已自动切换") } else { - setSelectedTemplateRaw(""); + setSelectedTemplateRaw("") } - return true; - }, [validTemplates]); + return true + }, [validTemplates]) /* ── source_edit_plan_id ── */ - const sourceEditPlanId = editPlanId || null; + const sourceEditPlanId = editPlanId || null /* ── 素材 ── */ - const [selectedMaterials, setSelectedMaterials] = useState([]); - const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual"); - const [smartSelectedIds, setSmartSelectedIds] = useState([]); + const [selectedMaterials, setSelectedMaterials] = useState([]) + const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual") + const [smartSelectedIds, setSmartSelectedIds] = useState([]) /* ── 服务端片段 ── */ - const [serverClips, setServerClips] = useState([]); + const [serverClips, setServerClips] = useState([]) /* ── 标题设置 ── */ - const [titleSettings, setTitleSettings] = useState( - DEFAULT_TITLE_SETTINGS, - ); + const [titleSettings, setTitleSettings] = useState(DEFAULT_TITLE_SETTINGS) /* ── 封面设置 ── */ - const [coverSettings, setCoverSettings] = useState( - DEFAULT_COVER_SETTINGS, - ); + const [coverSettings, setCoverSettings] = useState(DEFAULT_COVER_SETTINGS) /* ── 配音状态 ── */ const { @@ -209,45 +196,46 @@ export const useGenerateFormState = (): GenerateFormState => { selectedClonedVoice, setSelectedClonedVoice, presetVoices, - } = useVoiceState(); + } = useVoiceState() /* ── 克隆声音弹窗 ── */ - const [cloneModalOpen, setCloneModalOpen] = useState(false); + const [cloneModalOpen, setCloneModalOpen] = useState(false) /* ── 高级设置(隐藏但保留) ── */ - const [videoRatio] = useState("9:16"); - const [duration] = useState(30); - const [style] = useState("business"); - const [autoSubtitles] = useState(true); - const [bgm] = useState(true); + const [videoRatio] = useState("9:16") + const [duration] = useState(30) + const [style] = useState("business") + const [autoSubtitles] = useState(true) + const [bgm] = useState(true) /* ── 预览任务 ID ── */ const previewStorageKey = editPlanId ? `preview_task_id_${editPlanId}` : selectedTemplate ? `preview_task_id_tpl_${selectedTemplate}` - : null; + : null const [previewTaskId, setPreviewTaskId] = usePersistedState( previewStorageKey, null, - ); + ) const planIdStorageKey = editPlanId ? `source_edit_plan_id_${editPlanId}` : selectedTemplate ? `source_edit_plan_id_tpl_${selectedTemplate}` - : null; - const [storedSourceEditPlanId, setStoredSourceEditPlanId] = usePersistedState< - string | null - >(planIdStorageKey, null); + : null + const [storedSourceEditPlanId, setStoredSourceEditPlanId] = usePersistedState( + planIdStorageKey, + null, + ) /* ── 批量生成状态 ── */ - const [previewCount, setPreviewCount] = useState(1); - const [previewTitles, setPreviewTitles] = useState([""]); - const [voiceModePerVideo, setVoiceModePerVideo] = useState(false); - const [voiceLibraryIds, setVoiceLibraryIds] = useState([""]); - const [previewCovers, setPreviewCovers] = useState([""]); - const [selectedVariantIds, setSelectedVariantIds] = useState([0]); + const [previewCount, setPreviewCount] = useState(1) + const [previewTitles, setPreviewTitles] = useState([""]) + const [voiceModePerVideo, setVoiceModePerVideo] = useState(false) + const [voiceLibraryIds, setVoiceLibraryIds] = useState([""]) + const [previewCovers, setPreviewCovers] = useState([""]) + const [selectedVariantIds, setSelectedVariantIds] = useState([0]) /* ── 从 URL / 编辑计划加载配置 ── */ usePlanConfigLoader({ @@ -256,7 +244,7 @@ export const useGenerateFormState = (): GenerateFormState => { setTitleSettings, setCoverSettings, setSelectedMaterials, - }); + }) return { currentStep, @@ -312,5 +300,5 @@ export const useGenerateFormState = (): GenerateFormState => { setPreviewCovers, selectedVariantIds, setSelectedVariantIds, - }; -}; + } +}