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
369 lines
12 KiB
TypeScript
Executable File
369 lines
12 KiB
TypeScript
Executable File
/**
|
||
* 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,
|
||
}
|
||
}
|