diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
index 4e3c55730..8e819f2a1 100644
--- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx
+++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
@@ -30,7 +30,6 @@ import {
getMediaAssets,
getEditPlanGenerations,
MATERIAL_TYPE_LABELS,
- PLAN_STATUS_LABELS,
aiRecommendClips,
generateCover,
} from "@/api/editPlans";
@@ -45,6 +44,7 @@ import ClipPropertiesPanel from "./components/ClipPropertiesPanel";
import SaveModal from "./components/SaveModal";
import GenerationProgressModal from "./components/GenerationProgressModal";
import type { GenPhase } from "./components/GenerationProgressModal";
+import GenerationHistoryModal from "./components/GenerationHistoryModal";
import "./EditingPlanner.css";
/* ──────────── 类型 ──────────── */
@@ -167,6 +167,10 @@ const EditingPlanner: React.FC = () => {
/* ──────────── 加载 ──────────── */
+ /**
+ * 并行加载模板列表、分类、素材库
+ * 首次挂载时调用,三个接口无依赖关系,用 Promise.all 并发
+ */
const loadTemplates = useCallback(async () => {
setLoadingTemplates(true);
try {
@@ -189,7 +193,11 @@ const EditingPlanner: React.FC = () => {
loadTemplates();
}, [loadTemplates]);
- /* 加载模板详情 */
+ /**
+ * 加载模板详情并初始化片段列表
+ * 将后端 segments 映射为前端 ClipData,取 duration_min/max 均值作为默认时长
+ * 同时还原标题/字幕/BGM 配置
+ */
useEffect(() => {
if (!loadedTemplateId) return;
getEditingTemplate(loadedTemplateId)
@@ -316,7 +324,11 @@ const EditingPlanner: React.FC = () => {
setClips((prev) => [...prev, newClip]);
};
- /* AI 推荐片段 */
+ /**
+ * AI 推荐片段方案
+ * 将当前已绑定素材的片段 ID 列表 + 编辑模式 + 目标时长发送给后端
+ * 返回的推荐结果直接替换当前片段列表(不保留原手动编排)
+ */
const handleAiRecommend = async () => {
if (!loadedTemplateId) {
msg.warning("请先加载模板");
@@ -448,13 +460,16 @@ const EditingPlanner: React.FC = () => {
}
};
- /* 跳转到一键生成页面,携带当前编辑器配置 */
+ /**
+ * 跳转到一键生成页面
+ * 通过 URL SearchParams 传递 edit_plan_id 和完整 planConfig(JSON 序列化)
+ * 一键生成页面从 params 解析配置,无需重复请求接口
+ */
const handleGoToGenerate = () => {
if (!loadedTemplateId) {
msg.warning("请先加载一个模板");
return;
}
- // 构建配置对象,传递给一键生成页面
const planConfig = {
title_config: {
ai_auto_select: titleSettings.aiAutoSelect,
@@ -496,6 +511,12 @@ const EditingPlanner: React.FC = () => {
navigate(`/app/generate?${params.toString()}`);
};
+ /**
+ * 创建生成任务(两步)
+ * 1. generateFromTemplate — 通知后端基于模板生成视频
+ * 2. createGenerationTask — 创建任务记录,返回精简响应
+ * 再用 getTask 查询完整 TaskItem 供轮询使用
+ */
const handleGenerate = async () => {
if (!loadedTemplateId) return;
setGenSubmitting(true);
@@ -503,7 +524,7 @@ const EditingPlanner: React.FC = () => {
await generateFromTemplate(loadedTemplateId, {
voiceover_duration: voiceoverDuration || totalDuration,
});
- const task = await createGenerationTask({
+ const res = await createGenerationTask({
template_id: loadedTemplateId,
asset_ids: clips
.map((c) => c.media_asset_id)
@@ -511,7 +532,9 @@ const EditingPlanner: React.FC = () => {
title_ids: [],
voice_ids: [],
});
- setGenTask(task as unknown as TaskItem);
+ /* 创建接口返回的是精简响应,需查询完整 TaskItem 用于轮询 */
+ const task = await getTask(res.id);
+ setGenTask(task);
setGenPhase("progress");
msg.info("生成任务已创建");
} catch {
@@ -521,7 +544,11 @@ const EditingPlanner: React.FC = () => {
}
};
- /* 轮询任务状态 */
+ /**
+ * 轮询生成任务状态(每 3 秒)
+ * 仅在 genPhase === "progress" 且有任务 ID 时启动
+ * 任务完成/失败时自动停止轮询
+ */
useEffect(() => {
if (genPhase !== "progress" || !genTask?.id) return;
const timer = setInterval(async () => {
@@ -762,98 +789,13 @@ const EditingPlanner: React.FC = () => {
onClose={handleCancelGen}
/>
- {/* ═══ 生成历史 Modal ═══ */}
- {genHistoryOpen && (
-
setGenHistoryOpen(false)}>
-
e.stopPropagation()}
- >
-
-
📋 生成历史
-
-
-
- {genHistoryLoading ? (
-
- 加载中...
-
- ) : genHistory.length === 0 ? (
-
- 暂无生成记录
-
- ) : (
-
-
-
- | 任务ID |
- 状态 |
- 创建时间 |
- 更新时间 |
-
-
-
- {genHistory.map((gen) => (
-
- |
- {gen.generation_task_id.slice(0, 8)}...
- |
-
-
- {PLAN_STATUS_LABELS[gen.status] || gen.status}
-
- |
-
- {gen.created_at ? new Date(gen.created_at).toLocaleString("zh-CN") : "—"}
- |
-
- {gen.updated_at ? new Date(gen.updated_at).toLocaleString("zh-CN") : "—"}
- |
-
- ))}
-
-
- )}
-
-
-
-
-
-
- )}
+ {/* ═══ 生成历史弹窗 ═══ */}
+ setGenHistoryOpen(false)}
+ />
);
};
diff --git a/apps/web/src/pages/editing-planner/components/GenerationHistoryModal.tsx b/apps/web/src/pages/editing-planner/components/GenerationHistoryModal.tsx
new file mode 100644
index 000000000..447803986
--- /dev/null
+++ b/apps/web/src/pages/editing-planner/components/GenerationHistoryModal.tsx
@@ -0,0 +1,108 @@
+/**
+ * 生成历史弹窗 — 展示当前剪辑计划的生成任务记录
+ * 从 EditingPlanner 拆分,避免主文件过大
+ */
+import React from "react";
+import type { EditPlanGeneration } from "@/api/editPlans";
+import { PLAN_STATUS_LABELS } from "@/api/editPlans";
+
+interface GenerationHistoryModalProps {
+ open: boolean;
+ loading: boolean;
+ history: EditPlanGeneration[];
+ onClose: () => void;
+}
+
+/** 状态标签颜色映射 */
+const STATUS_STYLES: Record = {
+ completed: { bg: "#f6ffed", color: "#52c41a" },
+ failed: { bg: "#fff2f0", color: "#ff4d4f" },
+ rendering: { bg: "#e6f7ff", color: "#1890ff" },
+};
+const DEFAULT_STATUS_STYLE = { bg: "#fafafa", color: "#666" };
+
+const GenerationHistoryModal: React.FC = ({
+ open,
+ loading,
+ history,
+ onClose,
+}) => {
+ if (!open) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+
📋 生成历史
+
+
+
+ {loading ? (
+
+ 加载中...
+
+ ) : history.length === 0 ? (
+
+ 暂无生成记录
+
+ ) : (
+
+
+
+ | 任务ID |
+ 状态 |
+ 创建时间 |
+ 更新时间 |
+
+
+
+ {history.map((gen) => {
+ const style = STATUS_STYLES[gen.status] || DEFAULT_STATUS_STYLE;
+ return (
+
+ |
+ {gen.generation_task_id.slice(0, 8)}...
+ |
+
+
+ {PLAN_STATUS_LABELS[gen.status] || gen.status}
+
+ |
+
+ {gen.created_at ? new Date(gen.created_at).toLocaleString("zh-CN") : "—"}
+ |
+
+ {gen.updated_at ? new Date(gen.updated_at).toLocaleString("zh-CN") : "—"}
+ |
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default GenerationHistoryModal;