feat: 剪辑计划与一键生成串联 — URL 参数传递配置 + source_edit_plan_id 关联
Deploy / Deploy Staging (push) Failing after 112h0m23s
CI/CD Pipeline / Frontend Lint (push) Failing after 112h0m30s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 112h0m30s
Deploy / Production Browser E2E (push) Failing after 1770h31m52s
Deploy / Build Production Runtime Images (push) Failing after 1770h33m59s
Deploy / Staging E2E Tests (push) Failing after 1770h31m54s
Deploy / Deploy Production (push) Failing after 1771h3m20s

- editPlans.ts: 新增 source_edit_plan_id 字段 + EditPlanGeneration 类型 + getEditPlanGenerations API
- EditingPlanner.tsx: 顶栏按钮跳转到一键生成(携带 edit_plan_id + plan_config)+ 底栏生成历史入口
- GeneratePage.tsx: 读取 URL 参数自动填充表单 + 创建计划时传递 source_edit_plan_id + 来源标签提示

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
灵应
2026-07-04 20:53:56 +08:00
parent 0e94adf7b6
commit f447048dc7
3 changed files with 186 additions and 7 deletions
+20
View File
@@ -31,6 +31,8 @@ export interface CreateEditPlanRequest {
name: string;
config?: Record<string, unknown>;
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<EditPlanGeneration[]> {
const response = await apiClient.get(`/edit-plans/${planId}/generations`);
return response.data.items || [];
}
/**
* 获取素材库列表 — 调用 GET /api/v1/assets?library_id=xxx
* 将后端 AssetResponse 映射为前端 MediaAsset 类型
@@ -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 = () => {
</button>
<button
className="ep-btn ep-btn-primary"
onClick={handleOpenGenerateModal}
onClick={handleGoToGenerate}
>
🎬 使用此模板生成
</button>
@@ -658,6 +696,22 @@ const EditingPlanner: React.FC = () => {
<span>🎬 {MODE_LABELS[currentMode]}</span>
<span className="ep-status-sep">|</span>
<span>📐 模板片段: {currentTemplate?.segments.length || 0}</span>
<span className="ep-status-sep">|</span>
<button
className="ep-status-link"
onClick={() => {
if (loadedTemplateId) {
const params = new URLSearchParams();
params.set("edit_plan_id", loadedTemplateId);
params.set("tab", "history");
navigate(`/app/generate?${params.toString()}`);
} else {
navigate("/app/generate?tab=history");
}
}}
>
📋 生成历史
</button>
</div>
</div>
@@ -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<ReturnType<typeof setInterval>>(undefined);
const audioRef = useRef<HTMLAudioElement | null>(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<string, unknown>;
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 = () => {
<Text className="xx-generate-subtitle">
快速生成短视频,支持多种风格和素材组合
</Text>
{editPlanId && (
<Tag
color="blue"
style={{ marginLeft: 12, fontSize: 12, lineHeight: "20px" }}
>
🎬 来自剪辑计划
</Tag>
)}
</div>
<div className="xx-generate-layout">