import { useEffect } from "react" import type { CoverConfig } from "../../types/cover" import type { TitleSettings } from "../../types" import type { TitleLineOverride } from "@/components/title/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 } /** #2001:统一归一化 title_config snake_case -> camelCase TitleSettings */ function mapTitleCfgToSettings( prev: TitleSettings, tc: TitleConfig & Record, ): TitleSettings { const stroke = tc.stroke as boolean | { enabled?: boolean; width?: number; color?: string } | undefined const strokeEnabled: boolean | undefined = typeof stroke === "object" && stroke ? stroke.enabled !== false : !!stroke || undefined const strokeW: number | undefined = typeof stroke === "object" && stroke ? (stroke.width ?? (tc.stroke_width as number | undefined)) : (tc.stroke_width as number | undefined) const strokeC: string | undefined = typeof stroke === "object" && stroke ? (stroke.color ?? (tc.stroke_color as string | undefined)) : (tc.stroke_color as string | undefined) const shadow = tc.shadow as | boolean | { enabled?: boolean; offset_x?: number; offset_y?: number; blur?: number; color?: string } | undefined const shadowEnabled: boolean | undefined = typeof shadow === "object" && shadow ? shadow.enabled !== false : !!shadow || undefined const shOffX: number | undefined = typeof shadow === "object" && shadow ? (shadow.offset_x ?? (tc.shadow_offset_x as number | undefined)) : (tc.shadow_offset_x as number | undefined) const shOffY: number | undefined = typeof shadow === "object" && shadow ? (shadow.offset_y ?? (tc.shadow_offset_y as number | undefined)) : (tc.shadow_offset_y as number | undefined) const shBlur: number | undefined = typeof shadow === "object" && shadow ? (shadow.blur ?? (tc.shadow_blur as number | undefined)) : (tc.shadow_blur as number | undefined) const shColor: string | undefined = typeof shadow === "object" && shadow ? (shadow.color ?? (tc.shadow_color as string | undefined)) : (tc.shadow_color as string | undefined) const bg = tc.background as { enabled?: boolean; color?: string; padding?: number; radius?: number } | undefined const bgEnabled: boolean | undefined = (bg && typeof bg === "object" ? bg.enabled : undefined) ?? (tc.bg_enabled as boolean | undefined) const bgColor: string | undefined = (bg && typeof bg === "object" ? bg.color : undefined) ?? (tc.bg_color as string | undefined) const bgPadding: number | undefined = (bg && typeof bg === "object" ? bg.padding : undefined) ?? (tc.bg_padding as number | undefined) const bgRadius: number | undefined = (bg && typeof bg === "object" ? bg.radius : undefined) ?? (tc.bg_radius as number | undefined) const ct = (tc.cover_title_config ?? null) as null | Record let coverTitle: TitleSettings["coverTitle"] = prev.coverTitle if (ct) { const ctStroke = ct.stroke as boolean | { enabled?: boolean; width?: number; color?: string } | undefined const ctShadow = ct.shadow as | boolean | { enabled?: boolean; offset_x?: number; offset_y?: number; blur?: number; color?: string } | undefined const ctBg = ct.background as { enabled?: boolean; color?: string; padding?: number; radius?: number } | undefined coverTitle = { title: (ct.title as string | undefined) ?? prev.coverTitle?.title ?? "", font: (ct.font as string | undefined) ?? prev.coverTitle?.font, size: (ct.font_size as number | undefined) ?? (ct.size as number | undefined) ?? prev.coverTitle?.size, color: (ct.font_color as string | undefined) ?? (ct.color as string | undefined) ?? prev.coverTitle?.color, bold: (ct.bold as boolean | undefined) ?? prev.coverTitle?.bold, italic: (ct.italic as boolean | undefined) ?? prev.coverTitle?.italic, position: (ct.position as string | undefined) ?? prev.coverTitle?.position, stroke: typeof ctStroke === "object" && ctStroke ? ctStroke.enabled !== false : ((ctStroke as boolean | undefined) ?? prev.coverTitle?.stroke), strokeWidth: (typeof ctStroke === "object" && ctStroke ? ctStroke.width : undefined) ?? (ct.stroke_width as number | undefined) ?? prev.coverTitle?.strokeWidth, strokeColor: (typeof ctStroke === "object" && ctStroke ? ctStroke.color : undefined) ?? (ct.stroke_color as string | undefined) ?? prev.coverTitle?.strokeColor, shadow: typeof ctShadow === "object" && ctShadow ? ctShadow.enabled !== false : ((ctShadow as boolean | undefined) ?? prev.coverTitle?.shadow), shadowOffsetX: (typeof ctShadow === "object" && ctShadow ? ctShadow.offset_x : undefined) ?? (ct.shadow_offset_x as number | undefined) ?? prev.coverTitle?.shadowOffsetX, shadowOffsetY: (typeof ctShadow === "object" && ctShadow ? ctShadow.offset_y : undefined) ?? (ct.shadow_offset_y as number | undefined) ?? prev.coverTitle?.shadowOffsetY, shadowBlur: (typeof ctShadow === "object" && ctShadow ? ctShadow.blur : undefined) ?? (ct.shadow_blur as number | undefined) ?? prev.coverTitle?.shadowBlur, shadowColor: (typeof ctShadow === "object" && ctShadow ? ctShadow.color : undefined) ?? (ct.shadow_color as string | undefined) ?? prev.coverTitle?.shadowColor, bgEnabled: ctBg?.enabled ?? (ct.bg_enabled as boolean | undefined) ?? prev.coverTitle?.bgEnabled, bgColor: ctBg?.color ?? (ct.bg_color as string | undefined) ?? prev.coverTitle?.bgColor, bgPadding: ctBg?.padding ?? (ct.bg_padding as number | undefined) ?? prev.coverTitle?.bgPadding, bgRadius: ctBg?.radius ?? (ct.bg_radius as number | undefined) ?? prev.coverTitle?.bgRadius, } } const result: TitleSettings = { ...prev, title: (tc.content as string | undefined) || prev.title, aiAutoSelect: (tc.ai_auto_select as boolean | undefined) || false, position: prev.position, font: (tc.font_preset as string | undefined) || prev.font, size: (tc.font_size as number | undefined) || prev.size, color: (tc.font_color as string | undefined) || prev.color, bold: (tc.bold as boolean | undefined) ?? prev.bold, italic: (tc.italic as boolean | undefined) ?? prev.italic, stroke: strokeEnabled ?? prev.stroke, strokeWidth: strokeW ?? prev.strokeWidth, strokeColor: strokeC ?? prev.strokeColor, shadow: shadowEnabled ?? prev.shadow, shadowOffsetX: shOffX ?? prev.shadowOffsetX, shadowOffsetY: shOffY ?? prev.shadowOffsetY, shadowBlur: shBlur ?? prev.shadowBlur, shadowColor: shColor ?? prev.shadowColor, lineHeight: (tc.line_height as number | undefined) ?? prev.lineHeight, marginTop: (tc.margin_top as number | undefined) ?? prev.marginTop, maxCharsPerLine: (tc.max_chars_per_line as number | undefined) ?? prev.maxCharsPerLine, bgEnabled: bgEnabled ?? prev.bgEnabled, bgColor: bgColor ?? prev.bgColor, bgPadding: bgPadding ?? prev.bgPadding, bgRadius: bgRadius ?? prev.bgRadius, lineOverrides: ((tc.line_overrides as unknown[] | undefined) ?? []) as TitleLineOverride[], coverTitle, } return result } /** * 从 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?: Record 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 & Record setTitleSettings((prev: TitleSettings) => mapTitleCfgToSettings(prev, tc)) } 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: TitleSettings) => ({ ...prev, title: plan.name })) const cfg = plan.config if (cfg?.title_config) { const tc2 = cfg.title_config as unknown as TitleConfig & Record setTitleSettings((prev: TitleSettings) => mapTitleCfgToSettings(prev, tc2)) } if (cfg?.cover_config) { const cc = cfg.cover_config as CoverConfig setCoverSettings((prev: CoverConfig) => ({ ...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]) }