Files
xiaoxia-saas/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts
T
xiaoxia 8c715474f4
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 16s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 19s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m53s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 33s
AI Code Review / AI Code Review (pull_request) Successful in 6m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 36s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 8m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m48s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m45s
CI/CD Pipeline / Validate - Style (push) Successful in 27m21s
CI/CD Pipeline / Integration Tests (push) Successful in 26m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 31m35s
CI/CD Pipeline / Unit Tests (push) Failing after 35m54s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
feat(web): #2001 爆款标题样式配置面板升级(P0+P1) (#2002)
2026-09-20 23:09:04 +08:00

369 lines
12 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* GeneratePage 表单状态管理(#1899 简化:移除模板选择步骤)
* 集中管理 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 type { ScriptItem } from "@/api/scripts"
import { DEFAULT_TTS_STYLE, type TtsStyle } from "@/api/tts/styles"
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,
title: "",
position: "top",
font: "思源黑体",
size: 28,
bold: true,
italic: false,
stroke: true,
shadow: false,
color: "#ffffff",
posX: null,
posY: null,
lineHeight: 1.2,
marginTop: 24,
maxCharsPerLine: 0,
strokeWidth: 4,
strokeColor: "#000000",
shadowOffsetX: 2,
shadowOffsetY: 2,
shadowBlur: 4,
shadowColor: "rgba(0,0,0,0.8)",
bgEnabled: false,
bgColor: "rgba(0,0,0,0.5)",
bgPadding: 12,
bgRadius: 8,
lineOverrides: [],
coverTitle: null,
}
export interface GenerateFormState {
/* 步骤 */
currentStep: number
setCurrentStep: (step: number | ((prev: number) => number)) => void
/* 片段数量(#1899) */
clipCount: number
setClipCount: (n: number) => void
/* 模板(后台自动选择,不暴露 UI) */
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
/* 服务端片段 */
serverClips: EditPlanClip[]
setServerClips: (clips: EditPlanClip[]) => 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
/* ── 剪辑模式(#1970 流程重构)── */
editMode: "random" | "narrative"
setEditMode: (mode: "random" | "narrative") => void
/** 叙事模式下选中的文案 */
selectedScript: ScriptItem | null
setSelectedScript: (s: ScriptItem | null) => void
/** TTS 音色 ID */
ttsVoiceId: string
setTtsVoiceId: (id: string) => void
/** TTS 音色来源:preset 系统 / clone 克隆 */
ttsVoiceSource: "preset" | "clone"
setTtsVoiceSource: (src: "preset" | "clone") => void
/** TTS 配音风格 */
ttsStyle: TtsStyle
setTtsStyle: (s: TtsStyle) => void
/** 合成后配音库 asset id(叙事模式保存到库后获得;随机模式 = selectedVoice) */
ttsVoiceAssetId: string
setTtsVoiceAssetId: (id: string) => void
/** 智能降重开关(默认 true) */
dedupEnabled: boolean
setDedupEnabled: (v: boolean) => void
/* 高级设置 */
videoRatio: "9:16" | "16:9" | string
setVideoRatio: (r: "9:16" | "16:9") => void
duration: number
style: string
autoSubtitles: boolean
bgm: boolean
/* URL 参数 */
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
/* ── 批量生成(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
}
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)
/* ── 片段数量(#1899) ── */
const [clipCount, setClipCount] = useState(DEFAULT_CLIP_COUNT)
/* ── 模板自动加载(#1899:后台静默加载,不暴露 UI) ── */
const [selectedTemplate, setSelectedTemplateRaw] = useState("")
const { data: allTemplates = [] } = useQuery<EditingTemplate[]>({
queryKey: ["generate-templates"],
queryFn: () => getEditingTemplates({ validOnly: true }),
staleTime: 60_000,
})
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<string>("")
const setSelectedTemplate = useCallback((id: string) => {
fallbackNotifiedRef.current = ""
setSelectedTemplateRaw(id)
}, [])
// 自动选择第一个有效模板(静默,不弹 toast)
useEffect(() => {
if (selectedTemplate) return
const firstValid = validTemplates[0]
if (firstValid) {
setSelectedTemplateRaw(firstValid.id)
}
}, [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__"
if (fallback) {
setSelectedTemplateRaw(fallback.id)
message.warning("原模板已失效,已自动切换")
} else {
setSelectedTemplateRaw("")
}
return true
}, [validTemplates])
/* ── source_edit_plan_id ── */
const sourceEditPlanId = editPlanId || null
/* ── 素材 ── */
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([])
const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual")
const [smartSelectedIds, setSmartSelectedIds] = useState<string[]>([])
/* ── 服务端片段 ── */
const [serverClips, setServerClips] = useState<EditPlanClip[]>([])
/* ── 标题设置 ── */
const [titleSettings, setTitleSettings] = useState<TitleSettings>(DEFAULT_TITLE_SETTINGS)
/* ── 封面设置 ── */
const [coverSettings, setCoverSettings] = useState<CoverConfig>(DEFAULT_COVER_SETTINGS)
/* ── 配音状态 ── */
const {
selectedVoice,
setSelectedVoice,
voiceMode,
setVoiceMode,
selectedClonedVoice,
setSelectedClonedVoice,
presetVoices,
} = useVoiceState()
/* ── 克隆声音弹窗 ── */
const [cloneModalOpen, setCloneModalOpen] = useState(false)
/* ── 高级设置 ── */
const [videoRatio, setVideoRatio] = useState<"9:16" | "16:9">("9:16")
const [duration] = useState(30)
const [style] = useState("business")
const [autoSubtitles] = useState(true)
const [bgm] = useState(true)
/* ── 剪辑模式状态(#1970) ── */
const [editMode, setEditMode] = useState<"random" | "narrative">("random")
const [selectedScript, setSelectedScript] = useState<ScriptItem | null>(null)
const [ttsVoiceId, setTtsVoiceId] = useState<string>("")
const [ttsVoiceSource, setTtsVoiceSource] = useState<"preset" | "clone">("preset")
const [ttsStyle, setTtsStyle] = useState<TtsStyle>(DEFAULT_TTS_STYLE)
const [ttsVoiceAssetId, setTtsVoiceAssetId] = useState<string>("")
const [dedupEnabled, setDedupEnabled] = useState<boolean>(true)
/* ── 预览任务 ID ── */
const previewStorageKey = editPlanId
? `preview_task_id_${editPlanId}`
: selectedTemplate
? `preview_task_id_tpl_${selectedTemplate}`
: null
const [previewTaskId, setPreviewTaskId] = usePersistedState<string | null>(
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,
)
/* ── 批量生成状态 ── */
const [previewCount, setPreviewCount] = useState(1)
const [previewTitles, setPreviewTitles] = useState<string[]>([""])
const [voiceModePerVideo, setVoiceModePerVideo] = useState(false)
const [voiceLibraryIds, setVoiceLibraryIds] = useState<string[]>([""])
const [previewCovers, setPreviewCovers] = useState<string[]>([""])
const [selectedVariantIds, setSelectedVariantIds] = useState<number[]>([0])
/* ── 从 URL / 编辑计划加载配置 ── */
usePlanConfigLoader({
editPlanId,
planConfigStr,
setTitleSettings,
setCoverSettings,
setSelectedMaterials,
})
return {
currentStep,
setCurrentStep,
clipCount,
setClipCount,
selectedTemplate,
setSelectedTemplate,
userTemplates,
handleInvalidTemplate,
selectedMaterials,
setSelectedMaterials,
materialMode,
setMaterialMode,
smartSelectedIds,
setSmartSelectedIds,
serverClips,
setServerClips,
titleSettings,
setTitleSettings,
coverSettings,
setCoverSettings,
selectedVoice,
setSelectedVoice,
voiceMode,
setVoiceMode,
selectedClonedVoice,
setSelectedClonedVoice,
presetVoices,
editMode,
setEditMode,
selectedScript,
setSelectedScript,
ttsVoiceId,
setTtsVoiceId,
ttsVoiceSource,
setTtsVoiceSource,
ttsStyle,
setTtsStyle,
ttsVoiceAssetId,
setTtsVoiceAssetId,
dedupEnabled,
setDedupEnabled,
cloneModalOpen,
setCloneModalOpen,
videoRatio,
setVideoRatio,
duration,
style,
autoSubtitles,
bgm,
editPlanId,
sourceEditPlanId,
planConfigStr,
previewTaskId,
setPreviewTaskId,
storedSourceEditPlanId,
setStoredSourceEditPlanId,
previewCount,
setPreviewCount,
previewTitles,
setPreviewTitles,
voiceModePerVideo,
setVoiceModePerVideo,
voiceLibraryIds,
setVoiceLibraryIds,
previewCovers,
setPreviewCovers,
selectedVariantIds,
setSelectedVariantIds,
}
}