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
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user