Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7268d44476 | |||
| 83d30190ed | |||
| 0407f35ac2 | |||
| 4ec65cd531 | |||
| bb167c5d81 |
@@ -0,0 +1,157 @@
|
|||||||
|
import { useEffect, type Dispatch, type SetStateAction } from "react"
|
||||||
|
import { message } from "antd"
|
||||||
|
import { getEditPlan, getEditPlanClips } from "@/api/template-editor"
|
||||||
|
import type { ClipData, ClipType, TtsConfig, TtsMode, TrimConfig } from "../../types"
|
||||||
|
import type { SubtitleStyleConfig } from "../../types/subtitle"
|
||||||
|
import type { TitleConfig } from "@/api/template-editor"
|
||||||
|
import type { BgmMixConfig } from "@/api/bgm"
|
||||||
|
import type { TransitionEffect } from "@/api/template-editor"
|
||||||
|
import type { CoverConfig } from "../../types"
|
||||||
|
|
||||||
|
interface UsePlanLoadingOptions {
|
||||||
|
loadedPlanId: string | null
|
||||||
|
resetClips: (clips: ClipData[]) => void
|
||||||
|
setLoadedTemplateId: (id: string | null) => void
|
||||||
|
setDraftName: (name: string) => void
|
||||||
|
setTitleConfig: Dispatch<SetStateAction<TitleConfig>>
|
||||||
|
setSubtitleSettings: Dispatch<SetStateAction<SubtitleStyleConfig>>
|
||||||
|
setBgmSettings: Dispatch<SetStateAction<BgmMixConfig>>
|
||||||
|
setCoverConfig: Dispatch<SetStateAction<CoverConfig>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载已有计划草稿数据到编辑器
|
||||||
|
* 从列表页"编辑"按钮进入时,URL 带 planId,需要还原计划配置
|
||||||
|
*/
|
||||||
|
export function usePlanLoading({
|
||||||
|
loadedPlanId,
|
||||||
|
resetClips,
|
||||||
|
setLoadedTemplateId,
|
||||||
|
setDraftName,
|
||||||
|
setTitleConfig,
|
||||||
|
setSubtitleSettings,
|
||||||
|
setBgmSettings,
|
||||||
|
setCoverConfig,
|
||||||
|
}: UsePlanLoadingOptions) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedPlanId) return
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
getEditPlan(loadedPlanId),
|
||||||
|
getEditPlanClips(loadedPlanId, { limit: 500 }).catch(() => ({
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
.then(([plan, clipsRes]) => {
|
||||||
|
setLoadedTemplateId(plan.template_id)
|
||||||
|
setDraftName(plan.name)
|
||||||
|
|
||||||
|
const cfg = plan.config
|
||||||
|
if (cfg.title_config) {
|
||||||
|
setTitleConfig({
|
||||||
|
ai_auto_select: cfg.title_config!.ai_auto_select,
|
||||||
|
content: cfg.title_config!.content,
|
||||||
|
position: cfg.title_config!.position,
|
||||||
|
font_preset: cfg.title_config!.font_preset,
|
||||||
|
font_size: cfg.title_config!.font_size,
|
||||||
|
font_color: cfg.title_config!.font_color || "#ffffff",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (cfg.subtitle_config) {
|
||||||
|
setSubtitleSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
enabled: cfg.subtitle_config!.enabled,
|
||||||
|
position: (cfg.subtitle_config!.position ||
|
||||||
|
"bottom") as SubtitleStyleConfig["position"],
|
||||||
|
font: cfg.subtitle_config!.font,
|
||||||
|
fontSize: cfg.subtitle_config!.size,
|
||||||
|
fontColor: cfg.subtitle_config!.color || "#ffffff",
|
||||||
|
animation: cfg.subtitle_config!.animation,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (cfg.bgm_config) {
|
||||||
|
setBgmSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
enabled: cfg.bgm_config!.enabled,
|
||||||
|
music_id: cfg.bgm_config!.music_id || "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (cfg.cover_config) {
|
||||||
|
setCoverConfig((prev: CoverConfig) => ({
|
||||||
|
...prev,
|
||||||
|
enabled: cfg.cover_config!.enabled ?? prev.enabled,
|
||||||
|
mode: (cfg.cover_config!.mode as CoverConfig["mode"]) || prev.mode,
|
||||||
|
frame_time: cfg.cover_config!.frame_time ?? prev.frame_time,
|
||||||
|
upload_url: cfg.cover_config!.upload_url || prev.upload_url,
|
||||||
|
thumbnail_url: cfg.cover_config!.thumbnail_url || prev.thumbnail_url,
|
||||||
|
ai_suggested_time: cfg.cover_config!.ai_suggested_time ?? prev.ai_suggested_time,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 还原片段:优先从后端 clips 表,其次从 config.segments 兜底 */
|
||||||
|
const backendClips = clipsRes?.items || []
|
||||||
|
if (backendClips.length > 0) {
|
||||||
|
const sorted = [...backendClips].sort((a, b) => a.order - b.order)
|
||||||
|
const mapped: ClipData[] = sorted.map((clip) => ({
|
||||||
|
id: clip.id,
|
||||||
|
template_segment_id: (clip.config?.template_segment_id as string) || "",
|
||||||
|
type: (clip.clip_type === "voiceover" ? "voice" : "pip") as ClipType,
|
||||||
|
duration: clip.duration || 3,
|
||||||
|
startOffset: 0,
|
||||||
|
script_text: clip.text_content || "",
|
||||||
|
order: clip.order,
|
||||||
|
media_asset_id: clip.asset_id || undefined,
|
||||||
|
transition:
|
||||||
|
clip.transition_effect && clip.transition_effect !== "none"
|
||||||
|
? {
|
||||||
|
type: clip.transition_effect as TransitionEffect["type"],
|
||||||
|
duration: clip.transition_duration || 0.3,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
speed: clip.playback_speed
|
||||||
|
? { rate: clip.playback_speed, pitchCorrection: true }
|
||||||
|
: undefined,
|
||||||
|
tts_config: (clip.config?.tts_config as TtsConfig) || undefined,
|
||||||
|
trim_config: (clip.config?.trim_config as TrimConfig) || undefined,
|
||||||
|
}))
|
||||||
|
setTimeout(() => resetClips(mapped), 100)
|
||||||
|
} else if (cfg.segments && cfg.segments.length > 0) {
|
||||||
|
/* 兜底:从 config.segments 还原(老数据兼容) */
|
||||||
|
const mapped: ClipData[] = cfg.segments.map((seg, idx) => ({
|
||||||
|
id: `seg-${idx}`,
|
||||||
|
template_segment_id: `seg-${idx}`,
|
||||||
|
type: (seg.material_type === "voiceover" ? "voice" : "pip") as ClipType,
|
||||||
|
duration: (seg.duration_min + seg.duration_max) / 2,
|
||||||
|
startOffset: 0,
|
||||||
|
script_text: "",
|
||||||
|
order: seg.segment_order,
|
||||||
|
transition: seg.transition
|
||||||
|
? {
|
||||||
|
type: seg.transition.type as TransitionEffect["type"],
|
||||||
|
duration: seg.transition.duration,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
speed: seg.playback_speed
|
||||||
|
? { rate: seg.playback_speed, pitchCorrection: true }
|
||||||
|
: undefined,
|
||||||
|
tts_config: seg.tts_config
|
||||||
|
? { ...seg.tts_config, mode: seg.tts_config.mode as TtsMode }
|
||||||
|
: undefined,
|
||||||
|
trim_config: seg.trim_config || undefined,
|
||||||
|
}))
|
||||||
|
setTimeout(() => resetClips(mapped), 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => message.error("加载模板草稿失败"))
|
||||||
|
}, [
|
||||||
|
loadedPlanId,
|
||||||
|
resetClips,
|
||||||
|
setLoadedTemplateId,
|
||||||
|
setDraftName,
|
||||||
|
setTitleConfig,
|
||||||
|
setSubtitleSettings,
|
||||||
|
setBgmSettings,
|
||||||
|
setCoverConfig,
|
||||||
|
])
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect, type Dispatch, type SetStateAction } from "react"
|
||||||
|
import { message } from "antd"
|
||||||
|
import { getEditingTemplate, type TemplateMode } from "@/api/editing-planner"
|
||||||
|
import type { TitleConfig } from "@/api/template-editor"
|
||||||
|
import type { ClipData, ClipType } from "../../types"
|
||||||
|
import type { SubtitleStyleConfig } from "../../types/subtitle"
|
||||||
|
import type { BgmMixConfig } from "@/api/bgm"
|
||||||
|
|
||||||
|
interface UseTemplateDetailOptions {
|
||||||
|
loadedTemplateId: string | null
|
||||||
|
resetClips: (clips: ClipData[]) => void
|
||||||
|
setCurrentMode: (mode: TemplateMode) => void
|
||||||
|
setTitleConfig: Dispatch<SetStateAction<TitleConfig>>
|
||||||
|
setSubtitleSettings: Dispatch<SetStateAction<SubtitleStyleConfig>>
|
||||||
|
setBgmSettings: Dispatch<SetStateAction<BgmMixConfig>>
|
||||||
|
setDraftName: (name: string) => void
|
||||||
|
setDraftCategory: (cat: string) => void
|
||||||
|
setDraftTags: (tags: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载模板详情并初始化片段列表 + 配置
|
||||||
|
*/
|
||||||
|
export function useTemplateDetail({
|
||||||
|
loadedTemplateId,
|
||||||
|
resetClips,
|
||||||
|
setCurrentMode,
|
||||||
|
setTitleConfig,
|
||||||
|
setSubtitleSettings,
|
||||||
|
setBgmSettings,
|
||||||
|
setDraftName,
|
||||||
|
setDraftCategory,
|
||||||
|
setDraftTags,
|
||||||
|
}: UseTemplateDetailOptions) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedTemplateId) return
|
||||||
|
getEditingTemplate(loadedTemplateId)
|
||||||
|
.then((tpl) => {
|
||||||
|
if (!tpl) return
|
||||||
|
setCurrentMode(tpl.mode)
|
||||||
|
|
||||||
|
const mapped: ClipData[] = tpl.segments.map((seg, idx) => ({
|
||||||
|
id: seg.id || `seg-${idx}`,
|
||||||
|
template_segment_id: seg.id || `seg-${idx}`,
|
||||||
|
type: (seg.material_type === "voiceover" ? "voice" : "pip") as ClipType,
|
||||||
|
duration: (seg.duration_min + seg.duration_max) / 2,
|
||||||
|
startOffset: 0,
|
||||||
|
script_text: "",
|
||||||
|
order: seg.segment_order,
|
||||||
|
}))
|
||||||
|
resetClips(mapped)
|
||||||
|
|
||||||
|
setTitleConfig({
|
||||||
|
ai_auto_select: tpl.title_config.ai_auto_select,
|
||||||
|
content: tpl.title_config.content,
|
||||||
|
position: tpl.title_config.position,
|
||||||
|
font_preset: tpl.title_config.font_preset,
|
||||||
|
font_size: tpl.title_config.font_size,
|
||||||
|
font_color: tpl.title_config.font_color || "#ffffff",
|
||||||
|
})
|
||||||
|
setSubtitleSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
enabled: tpl.subtitle_config.enabled,
|
||||||
|
position: (tpl.subtitle_config.position || "bottom") as SubtitleStyleConfig["position"],
|
||||||
|
font: tpl.subtitle_config.font,
|
||||||
|
fontSize: tpl.subtitle_config.size,
|
||||||
|
fontColor: tpl.subtitle_config.color || "#ffffff",
|
||||||
|
animation: tpl.subtitle_config.animation,
|
||||||
|
}))
|
||||||
|
setBgmSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
enabled: tpl.bgm_config.enabled,
|
||||||
|
music_id: tpl.bgm_config.music_id || "",
|
||||||
|
}))
|
||||||
|
setDraftName(tpl.name)
|
||||||
|
setDraftCategory(tpl.category)
|
||||||
|
setDraftTags(tpl.tags.join(", "))
|
||||||
|
})
|
||||||
|
.catch(() => message.error("加载模板详情失败"))
|
||||||
|
}, [
|
||||||
|
loadedTemplateId,
|
||||||
|
resetClips,
|
||||||
|
setCurrentMode,
|
||||||
|
setTitleConfig,
|
||||||
|
setSubtitleSettings,
|
||||||
|
setBgmSettings,
|
||||||
|
setDraftName,
|
||||||
|
setDraftCategory,
|
||||||
|
setDraftTags,
|
||||||
|
])
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useState, useCallback, useEffect, useMemo } from "react"
|
||||||
|
import { message } from "antd"
|
||||||
|
import {
|
||||||
|
getEditingTemplates,
|
||||||
|
getTemplateCategories,
|
||||||
|
type EditingTemplate,
|
||||||
|
type TemplateCategory,
|
||||||
|
} from "@/api/editing-planner"
|
||||||
|
import { getMediaAssets, type MediaAsset } from "@/api/template-editor"
|
||||||
|
import { FILTER_CATEGORIES } from "../../constants"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板列表 + 分类 + 筛选搜索
|
||||||
|
*/
|
||||||
|
export function useTemplateList(
|
||||||
|
setMediaAssets: (assets: MediaAsset[]) => void,
|
||||||
|
initialTemplateId: string | null,
|
||||||
|
) {
|
||||||
|
const [templates, setTemplates] = useState<EditingTemplate[]>([])
|
||||||
|
const [categories, setCategories] = useState<TemplateCategory[]>([])
|
||||||
|
const [loadingTemplates, setLoadingTemplates] = useState(false)
|
||||||
|
const [currentFilter, setCurrentFilter] = useState("全部")
|
||||||
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
const [loadedTemplateId, setLoadedTemplateId] = useState<string | null>(initialTemplateId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 并行加载模板列表、分类、素材库
|
||||||
|
* 三个接口无依赖关系,用 Promise.all 并发
|
||||||
|
*/
|
||||||
|
const loadTemplates = useCallback(async () => {
|
||||||
|
setLoadingTemplates(true)
|
||||||
|
try {
|
||||||
|
const [tpls, cats, assets] = await Promise.all([
|
||||||
|
getEditingTemplates(),
|
||||||
|
getTemplateCategories(),
|
||||||
|
getMediaAssets(),
|
||||||
|
])
|
||||||
|
setTemplates(tpls)
|
||||||
|
setCategories(cats)
|
||||||
|
setMediaAssets(assets)
|
||||||
|
} catch {
|
||||||
|
message.error("加载模板失败")
|
||||||
|
} finally {
|
||||||
|
setLoadingTemplates(false)
|
||||||
|
}
|
||||||
|
}, [setMediaAssets])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTemplates()
|
||||||
|
}, [loadTemplates])
|
||||||
|
|
||||||
|
const filteredTemplates = useMemo(
|
||||||
|
() =>
|
||||||
|
templates.filter((t) => {
|
||||||
|
if (currentFilter !== "全部" && t.category !== currentFilter) return false
|
||||||
|
if (searchQuery && !t.name.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
[templates, currentFilter, searchQuery],
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentTemplate = useMemo(
|
||||||
|
() => templates.find((t) => t.id === loadedTemplateId) || null,
|
||||||
|
[templates, loadedTemplateId],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
templates,
|
||||||
|
categories,
|
||||||
|
loadingTemplates,
|
||||||
|
loadedTemplateId,
|
||||||
|
setLoadedTemplateId,
|
||||||
|
currentFilter,
|
||||||
|
setCurrentFilter,
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
filteredTemplates,
|
||||||
|
currentTemplate,
|
||||||
|
loadTemplates,
|
||||||
|
FILTER_CATEGORIES,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { useState, useCallback } from "react"
|
||||||
|
import { message } from "antd"
|
||||||
|
import {
|
||||||
|
createEditingTemplate,
|
||||||
|
updateEditingTemplate,
|
||||||
|
type SaveTemplatePayload,
|
||||||
|
type TemplateMode,
|
||||||
|
} from "@/api/editing-planner"
|
||||||
|
import type {
|
||||||
|
ClipData,
|
||||||
|
WatermarkConfig,
|
||||||
|
IntroOutroConfig,
|
||||||
|
PipConfig,
|
||||||
|
FilterConfig,
|
||||||
|
ChromaKeyConfig,
|
||||||
|
StickerConfig,
|
||||||
|
} from "../../types"
|
||||||
|
import type { SubtitleStyleConfig } from "../../types/subtitle"
|
||||||
|
import type { TitleConfig } from "@/api/template-editor"
|
||||||
|
import type { CoverConfig } from "../../types"
|
||||||
|
import type { BgmMixConfig } from "@/api/bgm"
|
||||||
|
|
||||||
|
interface UseTemplateSaveOptions {
|
||||||
|
currentMode: TemplateMode
|
||||||
|
clips: ClipData[]
|
||||||
|
totalDuration: number
|
||||||
|
titleConfig: TitleConfig
|
||||||
|
subtitleSettings: SubtitleStyleConfig
|
||||||
|
bgmSettings: BgmMixConfig
|
||||||
|
watermarkSettings: WatermarkConfig
|
||||||
|
introOutroSettings: IntroOutroConfig
|
||||||
|
pipSettings: PipConfig
|
||||||
|
filterSettings: FilterConfig
|
||||||
|
chromaKeySettings: ChromaKeyConfig
|
||||||
|
stickerSettings: StickerConfig
|
||||||
|
coverConfig: CoverConfig
|
||||||
|
loadedTemplateId: string | null
|
||||||
|
loadTemplates: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板保存(创建/更新)
|
||||||
|
*/
|
||||||
|
export function useTemplateSave(options: UseTemplateSaveOptions) {
|
||||||
|
const {
|
||||||
|
currentMode,
|
||||||
|
clips,
|
||||||
|
totalDuration,
|
||||||
|
titleConfig,
|
||||||
|
subtitleSettings,
|
||||||
|
bgmSettings,
|
||||||
|
watermarkSettings,
|
||||||
|
introOutroSettings,
|
||||||
|
pipSettings,
|
||||||
|
filterSettings,
|
||||||
|
chromaKeySettings,
|
||||||
|
stickerSettings,
|
||||||
|
coverConfig,
|
||||||
|
loadedTemplateId,
|
||||||
|
loadTemplates,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const [saveModalOpen, setSaveModalOpen] = useState(false)
|
||||||
|
const [draftName, setDraftName] = useState("")
|
||||||
|
const [draftCategory, setDraftCategory] = useState("")
|
||||||
|
const [draftTags, setDraftTags] = useState("")
|
||||||
|
const [saveLoading, setSaveLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleOpenSaveModal = useCallback(() => {
|
||||||
|
setSaveModalOpen(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
if (!draftName.trim()) {
|
||||||
|
message.warning("请输入模板名称")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSaveLoading(true)
|
||||||
|
try {
|
||||||
|
const payload: SaveTemplatePayload = {
|
||||||
|
name: draftName,
|
||||||
|
mode: currentMode,
|
||||||
|
category: draftCategory,
|
||||||
|
tags: draftTags
|
||||||
|
.split(",")
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
title_config: titleConfig,
|
||||||
|
subtitle_config: {
|
||||||
|
enabled: subtitleSettings.enabled,
|
||||||
|
position: subtitleSettings.position,
|
||||||
|
font: subtitleSettings.font,
|
||||||
|
color: subtitleSettings.fontColor,
|
||||||
|
size: subtitleSettings.fontSize,
|
||||||
|
animation: subtitleSettings.animation,
|
||||||
|
},
|
||||||
|
bgm_config: {
|
||||||
|
enabled: bgmSettings.enabled,
|
||||||
|
music_id: bgmSettings.music_id,
|
||||||
|
},
|
||||||
|
estimated_duration: totalDuration,
|
||||||
|
segments: clips.map((c, i) => ({
|
||||||
|
segment_order: i,
|
||||||
|
duration_min: Math.max(1, c.duration - 2),
|
||||||
|
duration_max: c.duration + 2,
|
||||||
|
material_type: c.type === "voice" ? "voiceover" : "video",
|
||||||
|
transition: c.transition
|
||||||
|
? { type: c.transition.type, duration: c.transition.duration }
|
||||||
|
: undefined,
|
||||||
|
playback_speed: c.speed ? c.speed.rate : undefined,
|
||||||
|
tts_config: c.tts_config
|
||||||
|
? {
|
||||||
|
mode: c.tts_config.mode,
|
||||||
|
text: c.tts_config.text,
|
||||||
|
voice_id: c.tts_config.voice_id,
|
||||||
|
speed: c.tts_config.speed,
|
||||||
|
pitch: c.tts_config.pitch,
|
||||||
|
volume: c.tts_config.volume,
|
||||||
|
subtitle_sync: c.tts_config.subtitle_sync,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
trim_config: c.trim_config
|
||||||
|
? {
|
||||||
|
start_time: c.trim_config.start_time,
|
||||||
|
end_time: c.trim_config.end_time,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
watermark_config: { ...watermarkSettings },
|
||||||
|
intro_outro_config: { ...introOutroSettings },
|
||||||
|
pip_config: { ...pipSettings },
|
||||||
|
filter_config: { ...filterSettings },
|
||||||
|
green_screen_config: { ...chromaKeySettings },
|
||||||
|
sticker_config: { ...stickerSettings },
|
||||||
|
cover_config: { ...coverConfig },
|
||||||
|
}
|
||||||
|
if (loadedTemplateId) {
|
||||||
|
await updateEditingTemplate(loadedTemplateId, payload)
|
||||||
|
} else {
|
||||||
|
await createEditingTemplate(payload)
|
||||||
|
}
|
||||||
|
message.success(loadedTemplateId ? "模板保存成功" : "模板创建成功")
|
||||||
|
setSaveModalOpen(false)
|
||||||
|
loadTemplates()
|
||||||
|
} catch {
|
||||||
|
message.error("保存失败")
|
||||||
|
} finally {
|
||||||
|
setSaveLoading(false)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
draftName,
|
||||||
|
draftCategory,
|
||||||
|
draftTags,
|
||||||
|
currentMode,
|
||||||
|
clips,
|
||||||
|
totalDuration,
|
||||||
|
titleConfig,
|
||||||
|
subtitleSettings,
|
||||||
|
bgmSettings,
|
||||||
|
watermarkSettings,
|
||||||
|
introOutroSettings,
|
||||||
|
pipSettings,
|
||||||
|
filterSettings,
|
||||||
|
chromaKeySettings,
|
||||||
|
stickerSettings,
|
||||||
|
coverConfig,
|
||||||
|
loadedTemplateId,
|
||||||
|
loadTemplates,
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
saveModalOpen,
|
||||||
|
setSaveModalOpen,
|
||||||
|
draftName,
|
||||||
|
setDraftName,
|
||||||
|
draftCategory,
|
||||||
|
setDraftCategory,
|
||||||
|
draftTags,
|
||||||
|
setDraftTags,
|
||||||
|
saveLoading,
|
||||||
|
handleOpenSaveModal,
|
||||||
|
handleSave,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,8 @@
|
|||||||
import { useState, useCallback, useEffect, type Dispatch, type SetStateAction } from "react"
|
import { useState, useCallback, type Dispatch, type SetStateAction } from "react"
|
||||||
import { FILTER_CATEGORIES } from "../constants"
|
import type { TemplateMode } from "@/api/editing-planner"
|
||||||
import { message } from "antd"
|
import type { MediaAsset, TitleConfig } from "@/api/template-editor"
|
||||||
import type {
|
|
||||||
EditingTemplate,
|
|
||||||
TemplateCategory,
|
|
||||||
TemplateMode,
|
|
||||||
SaveTemplatePayload,
|
|
||||||
} from "@/api/editing-planner"
|
|
||||||
import {
|
|
||||||
getEditingTemplates,
|
|
||||||
getEditingTemplate,
|
|
||||||
createEditingTemplate,
|
|
||||||
updateEditingTemplate,
|
|
||||||
getTemplateCategories,
|
|
||||||
} from "@/api/editing-planner"
|
|
||||||
import type { MediaAsset, TitleConfig, TransitionEffect } from "@/api/template-editor"
|
|
||||||
import { getMediaAssets, getEditPlan, getEditPlanClips } from "@/api/template-editor"
|
|
||||||
import type {
|
import type {
|
||||||
ClipData,
|
ClipData,
|
||||||
ClipType,
|
|
||||||
TtsConfig,
|
|
||||||
TtsMode,
|
|
||||||
TrimConfig,
|
|
||||||
WatermarkConfig,
|
WatermarkConfig,
|
||||||
IntroOutroConfig,
|
IntroOutroConfig,
|
||||||
PipConfig,
|
PipConfig,
|
||||||
@@ -32,6 +13,11 @@ import type {
|
|||||||
} from "../types"
|
} from "../types"
|
||||||
import type { SubtitleStyleConfig } from "../types/subtitle"
|
import type { SubtitleStyleConfig } from "../types/subtitle"
|
||||||
import type { BgmMixConfig } from "@/api/bgm"
|
import type { BgmMixConfig } from "@/api/bgm"
|
||||||
|
import { useTemplateList } from "./template-management/useTemplateList"
|
||||||
|
import { useTemplateDetail } from "./template-management/useTemplateDetail"
|
||||||
|
import { usePlanLoading } from "./template-management/usePlanLoading"
|
||||||
|
import { useTemplateSave } from "./template-management/useTemplateSave"
|
||||||
|
import { FILTER_CATEGORIES } from "../constants"
|
||||||
|
|
||||||
interface UseTemplateManagementParams {
|
interface UseTemplateManagementParams {
|
||||||
urlTemplateId: string
|
urlTemplateId: string
|
||||||
@@ -44,7 +30,6 @@ interface UseTemplateManagementParams {
|
|||||||
setSubtitleSettings: Dispatch<SetStateAction<SubtitleStyleConfig>>
|
setSubtitleSettings: Dispatch<SetStateAction<SubtitleStyleConfig>>
|
||||||
setBgmSettings: Dispatch<SetStateAction<BgmMixConfig>>
|
setBgmSettings: Dispatch<SetStateAction<BgmMixConfig>>
|
||||||
setCoverConfig: Dispatch<SetStateAction<CoverConfig>>
|
setCoverConfig: Dispatch<SetStateAction<CoverConfig>>
|
||||||
// 保存时需要的配置
|
|
||||||
clips: ClipData[]
|
clips: ClipData[]
|
||||||
totalDuration: number
|
totalDuration: number
|
||||||
titleConfig: TitleConfig
|
titleConfig: TitleConfig
|
||||||
@@ -61,7 +46,7 @@ interface UseTemplateManagementParams {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 模板管理 Hook
|
* 模板管理 Hook
|
||||||
* 模板列表/分类/加载/保存/模式切换/筛选搜索 + 3 个 useEffect
|
* 模板列表/分类/加载/保存/模式切换/筛选搜索
|
||||||
*/
|
*/
|
||||||
export const useTemplateManagement = (params: UseTemplateManagementParams) => {
|
export const useTemplateManagement = (params: UseTemplateManagementParams) => {
|
||||||
const {
|
const {
|
||||||
@@ -89,346 +74,101 @@ export const useTemplateManagement = (params: UseTemplateManagementParams) => {
|
|||||||
coverConfig,
|
coverConfig,
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
/* ── 模板列表 ── */
|
|
||||||
const [templates, setTemplates] = useState<EditingTemplate[]>([])
|
|
||||||
const [categories, setCategories] = useState<TemplateCategory[]>([])
|
|
||||||
const [loadingTemplates, setLoadingTemplates] = useState(false)
|
|
||||||
const [loadedTemplateId, setLoadedTemplateId] = useState<string | null>(urlTemplateId || null)
|
|
||||||
const [currentMode, setCurrentMode] = useState<TemplateMode>("pip")
|
const [currentMode, setCurrentMode] = useState<TemplateMode>("pip")
|
||||||
|
|
||||||
/* ── 左栏筛选 ── */
|
|
||||||
const [currentFilter, setCurrentFilter] = useState("全部")
|
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
|
||||||
|
|
||||||
/* ── 保存弹窗 ── */
|
|
||||||
const [saveModalOpen, setSaveModalOpen] = useState(false)
|
|
||||||
const [draftName, setDraftName] = useState("")
|
|
||||||
const [draftCategory, setDraftCategory] = useState("")
|
|
||||||
const [draftTags, setDraftTags] = useState("")
|
|
||||||
const [saveLoading, setSaveLoading] = useState(false)
|
|
||||||
|
|
||||||
/* ── 计划 ID(从 URL 传入,不变) ── */
|
|
||||||
const [loadedPlanId] = useState<string | null>(urlPlanId || null)
|
const [loadedPlanId] = useState<string | null>(urlPlanId || null)
|
||||||
|
|
||||||
/* ── 计算 ── */
|
/* ── 模板列表 ── */
|
||||||
const filteredTemplates = templates.filter((t) => {
|
const {
|
||||||
if (currentFilter !== "全部" && t.category !== currentFilter) return false
|
templates,
|
||||||
if (searchQuery && !t.name.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
categories,
|
||||||
return true
|
loadingTemplates,
|
||||||
|
loadedTemplateId,
|
||||||
|
setLoadedTemplateId,
|
||||||
|
currentFilter,
|
||||||
|
setCurrentFilter,
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
filteredTemplates,
|
||||||
|
currentTemplate,
|
||||||
|
loadTemplates,
|
||||||
|
} = useTemplateList(setMediaAssets, urlTemplateId || null)
|
||||||
|
|
||||||
|
/* ── 保存 ── */
|
||||||
|
const {
|
||||||
|
saveModalOpen,
|
||||||
|
setSaveModalOpen,
|
||||||
|
draftName,
|
||||||
|
setDraftName,
|
||||||
|
draftCategory,
|
||||||
|
setDraftCategory,
|
||||||
|
draftTags,
|
||||||
|
setDraftTags,
|
||||||
|
saveLoading,
|
||||||
|
handleOpenSaveModal,
|
||||||
|
handleSave,
|
||||||
|
} = useTemplateSave({
|
||||||
|
currentMode,
|
||||||
|
clips,
|
||||||
|
totalDuration,
|
||||||
|
titleConfig,
|
||||||
|
subtitleSettings,
|
||||||
|
bgmSettings,
|
||||||
|
watermarkSettings,
|
||||||
|
introOutroSettings,
|
||||||
|
pipSettings,
|
||||||
|
filterSettings,
|
||||||
|
chromaKeySettings,
|
||||||
|
stickerSettings,
|
||||||
|
coverConfig,
|
||||||
|
loadedTemplateId,
|
||||||
|
loadTemplates,
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentTemplate = templates.find((t) => t.id === loadedTemplateId) || null
|
/* ── 模板详情加载 ── */
|
||||||
|
useTemplateDetail({
|
||||||
|
loadedTemplateId,
|
||||||
|
resetClips,
|
||||||
|
setCurrentMode,
|
||||||
|
setTitleConfig,
|
||||||
|
setSubtitleSettings,
|
||||||
|
setBgmSettings,
|
||||||
|
setDraftName,
|
||||||
|
setDraftCategory,
|
||||||
|
setDraftTags,
|
||||||
|
})
|
||||||
|
|
||||||
/* ──────────── 加载 ──────────── */
|
/* ── 计划草稿加载 ── */
|
||||||
|
usePlanLoading({
|
||||||
/**
|
|
||||||
* 并行加载模板列表、分类、素材库
|
|
||||||
* 首次挂载时调用,三个接口无依赖关系,用 Promise.all 并发
|
|
||||||
*/
|
|
||||||
const loadTemplates = useCallback(async () => {
|
|
||||||
setLoadingTemplates(true)
|
|
||||||
try {
|
|
||||||
const [tpls, cats, assets] = await Promise.all([
|
|
||||||
getEditingTemplates(),
|
|
||||||
getTemplateCategories(),
|
|
||||||
getMediaAssets(),
|
|
||||||
])
|
|
||||||
setTemplates(tpls)
|
|
||||||
setCategories(cats)
|
|
||||||
setMediaAssets(assets)
|
|
||||||
} catch {
|
|
||||||
message.error("加载模板失败")
|
|
||||||
} finally {
|
|
||||||
setLoadingTemplates(false)
|
|
||||||
}
|
|
||||||
}, [setMediaAssets])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadTemplates()
|
|
||||||
}, [loadTemplates])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载模板详情并初始化片段列表
|
|
||||||
* 将后端 segments 映射为前端 ClipData,取 duration_min/max 均值作为默认时长
|
|
||||||
* 同时还原标题/字幕/BGM 配置
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loadedTemplateId) return
|
|
||||||
getEditingTemplate(loadedTemplateId)
|
|
||||||
.then((tpl) => {
|
|
||||||
if (!tpl) return
|
|
||||||
setCurrentMode(tpl.mode)
|
|
||||||
const mapped: ClipData[] = tpl.segments.map((seg, idx) => ({
|
|
||||||
id: seg.id || `seg-${idx}`,
|
|
||||||
template_segment_id: seg.id || `seg-${idx}`,
|
|
||||||
type: (seg.material_type === "voiceover" ? "voice" : "pip") as ClipType,
|
|
||||||
duration: (seg.duration_min + seg.duration_max) / 2,
|
|
||||||
startOffset: 0,
|
|
||||||
script_text: "",
|
|
||||||
order: seg.segment_order,
|
|
||||||
}))
|
|
||||||
resetClips(mapped)
|
|
||||||
|
|
||||||
setTitleConfig({
|
|
||||||
ai_auto_select: tpl.title_config.ai_auto_select,
|
|
||||||
content: tpl.title_config.content,
|
|
||||||
position: tpl.title_config.position,
|
|
||||||
font_preset: tpl.title_config.font_preset,
|
|
||||||
font_size: tpl.title_config.font_size,
|
|
||||||
font_color: tpl.title_config.font_color || "#ffffff",
|
|
||||||
})
|
|
||||||
setSubtitleSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
enabled: tpl.subtitle_config.enabled,
|
|
||||||
position: (tpl.subtitle_config.position || "bottom") as SubtitleStyleConfig["position"],
|
|
||||||
font: tpl.subtitle_config.font,
|
|
||||||
fontSize: tpl.subtitle_config.size,
|
|
||||||
fontColor: tpl.subtitle_config.color || "#ffffff",
|
|
||||||
animation: tpl.subtitle_config.animation,
|
|
||||||
}))
|
|
||||||
setBgmSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
enabled: tpl.bgm_config.enabled,
|
|
||||||
music_id: tpl.bgm_config.music_id || "",
|
|
||||||
}))
|
|
||||||
setDraftName(tpl.name)
|
|
||||||
setDraftCategory(tpl.category)
|
|
||||||
setDraftTags(tpl.tags.join(", "))
|
|
||||||
})
|
|
||||||
.catch(() => message.error("加载模板详情失败"))
|
|
||||||
}, [loadedTemplateId, resetClips, setTitleConfig, setSubtitleSettings, setBgmSettings])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载已有模板草稿数据到编辑器
|
|
||||||
* 从列表页"编辑"按钮进入时,URL 带 planId,需要还原计划配置
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loadedPlanId) return
|
|
||||||
|
|
||||||
// 并行加载计划基本信息 + 片段列表
|
|
||||||
Promise.all([
|
|
||||||
getEditPlan(loadedPlanId),
|
|
||||||
getEditPlanClips(loadedPlanId, { limit: 500 }).catch(() => ({
|
|
||||||
items: [],
|
|
||||||
total: 0,
|
|
||||||
})),
|
|
||||||
])
|
|
||||||
.then(([plan, clipsRes]) => {
|
|
||||||
// 设置关联的模板(触发模板加载 effect)
|
|
||||||
setLoadedTemplateId(plan.template_id)
|
|
||||||
|
|
||||||
// 还原基本信息
|
|
||||||
setDraftName(plan.name)
|
|
||||||
|
|
||||||
// 还原 config 中的编辑器状态
|
|
||||||
const cfg = plan.config
|
|
||||||
if (cfg.title_config) {
|
|
||||||
setTitleConfig({
|
|
||||||
ai_auto_select: cfg.title_config!.ai_auto_select,
|
|
||||||
content: cfg.title_config!.content,
|
|
||||||
position: cfg.title_config!.position,
|
|
||||||
font_preset: cfg.title_config!.font_preset,
|
|
||||||
font_size: cfg.title_config!.font_size,
|
|
||||||
font_color: cfg.title_config!.font_color || "#ffffff",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (cfg.subtitle_config) {
|
|
||||||
setSubtitleSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
enabled: cfg.subtitle_config!.enabled,
|
|
||||||
position: (cfg.subtitle_config!.position ||
|
|
||||||
"bottom") as SubtitleStyleConfig["position"],
|
|
||||||
font: cfg.subtitle_config!.font,
|
|
||||||
fontSize: cfg.subtitle_config!.size,
|
|
||||||
fontColor: cfg.subtitle_config!.color || "#ffffff",
|
|
||||||
animation: cfg.subtitle_config!.animation,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (cfg.bgm_config) {
|
|
||||||
setBgmSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
enabled: cfg.bgm_config!.enabled,
|
|
||||||
music_id: cfg.bgm_config!.music_id || "",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
// 还原封面配置
|
|
||||||
if (cfg.cover_config) {
|
|
||||||
setCoverConfig((prev) => ({
|
|
||||||
...prev,
|
|
||||||
enabled: cfg.cover_config!.enabled ?? prev.enabled,
|
|
||||||
mode: (cfg.cover_config!.mode as CoverConfig["mode"]) || prev.mode,
|
|
||||||
frame_time: cfg.cover_config!.frame_time ?? prev.frame_time,
|
|
||||||
upload_url: cfg.cover_config!.upload_url || prev.upload_url,
|
|
||||||
thumbnail_url: cfg.cover_config!.thumbnail_url || prev.thumbnail_url,
|
|
||||||
ai_suggested_time: cfg.cover_config!.ai_suggested_time ?? prev.ai_suggested_time,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 还原片段:优先从后端 clips 表,其次从 config.segments 兜底
|
|
||||||
const backendClips = clipsRes?.items || []
|
|
||||||
if (backendClips.length > 0) {
|
|
||||||
// 从后端 clips 表还原
|
|
||||||
const sorted = [...backendClips].sort((a, b) => a.order - b.order)
|
|
||||||
const mapped: ClipData[] = sorted.map((clip) => ({
|
|
||||||
id: clip.id,
|
|
||||||
template_segment_id: (clip.config?.template_segment_id as string) || "",
|
|
||||||
type: (clip.clip_type === "voiceover" ? "voice" : "pip") as ClipType,
|
|
||||||
duration: clip.duration || 3,
|
|
||||||
startOffset: 0,
|
|
||||||
script_text: clip.text_content || "",
|
|
||||||
order: clip.order,
|
|
||||||
media_asset_id: clip.asset_id || undefined,
|
|
||||||
transition:
|
|
||||||
clip.transition_effect && clip.transition_effect !== "none"
|
|
||||||
? {
|
|
||||||
type: clip.transition_effect as TransitionEffect["type"],
|
|
||||||
duration: clip.transition_duration || 0.3,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
speed: clip.playback_speed
|
|
||||||
? { rate: clip.playback_speed, pitchCorrection: true }
|
|
||||||
: undefined,
|
|
||||||
tts_config: (clip.config?.tts_config as TtsConfig) || undefined,
|
|
||||||
trim_config: (clip.config?.trim_config as TrimConfig) || undefined,
|
|
||||||
}))
|
|
||||||
setTimeout(() => resetClips(mapped), 100)
|
|
||||||
} else if (cfg.segments && cfg.segments.length > 0) {
|
|
||||||
// 兜底:从 config.segments 还原(老数据兼容)
|
|
||||||
const mapped: ClipData[] = cfg.segments.map((seg, idx) => ({
|
|
||||||
id: `seg-${idx}`,
|
|
||||||
template_segment_id: `seg-${idx}`,
|
|
||||||
type: (seg.material_type === "voiceover" ? "voice" : "pip") as ClipType,
|
|
||||||
duration: (seg.duration_min + seg.duration_max) / 2,
|
|
||||||
startOffset: 0,
|
|
||||||
script_text: "",
|
|
||||||
order: seg.segment_order,
|
|
||||||
transition: seg.transition
|
|
||||||
? {
|
|
||||||
type: seg.transition.type as TransitionEffect["type"],
|
|
||||||
duration: seg.transition.duration,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
speed: seg.playback_speed
|
|
||||||
? { rate: seg.playback_speed, pitchCorrection: true }
|
|
||||||
: undefined,
|
|
||||||
tts_config: seg.tts_config
|
|
||||||
? { ...seg.tts_config, mode: seg.tts_config.mode as TtsMode }
|
|
||||||
: undefined,
|
|
||||||
trim_config: seg.trim_config || undefined,
|
|
||||||
}))
|
|
||||||
setTimeout(() => resetClips(mapped), 100)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => message.error("加载模板草稿失败"))
|
|
||||||
}, [
|
|
||||||
loadedPlanId,
|
loadedPlanId,
|
||||||
resetClips,
|
resetClips,
|
||||||
|
setLoadedTemplateId,
|
||||||
|
setDraftName,
|
||||||
setTitleConfig,
|
setTitleConfig,
|
||||||
setSubtitleSettings,
|
setSubtitleSettings,
|
||||||
setBgmSettings,
|
setBgmSettings,
|
||||||
setCoverConfig,
|
setCoverConfig,
|
||||||
])
|
})
|
||||||
|
|
||||||
/* ──────────── 事件 ──────────── */
|
/* ── 事件 ── */
|
||||||
|
const handleLoadTemplate = useCallback(
|
||||||
|
(templateId: string) => {
|
||||||
|
setLoadedTemplateId(templateId)
|
||||||
|
setSelectedClipId(null)
|
||||||
|
},
|
||||||
|
[setLoadedTemplateId, setSelectedClipId],
|
||||||
|
)
|
||||||
|
|
||||||
const handleLoadTemplate = (templateId: string) => {
|
const handleModeChange = useCallback(
|
||||||
setLoadedTemplateId(templateId)
|
(mode: TemplateMode) => {
|
||||||
setSelectedClipId(null)
|
setCurrentMode(mode)
|
||||||
}
|
if (mode === "voice_over") {
|
||||||
|
setClips((prev) => prev.map((c) => ({ ...c, type: "voice" as const })))
|
||||||
const handleModeChange = (mode: TemplateMode) => {
|
} else if (mode === "pip") {
|
||||||
setCurrentMode(mode)
|
setClips((prev) => prev.map((c) => ({ ...c, type: "pip" as const })))
|
||||||
// 切换纯单类型模式时,自动转换所有已有片段的类型
|
|
||||||
if (mode === "voice_over") {
|
|
||||||
setClips((prev) => prev.map((c) => ({ ...c, type: "voice" as const })))
|
|
||||||
} else if (mode === "pip") {
|
|
||||||
setClips((prev) => prev.map((c) => ({ ...c, type: "pip" as const })))
|
|
||||||
}
|
|
||||||
// 混合模式(voice_pip)和一镜到底(one_take)不自动转换,保留原有类型
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpenSaveModal = () => {
|
|
||||||
setSaveModalOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!draftName.trim()) {
|
|
||||||
message.warning("请输入模板名称")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSaveLoading(true)
|
|
||||||
try {
|
|
||||||
const payload: SaveTemplatePayload = {
|
|
||||||
name: draftName,
|
|
||||||
mode: currentMode,
|
|
||||||
category: draftCategory,
|
|
||||||
tags: draftTags
|
|
||||||
.split(",")
|
|
||||||
.map((t) => t.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
title_config: titleConfig,
|
|
||||||
subtitle_config: {
|
|
||||||
enabled: subtitleSettings.enabled,
|
|
||||||
position: subtitleSettings.position,
|
|
||||||
font: subtitleSettings.font,
|
|
||||||
color: subtitleSettings.fontColor,
|
|
||||||
size: subtitleSettings.fontSize,
|
|
||||||
animation: subtitleSettings.animation,
|
|
||||||
},
|
|
||||||
bgm_config: {
|
|
||||||
enabled: bgmSettings.enabled,
|
|
||||||
music_id: bgmSettings.music_id,
|
|
||||||
},
|
|
||||||
estimated_duration: totalDuration,
|
|
||||||
segments: clips.map((c, i) => ({
|
|
||||||
segment_order: i,
|
|
||||||
duration_min: Math.max(1, c.duration - 2),
|
|
||||||
duration_max: c.duration + 2,
|
|
||||||
material_type: c.type === "voice" ? "voiceover" : "video",
|
|
||||||
transition: c.transition
|
|
||||||
? { type: c.transition.type, duration: c.transition.duration }
|
|
||||||
: undefined,
|
|
||||||
playback_speed: c.speed ? c.speed.rate : undefined,
|
|
||||||
tts_config: c.tts_config
|
|
||||||
? {
|
|
||||||
mode: c.tts_config.mode,
|
|
||||||
text: c.tts_config.text,
|
|
||||||
voice_id: c.tts_config.voice_id,
|
|
||||||
speed: c.tts_config.speed,
|
|
||||||
pitch: c.tts_config.pitch,
|
|
||||||
volume: c.tts_config.volume,
|
|
||||||
subtitle_sync: c.tts_config.subtitle_sync,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
trim_config: c.trim_config
|
|
||||||
? {
|
|
||||||
start_time: c.trim_config.start_time,
|
|
||||||
end_time: c.trim_config.end_time,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
})),
|
|
||||||
watermark_config: { ...watermarkSettings },
|
|
||||||
intro_outro_config: { ...introOutroSettings },
|
|
||||||
pip_config: { ...pipSettings },
|
|
||||||
filter_config: { ...filterSettings },
|
|
||||||
green_screen_config: { ...chromaKeySettings },
|
|
||||||
sticker_config: { ...stickerSettings },
|
|
||||||
cover_config: { ...coverConfig },
|
|
||||||
}
|
}
|
||||||
if (loadedTemplateId) {
|
},
|
||||||
await updateEditingTemplate(loadedTemplateId, payload)
|
[setClips],
|
||||||
} else {
|
)
|
||||||
await createEditingTemplate(payload)
|
|
||||||
}
|
|
||||||
message.success(loadedTemplateId ? "模板保存成功" : "模板创建成功")
|
|
||||||
setSaveModalOpen(false)
|
|
||||||
loadTemplates()
|
|
||||||
} catch {
|
|
||||||
message.error("保存失败")
|
|
||||||
} finally {
|
|
||||||
setSaveLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// state
|
// state
|
||||||
|
|||||||
Reference in New Issue
Block a user