diff --git a/apps/web/src/api/editPlans.ts b/apps/web/src/api/editPlans.ts index 90777846d..2280d7282 100644 --- a/apps/web/src/api/editPlans.ts +++ b/apps/web/src/api/editPlans.ts @@ -31,6 +31,8 @@ export interface CreateEditPlanRequest { name: string; config?: Record; total_duration?: number; + /** 来源剪辑计划 ID(从剪辑计划跳转到一键生成时关联) */ + source_edit_plan_id?: string; } /** 更新剪辑计划请求 */ @@ -49,6 +51,16 @@ export interface GenerateResponse { clip_count: number; } +/** 剪辑计划关联的生成记录 */ +export interface EditPlanGeneration { + id: string; + edit_plan_id: string; + generation_task_id: string; + status: EditPlanStatus; + created_at: string; + updated_at: string; +} + /** 片段生成状态 */ export interface ClipStatusItem { clip_id: string; @@ -246,6 +258,14 @@ export async function generateCover( return response.data; } +/** 获取剪辑计划关联的生成记录 */ +export async function getEditPlanGenerations( + planId: string, +): Promise { + const response = await apiClient.get(`/edit-plans/${planId}/generations`); + return response.data.items || []; +} + /** * 获取素材库列表 — 调用 GET /api/v1/assets?library_id=xxx * 将后端 AssetResponse 映射为前端 MediaAsset 类型 diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index bd33b5967..7efb6f698 100644 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -3,7 +3,7 @@ * 四行布局:顶栏(42px) → 模式栏(48px) → 三栏主体 → 底栏(40px) */ import React, { useState, useCallback, useEffect } from "react"; -import { useSearchParams } from "react-router-dom"; +import { useSearchParams, useNavigate } from "react-router-dom"; // 简易消息工具(UI 库未导出 message) const msg = { success: (s: string) => console.log(`✅ ${s}`), @@ -75,6 +75,7 @@ const FILTER_CATEGORIES = ["全部", "种草", "知识", "日常", "推荐"]; const EditingPlanner: React.FC = () => { const [searchParams] = useSearchParams(); + const navigate = useNavigate(); const urlTemplateId = searchParams.get("templateId") || ""; /* ── 模板列表 ── */ @@ -440,15 +441,52 @@ const EditingPlanner: React.FC = () => { } }; - /* 生成 */ - const handleOpenGenerateModal = () => { + /* 跳转到一键生成页面,携带当前编辑器配置 */ + const handleGoToGenerate = () => { if (!loadedTemplateId) { msg.warning("请先加载一个模板"); return; } - setGenPhase("setup"); - setGenTask(null); - setGenModalOpen(true); + // 构建配置对象,传递给一键生成页面 + const planConfig = { + title_config: { + ai_auto_select: titleSettings.aiAutoSelect, + content: titleSettings.title, + position: titleSettings.position, + font_preset: titleSettings.font, + font_size: titleSettings.size, + bold: titleSettings.bold, + italic: titleSettings.italic, + stroke: titleSettings.stroke, + shadow: titleSettings.shadow, + }, + subtitle_config: { + enabled: subtitleSettings.enabled, + position: subtitleSettings.position, + font: subtitleSettings.font, + size: subtitleSettings.size, + animation: subtitleSettings.animation, + }, + bgm_config: { + enabled: bgmSettings.music !== "none", + music_id: bgmSettings.music, + }, + mode: currentMode, + total_duration: totalDuration, + segments: clips.map((c, i) => ({ + order: i, + name: c.name, + material_type: c.material_type, + duration: c.duration, + media_asset_id: c.media_asset_id, + template_segment_id: c.template_segment_id, + script_text: c.script_text, + })), + }; + const params = new URLSearchParams(); + params.set("edit_plan_id", loadedTemplateId); + params.set("plan_config", JSON.stringify(planConfig)); + navigate(`/app/generate?${params.toString()}`); }; const handleGenerate = async () => { @@ -557,7 +595,7 @@ const EditingPlanner: React.FC = () => { @@ -658,6 +696,22 @@ const EditingPlanner: React.FC = () => { 🎬 {MODE_LABELS[currentMode]} | 📐 模板片段: {currentTemplate?.segments.length || 0} + | + diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 65822c93b..661b3d959 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -47,6 +47,8 @@ import type { VoiceClone } from "@/api/voiceClone"; import CloneModal from "@/components/voice/CloneModal"; import { synthesizeSpeech, getTTSJobStatus } from "@/api/tts"; import { useCloneProgress } from "@/hooks/useCloneProgress"; +import { useSearchParams } from "react-router-dom"; +import { getEditPlan } from "@/api/editPlans"; import "./generate.css"; const { TextArea } = Input; @@ -206,6 +208,98 @@ const GeneratePage: React.FC = () => { const progressTimer = useRef>(undefined); const audioRef = useRef(null); + /* ── URL 参数:从剪辑计划跳转过来时携带 edit_plan_id + plan_config ── */ + const [searchParams] = useSearchParams(); + const editPlanId = searchParams.get("edit_plan_id"); + const planConfigStr = searchParams.get("plan_config"); + + /** 解析 plan_config 并自动填充表单(从剪辑计划跳转过来时) */ + useEffect(() => { + if (!planConfigStr) return; + try { + const config = JSON.parse(planConfigStr) as { + title_config?: { + content?: string; + ai_auto_select?: boolean; + }; + 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?.content) { + setTitle(config.title_config.content); + } + if (config.title_config?.ai_auto_select === false) { + setTitleMode("manual"); + } + + // 填充字幕 + if (config.subtitle_config?.enabled !== undefined) { + setAutoSubtitles(config.subtitle_config.enabled); + } + + // 填充 BGM + if (config.bgm_config) { + setBgm(config.bgm_config.enabled ?? true); + } + + // 填充时长 + if (config.total_duration && config.total_duration > 0) { + setDuration(config.total_duration); + } + + // 填充素材列表(从 segments 中提取 media_asset_id) + 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]); + + /** 如果没有 plan_config,尝试通过 edit_plan_id 从后端拉取配置 */ + useEffect(() => { + if (!editPlanId || planConfigStr) return; // 有 plan_config 时不走这个 fallback + const loadPlanConfig = async () => { + try { + const plan = await getEditPlan(editPlanId); + if (plan.name) setTitle(plan.name); + if (plan.total_duration > 0) setDuration(plan.total_duration); + const cfg = plan.config as Record; + if (cfg) { + if (Array.isArray(cfg.asset_ids)) { + setSelectedMaterials(cfg.asset_ids.filter((v): v is string => typeof v === "string")); + } + if (typeof cfg.auto_subtitles === "boolean") setAutoSubtitles(cfg.auto_subtitles); + if (typeof cfg.bgm === "boolean") setBgm(cfg.bgm); + if (typeof cfg.ratio === "string") setVideoRatio(cfg.ratio); + if (typeof cfg.style === "string") setStyle(cfg.style); + if (typeof cfg.duration === "number") setDuration(cfg.duration); + } + } catch (err) { + console.warn("加载剪辑计划配置失败:", err); + } + }; + loadPlanConfig(); + }, [editPlanId, planConfigStr]); + /* ── 预置音色:通过 API 获取 ── */ const { data: presetVoicesData, isLoading: presetVoicesLoading } = useQuery({ queryKey: ["preset-voices"], @@ -400,6 +494,8 @@ const GeneratePage: React.FC = () => { bgm, }, total_duration: duration, + // 从剪辑计划跳转过来时,关联来源剪辑计划 + source_edit_plan_id: editPlanId || undefined, }); // 2. 触发视频生成 @@ -461,6 +557,7 @@ const GeneratePage: React.FC = () => { duration, autoSubtitles, bgm, + editPlanId, ]); /* ── 字数统计 ── */ @@ -507,6 +604,14 @@ const GeneratePage: React.FC = () => { 快速生成短视频,支持多种风格和素材组合 + {editPlanId && ( + + 🎬 来自剪辑计划 + + )}