feat: 打通剪辑计划生成链路 — EditingPlanner 生成按钮对接真实 API
CI/CD Pipeline / Frontend Lint (push) Failing after 50s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m7s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m54s
CI/CD Pipeline / Unit Tests (push) Successful in 1m57s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 2m25s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m10s
CI/CD Pipeline / Integration Tests (push) Successful in 1m9s
CI/CD Pipeline / Frontend Lint (push) Failing after 50s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m7s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m54s
CI/CD Pipeline / Unit Tests (push) Successful in 1m57s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 2m25s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m10s
CI/CD Pipeline / Integration Tests (push) Successful in 1m9s
- 生成按钮调用 generateEditPlan(planId) 而非跳转一键生成页面 - 进度弹窗 + 轮询 getGenerationStatus(每2秒)+ 视频结果展示 - 列表页编辑按钮正确加载计划数据到编辑器(planId URL参数) - 生成历史弹窗正常工作 - 列表页状态自动同步(已有5秒 refetch) - 修复所有 TypeScript 编译错误(tsc 零错误)
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
* 四行布局:顶栏(42px) → 模式栏(48px) → 三栏主体 → 底栏(40px)
|
||||
*/
|
||||
import React, { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import { message } from "antd";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { message, Modal, Progress, Button } from "antd";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type {
|
||||
EditingTemplate,
|
||||
@@ -20,11 +20,23 @@ import {
|
||||
getTemplateCategories,
|
||||
MODE_LABELS,
|
||||
} from "@/api/editingPlanner";
|
||||
import type { EditPlanGeneration, MediaAsset } from "@/api/editPlans";
|
||||
import type {
|
||||
EditPlanGeneration,
|
||||
EditPlanConfig,
|
||||
GeneratedVideo,
|
||||
MediaAsset,
|
||||
TransitionEffect,
|
||||
} from "@/api/editPlans";
|
||||
import {
|
||||
getMediaAssets,
|
||||
getEditPlanGenerations,
|
||||
generateCover,
|
||||
getEditPlan,
|
||||
createEditPlan,
|
||||
updateEditPlan,
|
||||
generateEditPlan,
|
||||
getGenerationStatus,
|
||||
getGenerationTaskResults,
|
||||
} from "@/api/editPlans";
|
||||
import { useUndoRedo } from "./hooks/useUndoRedo";
|
||||
import type {
|
||||
@@ -33,6 +45,7 @@ import type {
|
||||
TransitionConfig,
|
||||
SpeedConfig,
|
||||
TtsConfig,
|
||||
TtsMode,
|
||||
TrimConfig,
|
||||
WatermarkConfig,
|
||||
IntroOutroConfig,
|
||||
@@ -107,8 +120,8 @@ const FILTER_CATEGORIES = ["全部", "种草", "知识", "日常", "推荐"];
|
||||
|
||||
const EditingPlanner: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const urlTemplateId = searchParams.get("templateId") || "";
|
||||
const urlPlanId = searchParams.get("planId") || "";
|
||||
|
||||
/* ── 模板列表 ── */
|
||||
const [templates, setTemplates] = useState<EditingTemplate[]>([]);
|
||||
@@ -244,6 +257,21 @@ const EditingPlanner: React.FC = () => {
|
||||
const [genHistory, setGenHistory] = useState<EditPlanGeneration[]>([]);
|
||||
const [genHistoryLoading, setGenHistoryLoading] = useState(false);
|
||||
|
||||
/* ── 剪辑计划(从列表页编辑进入时) ── */
|
||||
const [loadedPlanId, setLoadedPlanId] = useState<string | null>(
|
||||
urlPlanId || null,
|
||||
);
|
||||
|
||||
/* ── 生成进度 ── */
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [genProgress, setGenProgress] = useState(0);
|
||||
const [genTotalClips, setGenTotalClips] = useState(0);
|
||||
const [genDoneClips, setGenDoneClips] = useState(0);
|
||||
const [generated, setGenerated] = useState(false);
|
||||
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([]);
|
||||
const [genError, setGenError] = useState<string | null>(null);
|
||||
const genTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
/* ── 播放 ── */
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
@@ -374,6 +402,85 @@ const EditingPlanner: React.FC = () => {
|
||||
.catch(() => message.error("加载模板详情失败"));
|
||||
}, [loadedTemplateId, resetClips]);
|
||||
|
||||
/**
|
||||
* 加载已有剪辑计划数据到编辑器
|
||||
* 从列表页"编辑"按钮进入时,URL 带 planId,需要还原计划配置
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!loadedPlanId) return;
|
||||
getEditPlan(loadedPlanId)
|
||||
.then((plan) => {
|
||||
// 设置关联的模板(触发模板加载 effect)
|
||||
setLoadedTemplateId(plan.template_id);
|
||||
|
||||
// 还原基本信息
|
||||
setDraftName(plan.name);
|
||||
|
||||
// 还原 config 中的编辑器状态
|
||||
const cfg = plan.config;
|
||||
if (cfg.title_config) {
|
||||
setTitleSettings((prev) => ({
|
||||
...prev,
|
||||
aiAutoSelect: cfg.title_config!.ai_auto_select,
|
||||
title: cfg.title_config!.content,
|
||||
position: cfg.title_config!.position,
|
||||
font: cfg.title_config!.font_preset,
|
||||
size: cfg.title_config!.font_size,
|
||||
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 || "",
|
||||
}));
|
||||
}
|
||||
|
||||
// 还原片段 — 延迟设置,等模板加载 effect 先执行 resetClips
|
||||
if (cfg.segments && cfg.segments.length > 0) {
|
||||
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]);
|
||||
|
||||
/* ──────────── 计算 ──────────── */
|
||||
|
||||
const currentTemplate = templates.find((t) => t.id === loadedTemplateId);
|
||||
@@ -671,6 +778,65 @@ const EditingPlanner: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 构建剪辑计划 config(编辑器状态 → API config) */
|
||||
const buildPlanConfig = (): EditPlanConfig => ({
|
||||
title_config: {
|
||||
ai_auto_select: titleSettings.aiAutoSelect,
|
||||
content: titleSettings.title,
|
||||
position: titleSettings.position,
|
||||
font_preset: titleSettings.font,
|
||||
font_color: titleSettings.color,
|
||||
font_size: titleSettings.size,
|
||||
},
|
||||
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: { ...coverSettings },
|
||||
});
|
||||
|
||||
/* 保存 — 无论是否已加载模板,都打开保存弹窗;未加载时创建新模板 */
|
||||
const handleOpenSaveModal = () => {
|
||||
setSaveModalOpen(true);
|
||||
@@ -763,94 +929,139 @@ const EditingPlanner: React.FC = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 跳转到一键生成页面
|
||||
* 通过 URL SearchParams 传递 edit_plan_id 和完整 planConfig(JSON 序列化)
|
||||
* 一键生成页面从 params 解析配置,无需重复请求接口
|
||||
* 剪辑计划生成
|
||||
* 1. 有 planId → 更新计划配置 + 触发生成
|
||||
* 2. 无 planId(从模板库直接进入)→ 先创建计划 + 触发生成
|
||||
* 3. 触发生成后轮询状态,完成后获取视频结果
|
||||
*/
|
||||
const handleGoToGenerate = () => {
|
||||
const planConfig = {
|
||||
title_config: {
|
||||
ai_auto_select: titleSettings.aiAutoSelect,
|
||||
content: titleSettings.title,
|
||||
position: titleSettings.position,
|
||||
font_preset: titleSettings.font,
|
||||
font_color: titleSettings.color,
|
||||
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,
|
||||
color: subtitleSettings.fontColor,
|
||||
size: subtitleSettings.fontSize,
|
||||
animation: subtitleSettings.animation,
|
||||
},
|
||||
bgm_config: {
|
||||
enabled: bgmSettings.enabled,
|
||||
music_id: bgmSettings.music_id,
|
||||
},
|
||||
mode: currentMode,
|
||||
total_duration: totalDuration,
|
||||
segments: clips.map((c, i) => ({
|
||||
order: i,
|
||||
material_type: c.type === "voice" ? "voiceover" : "video",
|
||||
duration: c.duration,
|
||||
template_segment_id: c.template_segment_id,
|
||||
script_text: c.script_text,
|
||||
voice_asset_id: c.voice_asset_id,
|
||||
voice_file_url: c.voice_file_url,
|
||||
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: { ...coverSettings },
|
||||
};
|
||||
const params = new URLSearchParams();
|
||||
if (loadedTemplateId) {
|
||||
params.set("edit_plan_id", loadedTemplateId);
|
||||
const handleGoToGenerate = async () => {
|
||||
if (!loadedTemplateId) {
|
||||
message.warning("请先选择一个模板");
|
||||
return;
|
||||
}
|
||||
if (clips.length === 0) {
|
||||
message.warning("请先添加片段");
|
||||
return;
|
||||
}
|
||||
|
||||
setGenerating(true);
|
||||
setGenerated(false);
|
||||
setGeneratedVideos([]);
|
||||
setGenError(null);
|
||||
setGenProgress(0);
|
||||
|
||||
try {
|
||||
const config = buildPlanConfig();
|
||||
let planId = loadedPlanId;
|
||||
|
||||
if (planId) {
|
||||
// 已有计划 → 更新配置
|
||||
await updateEditPlan(planId, {
|
||||
config,
|
||||
total_duration: totalDuration,
|
||||
status: "editing",
|
||||
});
|
||||
} else {
|
||||
// 无计划 → 创建新计划
|
||||
const plan = await createEditPlan({
|
||||
template_id: loadedTemplateId,
|
||||
name: draftName || "未命名计划",
|
||||
config,
|
||||
total_duration: totalDuration,
|
||||
});
|
||||
planId = plan.id;
|
||||
setLoadedPlanId(planId);
|
||||
// 更新 URL 参数(不刷新页面)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("planId", planId);
|
||||
window.history.replaceState(null, "", `?${params.toString()}`);
|
||||
}
|
||||
|
||||
// 触发生成
|
||||
const genRes = await generateEditPlan(planId);
|
||||
setGenTotalClips(genRes.clip_count);
|
||||
message.info("已提交生成,等待处理...");
|
||||
|
||||
// 开始轮询
|
||||
startPolling(planId);
|
||||
} catch (err) {
|
||||
console.error("[生成失败]", err);
|
||||
setGenError("生成提交失败,请重试");
|
||||
setGenerating(false);
|
||||
}
|
||||
params.set("plan_config", JSON.stringify(planConfig));
|
||||
navigate(`/app/generate?${params.toString()}`);
|
||||
};
|
||||
|
||||
/** 轮询生成状态,每 2 秒一次 */
|
||||
const startPolling = (planId: string) => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
const status = await getGenerationStatus(planId);
|
||||
|
||||
// 计算进度
|
||||
const total = status.clips.length || genTotalClips;
|
||||
const done = status.clips.filter(
|
||||
(c) => c.status === "completed" || c.status === "failed",
|
||||
).length;
|
||||
setGenDoneClips(done);
|
||||
setGenTotalClips(total);
|
||||
setGenProgress(total > 0 ? Math.round((done / total) * 100) : 5);
|
||||
|
||||
if (status.plan_status === "completed") {
|
||||
setGenProgress(100);
|
||||
setGenerating(false);
|
||||
setGenerated(true);
|
||||
|
||||
// 获取视频结果
|
||||
if (status.generation_task_id) {
|
||||
try {
|
||||
const videos = await getGenerationTaskResults(
|
||||
status.generation_task_id,
|
||||
);
|
||||
setGeneratedVideos(videos);
|
||||
} catch (e) {
|
||||
console.error("[获取视频结果失败]", e);
|
||||
}
|
||||
}
|
||||
message.success("视频生成完成!");
|
||||
return; // 停止轮询
|
||||
}
|
||||
|
||||
if (status.plan_status === "failed") {
|
||||
setGenerating(false);
|
||||
setGenError("生成失败,请重试");
|
||||
return; // 停止轮询
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
genTimerRef.current = setTimeout(poll, 2000);
|
||||
} catch (err) {
|
||||
console.error("[轮询状态失败]", err);
|
||||
genTimerRef.current = setTimeout(poll, 5000); // 出错后 5 秒重试
|
||||
}
|
||||
};
|
||||
|
||||
// 首次延迟 2 秒后开始
|
||||
genTimerRef.current = setTimeout(poll, 2000);
|
||||
};
|
||||
|
||||
/** 清理轮询定时器 */
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (genTimerRef.current) clearTimeout(genTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/* 查看生成历史 */
|
||||
const handleViewGenHistory = async () => {
|
||||
if (!loadedTemplateId) {
|
||||
message.warning("请先加载一个模板");
|
||||
const targetId = loadedPlanId || loadedTemplateId;
|
||||
if (!targetId) {
|
||||
message.warning("请先加载一个模板或计划");
|
||||
return;
|
||||
}
|
||||
setGenHistoryOpen(true);
|
||||
setGenHistoryLoading(true);
|
||||
try {
|
||||
const items = await getEditPlanGenerations(loadedTemplateId);
|
||||
const items = await getEditPlanGenerations(targetId);
|
||||
setGenHistory(items);
|
||||
} catch {
|
||||
message.error("加载生成历史失败");
|
||||
@@ -899,8 +1110,9 @@ const EditingPlanner: React.FC = () => {
|
||||
<button
|
||||
className="ep-btn ep-btn-primary"
|
||||
onClick={handleGoToGenerate}
|
||||
disabled={generating}
|
||||
>
|
||||
🎬 使用此模板生成
|
||||
{loadedPlanId ? "🎬 生成视频" : "🎬 创建计划并生成"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1065,6 +1277,112 @@ const EditingPlanner: React.FC = () => {
|
||||
onClose={() => setGenHistoryOpen(false)}
|
||||
/>
|
||||
|
||||
{/* ═══ 生成进度弹窗 ═══ */}
|
||||
<Modal
|
||||
title={generated ? "生成完成" : "正在生成视频"}
|
||||
open={generating || generated}
|
||||
footer={
|
||||
generated
|
||||
? [
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
setGenerated(false);
|
||||
setGenerating(false);
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>,
|
||||
generatedVideos.length > 0 && (
|
||||
<Button
|
||||
key="download"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
const v = generatedVideos[0];
|
||||
const url = v.download_url || v.file_url;
|
||||
if (url) {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = v.name || "video.mp4";
|
||||
a.target = "_blank";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
}}
|
||||
>
|
||||
下载视频
|
||||
</Button>
|
||||
),
|
||||
]
|
||||
: null
|
||||
}
|
||||
closable={!generating}
|
||||
maskClosable={false}
|
||||
width={520}
|
||||
>
|
||||
{generating && (
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<Progress percent={genProgress} status="active" />
|
||||
<p style={{ marginTop: 8, color: "var(--text-secondary)" }}>
|
||||
已处理 {genDoneClips}/{genTotalClips} 个片段
|
||||
</p>
|
||||
<p style={{ color: "var(--text-secondary)", fontSize: 12 }}>
|
||||
请耐心等待,生成过程中请勿关闭页面
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{generated && generatedVideos.length > 0 && (
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<video
|
||||
src={
|
||||
generatedVideos[0].file_url ||
|
||||
generatedVideos[0].download_url
|
||||
}
|
||||
controls
|
||||
preload="metadata"
|
||||
style={{ width: "100%", maxHeight: 320, borderRadius: 8 }}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
marginTop: 8,
|
||||
textAlign: "center",
|
||||
color: "var(--text-secondary)",
|
||||
}}
|
||||
>
|
||||
{generatedVideos[0].name}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{generated && !generatedVideos.length && (
|
||||
<div style={{ padding: "24px 0", textAlign: "center" }}>
|
||||
<p>生成完成,但暂未获取到视频结果</p>
|
||||
<p style={{ color: "var(--text-secondary)", fontSize: 12 }}>
|
||||
请稍后在剪辑计划列表中查看
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{genError && (
|
||||
<div
|
||||
style={{
|
||||
padding: "16px 0",
|
||||
textAlign: "center",
|
||||
color: "#ff4d4f",
|
||||
}}
|
||||
>
|
||||
<p>{genError}</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setGenError(null);
|
||||
setGenerating(false);
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* ═══ BGM 选择器 Drawer ═══ */}
|
||||
<BgmSelector
|
||||
open={bgmDrawerOpen}
|
||||
|
||||
Reference in New Issue
Block a user