From 11c00811ebd37934d49243db9e0a1df13a1681a6 Mon Sep 17 00:00:00 2001
From: CI Bot
Date: Thu, 16 Jul 2026 09:52:58 +0800
Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E4=BB=BB?=
=?UTF-8?q?=E5=8A=A1=E5=8F=96=E6=B6=88=E6=8E=A5=E5=8F=A3=20-=20POST=20/tas?=
=?UTF-8?q?ks/{task=5Fid}/cancel?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增生成任务取消接口:
- 支持 pending / running 状态的任务取消
- 取消后状态变为 cancelled,记录取消日志
- 终态(completed/failed/cancelled)不可取消,返回 409
- 权限校验:只能取消自己创建的任务
- 复用领域模型已有的 mark_cancelled 方法
---
apps/api/app/api/routes/generation_tasks.py | 50 +++++++++++++++++++++
1 file changed, 50 insertions(+)
mode change 100644 => 100755 apps/api/app/api/routes/generation_tasks.py
diff --git a/apps/api/app/api/routes/generation_tasks.py b/apps/api/app/api/routes/generation_tasks.py
old mode 100644
new mode 100755
index 7f7b3fa60..4f625ec53
--- a/apps/api/app/api/routes/generation_tasks.py
+++ b/apps/api/app/api/routes/generation_tasks.py
@@ -427,3 +427,53 @@ def retry_generation_task(
detail="系统繁忙,请稍后再试",
) from None
return _to_generation_task_response(retried)
+
+
+@router.post("/tasks/{task_id}/cancel", response_model=GenerationTaskResponse)
+def cancel_generation_task(
+ task_id: str,
+ authenticated_user: AuthenticatedUser = Depends(get_current_user),
+ generation_task_repository: Any = Depends(get_generation_task_repository),
+) -> GenerationTaskResponse:
+ """取消生成任务。
+
+ 仅 pending / running 状态的任务可取消;取消后状态变为 cancelled。
+ 对于已在运行的 Celery 任务,标记为 cancelled 后,worker 在下次检查点会中止执行。
+ """
+ task = generation_task_repository.get(task_id)
+ if task is None:
+ raise HTTPException(status_code=404, detail="Generation task not found")
+
+ # 权限校验
+ if task.created_by_user_id and task.created_by_user_id != authenticated_user.user.id:
+ raise HTTPException(status_code=403, detail="Access denied to this task")
+
+ status_val = task.status.value if hasattr(task.status, "value") else str(task.status)
+
+ # 终态不可取消
+ if status_val in ("completed", "failed", "cancelled"):
+ raise HTTPException(
+ status_code=409,
+ detail=f"Cannot cancel task in {status_val} status",
+ )
+
+ # 执行取消
+ try:
+ task.mark_cancelled()
+ task.append_log(
+ stage="cancelled",
+ message="用户主动取消任务",
+ level="INFO",
+ cancelled_by=authenticated_user.user.id,
+ )
+ generation_task_repository.update(task)
+ logger.info(
+ "生成任务已取消: task_id=%s user_id=%s previous_status=%s",
+ task_id,
+ authenticated_user.user.id,
+ status_val,
+ )
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e)) from e
+
+ return _to_generation_task_response(task)
--
2.54.0
From a08eea16309f705fd8be343dc307d946fd3b14fb Mon Sep 17 00:00:00 2001
From: CI Bot
Date: Thu, 16 Jul 2026 10:06:02 +0800
Subject: [PATCH 02/17] =?UTF-8?q?feat:=20worker=E6=B8=B2=E6=9F=93=E4=BB=BB?=
=?UTF-8?q?=E5=8A=A1=E5=8F=96=E6=B6=88=E6=A3=80=E6=9F=A5=E7=82=B9=20-=20?=
=?UTF-8?q?=E7=B4=A0=E6=9D=90=E4=B8=8B=E8=BD=BD=E5=90=8E=E6=A3=80=E6=B5=8B?=
=?UTF-8?q?cancelled=E7=8A=B6=E6=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
在渲染主流程中增加取消检查点:
- 素材下载完成后、启动FFmpeg前,检查generation_task状态
- 如果已被取消(cancelled),立即中止,不启动渲染
- 计划状态从 rendering 切回 editing,用户可继续编辑
- 与 API 层的取消接口配套
不修改 FFmpeg 运行时取消(后续迭代),覆盖排队/下载期间取消的场景
---
.../worker_app/tasks/edit_plan_generation.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/apps/worker/worker_app/tasks/edit_plan_generation.py b/apps/worker/worker_app/tasks/edit_plan_generation.py
index 3eb54cc93..19ce9d826 100755
--- a/apps/worker/worker_app/tasks/edit_plan_generation.py
+++ b/apps/worker/worker_app/tasks/edit_plan_generation.py
@@ -491,6 +491,23 @@ def render_edit_plan(self, plan_id: str) -> dict:
gen_task_repo.update(gen_task)
# 4. 根据引擎选择渲染方式
+ # 取消检查:素材下载完后,确认任务没有被用户取消
+ if generation_task_id:
+ current_task = gen_task_repo.get(generation_task_id)
+ if current_task:
+ task_status = current_task.status.value if hasattr(current_task.status, "value") else str(current_task.status)
+ if task_status == "cancelled":
+ logger.info("任务已被取消,中止渲染: plan_id=%s task_id=%s", plan_id, generation_task_id)
+ # 计划回到 editing 状态,用户可以继续编辑
+ from packages.domain.edit_plan import EditPlanStatus
+ if plan.status.value == "rendering":
+ try:
+ plan.resume_editing()
+ plan_repo.update(plan)
+ except ValueError:
+ pass
+ return {"status": "cancelled", "plan_id": plan_id, "message": "任务已取消"}
+
if engine == "unified":
result = _render_with_unified(
plan=plan,
--
2.54.0
From 7060fa31248140490519f4efb0c8c15e23ff51c4 Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Thu, 16 Jul 2026 10:18:22 +0800
Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E8=BF=9B?=
=?UTF-8?q?=E5=BA=A6=E5=BC=B9=E7=AA=97=E5=A2=9E=E5=8A=A0=E5=8F=96=E6=B6=88?=
=?UTF-8?q?=E6=8C=89=E9=92=AE=20-=20=E5=AF=B9=E6=8E=A5=20cancelGenerationT?=
=?UTF-8?q?ask=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- EditPlanStatus 新增 cancelled 状态及标签
- 新增 cancelGenerationTask API 函数
- 生成进度弹窗生成中状态增加"取消生成"按钮
- 取消前二次确认,防止误操作
- 轮询检测 cancelled 状态,取消后显示已取消提示
- 保存当前 generation_task_id 用于取消调用
---
apps/web/src/api/editPlans.ts | 15 +++-
.../pages/editing-planner/EditingPlanner.tsx | 78 ++++++++++++++++++-
2 files changed, 89 insertions(+), 4 deletions(-)
mode change 100644 => 100755 apps/web/src/api/editPlans.ts
diff --git a/apps/web/src/api/editPlans.ts b/apps/web/src/api/editPlans.ts
old mode 100644
new mode 100755
index 93bc13477..7465edba7
--- a/apps/web/src/api/editPlans.ts
+++ b/apps/web/src/api/editPlans.ts
@@ -20,7 +20,12 @@ import type {
/** 剪辑计划状态枚举 */
export type EditPlanStatus =
- "draft" | "editing" | "rendering" | "completed" | "failed";
+ | "draft"
+ | "editing"
+ | "rendering"
+ | "completed"
+ | "failed"
+ | "cancelled";
/** 标题配置(对齐后端 title_config) */
export interface TitleConfig {
@@ -453,6 +458,13 @@ export async function getGenerationTaskResults(
return response.data.items || response.data || [];
}
+/** 取消生成任务 */
+export async function cancelGenerationTask(
+ taskId: string,
+): Promise {
+ await apiClient.post(`/generation/tasks/${taskId}/cancel`);
+}
+
/**
* 获取素材库列表 — 调用 GET /api/v1/assets?library_id=xxx
* 将后端 AssetResponse 映射为前端 MediaAsset 类型
@@ -553,6 +565,7 @@ export const PLAN_STATUS_LABELS: Record = {
rendering: "渲染中",
completed: "已完成",
failed: "失败",
+ cancelled: "已取消",
};
/** 质量分筛选选项 */
diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
index 783e502a4..b4c85dc47 100755
--- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx
+++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
@@ -37,6 +37,7 @@ import {
generateEditPlan,
getGenerationStatus,
getGenerationTaskResults,
+ cancelGenerationTask,
} from "@/api/editPlans";
import { useUndoRedo } from "./hooks/useUndoRedo";
import type {
@@ -270,6 +271,8 @@ const EditingPlanner: React.FC = () => {
const [generated, setGenerated] = useState(false);
const [generatedVideos, setGeneratedVideos] = useState([]);
const [genError, setGenError] = useState(null);
+ const [genCancelled, setGenCancelled] = useState(false);
+ const [currentGenTaskId, setCurrentGenTaskId] = useState(null);
const genTimerRef = useRef | null>(null);
/* ── 播放 ── */
@@ -949,6 +952,8 @@ const EditingPlanner: React.FC = () => {
setGeneratedVideos([]);
setGenError(null);
setGenProgress(0);
+ setGenCancelled(false);
+ setCurrentGenTaskId(null);
try {
const config = buildPlanConfig();
@@ -989,6 +994,7 @@ const EditingPlanner: React.FC = () => {
// 触发生成
const genRes = await generateEditPlan(planId);
setGenTotalClips(genRes.clip_count);
+ setCurrentGenTaskId(genRes.generation_task_id);
message.info("已提交生成,等待处理...");
// 开始轮询
@@ -1041,6 +1047,13 @@ const EditingPlanner: React.FC = () => {
return; // 停止轮询
}
+ if (status.plan_status === "cancelled") {
+ setGenerating(false);
+ setGenCancelled(true);
+ message.info("生成已取消");
+ return; // 停止轮询
+ }
+
// 继续轮询
genTimerRef.current = setTimeout(poll, 2000);
} catch (err) {
@@ -1060,6 +1073,30 @@ const EditingPlanner: React.FC = () => {
};
}, []);
+ /* 取消生成 */
+ const handleCancelGeneration = async () => {
+ if (!currentGenTaskId) return;
+ Modal.confirm({
+ title: "确认取消生成?",
+ content: "取消后已处理的片段不会保留,需要重新生成。",
+ okText: "取消生成",
+ cancelText: "继续等待",
+ okButtonProps: { danger: true },
+ onOk: async () => {
+ try {
+ await cancelGenerationTask(currentGenTaskId);
+ message.info("取消请求已提交,正在停止...");
+ // 停止本地轮询,等待服务端状态通过轮询或下一次检查返回 cancelled
+ // 这里不立即停止,让轮询检测到 cancelled 状态后再收尾,以确保服务端确实处理了
+ } catch (err: any) {
+ console.error("[取消生成失败]", err);
+ const msg = err?.response?.data?.detail || "取消失败,请稍后重试";
+ message.error(msg);
+ }
+ },
+ });
+ };
+
/* 查看生成历史 */
const handleViewGenHistory = async () => {
const targetId = loadedPlanId || loadedTemplateId;
@@ -1288,8 +1325,16 @@ const EditingPlanner: React.FC = () => {
{/* ═══ 生成进度弹窗 ═══ */}
{
),
]
- : null
+ : genCancelled
+ ? [
+ ,
+ ]
+ : generating
+ ? [
+ ,
+ ]
+ : null
}
closable={!generating}
maskClosable={false}
@@ -1341,6 +1405,14 @@ const EditingPlanner: React.FC = () => {
)}
+ {genCancelled && (
+
+
生成已取消
+
+ 你可以继续编辑后重新生成
+
+
+ )}
{generated && generatedVideos.length > 0 && (
{/* 右栏 260px:设置面板 */}
-
- setTitleSettings((prev) => ({ ...prev, ...partial }))
- }
- onSubtitleSettingsChange={(partial) =>
- setSubtitleSettings(
- (prev) => ({ ...prev, ...partial }) as SubtitleStyleConfig,
- )
- }
- onBgmSettingsChange={(partial) =>
- setBgmSettings((prev) => ({ ...prev, ...partial }))
- }
- onClipUpdate={handleClipUpdate}
- onOpenBgmDrawer={() => setBgmDrawerOpen(true)}
- onOpenSubtitleDrawer={() => setSubtitleDrawerOpen(true)}
- voiceMaterials={voiceMaterials}
- voiceMaterialsLoading={voiceMaterialsQuery.isLoading}
- onRefreshVoiceMaterials={() => voiceMaterialsQuery.refetch()}
- onClipVoiceSelect={handleClipVoiceSelect}
- onOpenTransitionDrawer={handleOpenTransitionDrawer}
- onOpenSpeedDrawer={handleOpenSpeedDrawer}
- onOpenTtsDrawer={handleOpenTtsDrawer}
- onOpenWatermarkDrawer={() => setWatermarkDrawerOpen(true)}
- onOpenIntroOutroDrawer={() => setIntroOutroDrawerOpen(true)}
- onOpenPipDrawer={() => setPipDrawerOpen(true)}
- onOpenFilterDrawer={() => setFilterDrawerOpen(true)}
- onOpenGreenScreenDrawer={() => setChromaKeyDrawerOpen(true)}
- onOpenStickerDrawer={() => setStickerDrawerOpen(true)}
- onOpenCoverDrawer={() => setCoverDrawerOpen(true)}
- />
+
+ {/* Tab 切换 */}
+
+
+
+
+
+ {/* 属性 Tab */}
+ {rightTab === "properties" && (
+
+
+ setTitleSettings((prev) => ({ ...prev, ...partial }))
+ }
+ onSubtitleSettingsChange={(partial) =>
+ setSubtitleSettings(
+ (prev) => ({ ...prev, ...partial }) as SubtitleStyleConfig,
+ )
+ }
+ onBgmSettingsChange={(partial) =>
+ setBgmSettings((prev) => ({ ...prev, ...partial }))
+ }
+ onClipUpdate={handleClipUpdate}
+ onOpenBgmDrawer={() => setBgmDrawerOpen(true)}
+ onOpenSubtitleDrawer={() => setSubtitleDrawerOpen(true)}
+ voiceMaterials={voiceMaterials}
+ voiceMaterialsLoading={voiceMaterialsQuery.isLoading}
+ onRefreshVoiceMaterials={() => voiceMaterialsQuery.refetch()}
+ onClipVoiceSelect={handleClipVoiceSelect}
+ onOpenTransitionDrawer={handleOpenTransitionDrawer}
+ onOpenSpeedDrawer={handleOpenSpeedDrawer}
+ onOpenTtsDrawer={handleOpenTtsDrawer}
+ onOpenWatermarkDrawer={() => setWatermarkDrawerOpen(true)}
+ onOpenIntroOutroDrawer={() => setIntroOutroDrawerOpen(true)}
+ onOpenPipDrawer={() => setPipDrawerOpen(true)}
+ onOpenFilterDrawer={() => setFilterDrawerOpen(true)}
+ onOpenGreenScreenDrawer={() => setChromaKeyDrawerOpen(true)}
+ onOpenStickerDrawer={() => setStickerDrawerOpen(true)}
+ onOpenCoverDrawer={() => setCoverDrawerOpen(true)}
+ />
+
+ )}
+
+ {/* 片段 Tab */}
+ {rightTab === "clips" && (
+
+ {
+ const idx = clips.findIndex((c) => c.id === clipId);
+ if (idx > 0) handleClipReorder(clipId, idx - 1);
+ }}
+ onMoveDown={(clipId) => {
+ const idx = clips.findIndex((c) => c.id === clipId);
+ if (idx < clips.length - 1) handleClipReorder(clipId, idx + 1);
+ }}
+ onRemove={handleClipRemove}
+ onAdd={handleAddClip}
+ />
+
+ )}
+
{/* ═══ 第4行:底栏 40px ═══ */}
diff --git a/apps/web/src/pages/editing-planner/components/EditorClipList.tsx b/apps/web/src/pages/editing-planner/components/EditorClipList.tsx
new file mode 100755
index 000000000..ae9b5c0cb
--- /dev/null
+++ b/apps/web/src/pages/editing-planner/components/EditorClipList.tsx
@@ -0,0 +1,166 @@
+/**
+ * 编辑器右侧栏 — 片段列表 Tab
+ * 紧凑版片段管理:选中、上下移动、删除、添加
+ */
+import React from "react";
+import { Button, Tooltip, Empty } from "antd";
+import {
+ UpOutlined,
+ DownOutlined,
+ DeleteOutlined,
+ PlusOutlined,
+ ScissorOutlined,
+ SoundOutlined,
+ PictureOutlined,
+ VideoCameraOutlined,
+} from "@ant-design/icons";
+import type { ClipData, ClipType } from "../types";
+
+interface EditorClipListProps {
+ clips: ClipData[];
+ selectedClipId: string | null;
+ onSelect: (clipId: string) => void;
+ onMoveUp: (clipId: string) => void;
+ onMoveDown: (clipId: string) => void;
+ onRemove: (clipId: string) => void;
+ onAdd: () => void;
+ onSplit?: (clipId: string) => void;
+}
+
+const clipTypeIcon: Record = {
+ video: ,
+ image: ,
+ voice: ,
+ pip: ,
+};
+
+const clipTypeLabel: Record = {
+ video: "视频",
+ image: "图片",
+ voice: "配音",
+ pip: "画中画",
+};
+
+const formatDuration = (sec: number) => {
+ if (sec < 60) return `${sec.toFixed(1)}s`;
+ const m = Math.floor(sec / 60);
+ const s = (sec % 60).toFixed(0);
+ return `${m}m${s.padStart(2, "0")}s`;
+};
+
+const EditorClipList: React.FC = ({
+ clips,
+ selectedClipId,
+ onSelect,
+ onMoveUp,
+ onMoveDown,
+ onRemove,
+ onAdd,
+}) => {
+ if (clips.length === 0) {
+ return (
+
+
+ } block onClick={onAdd}>
+ 添加片段
+
+
+ );
+ }
+
+ return (
+
+ {/* 顶部工具栏 */}
+
+
+ 共 {clips.length} 个片段
+
+
+ }
+ onClick={onAdd}
+ >
+ 添加
+
+
+
+
+ {/* 片段列表 */}
+
+ {clips.map((clip, index) => (
+
onSelect(clip.id)}
+ >
+ {/* 序号 + 类型图标 */}
+
+ {index + 1}
+
+ {clipTypeIcon[clip.type] || }
+
+ {clipTypeLabel[clip.type] || "片段"}
+
+
+
+ {formatDuration(clip.duration)}
+
+
+
+ {/* 文案预览 */}
+ {clip.script_text && (
+
+ {clip.script_text.slice(0, 40)}
+ {clip.script_text.length > 40 ? "..." : ""}
+
+ )}
+
+ {/* 操作按钮 */}
+
e.stopPropagation()}>
+
+ }
+ disabled={index === 0}
+ onClick={() => onMoveUp(clip.id)}
+ className="ep-clip-item-btn"
+ />
+
+
+ }
+ disabled={index === clips.length - 1}
+ onClick={() => onMoveDown(clip.id)}
+ className="ep-clip-item-btn"
+ />
+
+
+ }
+ onClick={() => onRemove(clip.id)}
+ className="ep-clip-item-btn"
+ />
+
+
+
+ ))}
+
+
+ );
+};
+
+export default EditorClipList;
--
2.54.0
From bebe5a78c6290156476599b98b8fac23f1bbe2c6 Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Thu, 16 Jul 2026 12:55:29 +0800
Subject: [PATCH 09/17] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4EditorClipList?=
=?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84onSplit=20prop?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/web/src/pages/editing-planner/components/EditorClipList.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/apps/web/src/pages/editing-planner/components/EditorClipList.tsx b/apps/web/src/pages/editing-planner/components/EditorClipList.tsx
index ae9b5c0cb..1d5582f7c 100755
--- a/apps/web/src/pages/editing-planner/components/EditorClipList.tsx
+++ b/apps/web/src/pages/editing-planner/components/EditorClipList.tsx
@@ -24,7 +24,6 @@ interface EditorClipListProps {
onMoveDown: (clipId: string) => void;
onRemove: (clipId: string) => void;
onAdd: () => void;
- onSplit?: (clipId: string) => void;
}
const clipTypeIcon: Record = {
--
2.54.0
From ae20abaed59f5de721e0769bc36adc167ef5c750 Mon Sep 17 00:00:00 2001
From: XiaoXia Bot
Date: Thu, 16 Jul 2026 13:54:15 +0800
Subject: [PATCH 10/17] style: auto-format with black
---
apps/worker/worker_app/tasks/edit_plan_generation.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/apps/worker/worker_app/tasks/edit_plan_generation.py b/apps/worker/worker_app/tasks/edit_plan_generation.py
index 19ce9d826..b671df4b5 100755
--- a/apps/worker/worker_app/tasks/edit_plan_generation.py
+++ b/apps/worker/worker_app/tasks/edit_plan_generation.py
@@ -495,11 +495,16 @@ def render_edit_plan(self, plan_id: str) -> dict:
if generation_task_id:
current_task = gen_task_repo.get(generation_task_id)
if current_task:
- task_status = current_task.status.value if hasattr(current_task.status, "value") else str(current_task.status)
+ task_status = (
+ current_task.status.value
+ if hasattr(current_task.status, "value")
+ else str(current_task.status)
+ )
if task_status == "cancelled":
logger.info("任务已被取消,中止渲染: plan_id=%s task_id=%s", plan_id, generation_task_id)
# 计划回到 editing 状态,用户可以继续编辑
from packages.domain.edit_plan import EditPlanStatus
+
if plan.status.value == "rendering":
try:
plan.resume_editing()
--
2.54.0
From 5d0880c323ab0485d4af5ea11d65aa82d7db8088 Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Thu, 16 Jul 2026 21:26:19 +0800
Subject: [PATCH 11/17] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4PlanClipsManager?=
=?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84Tooltip=E5=AF=BC=E5=85=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/web/src/pages/edit-plans/PlanClipsManager.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/apps/web/src/pages/edit-plans/PlanClipsManager.tsx b/apps/web/src/pages/edit-plans/PlanClipsManager.tsx
index 5b3ab4ed4..9c92339d6 100755
--- a/apps/web/src/pages/edit-plans/PlanClipsManager.tsx
+++ b/apps/web/src/pages/edit-plans/PlanClipsManager.tsx
@@ -19,7 +19,6 @@ import {
Select,
Tag,
Drawer,
- Tooltip,
Empty,
Card,
} from "antd";
--
2.54.0
From 646217f78cd9f50131baccca9eb7c422ad4a09a1 Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Thu, 16 Jul 2026 21:48:25 +0800
Subject: [PATCH 12/17] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dlint=E9=94=99?=
=?UTF-8?q?=E8=AF=AF=20-=20=E7=A7=BB=E9=99=A4=E9=87=8D=E5=A4=8DEditPlanCli?=
=?UTF-8?q?p=E5=AE=9A=E4=B9=89/=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=B1=BB?=
=?UTF-8?q?=E5=9E=8B=E5=AF=BC=E5=85=A5/handleClipReorder=E4=BC=A0=E5=8F=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/web/src/api/editPlans.ts | 18 ------------------
.../src/pages/edit-plans/PlanClipsManager.tsx | 1 -
.../pages/editing-planner/EditingPlanner.tsx | 4 ++--
3 files changed, 2 insertions(+), 21 deletions(-)
diff --git a/apps/web/src/api/editPlans.ts b/apps/web/src/api/editPlans.ts
index 9c56b1f07..84d8ea9fc 100755
--- a/apps/web/src/api/editPlans.ts
+++ b/apps/web/src/api/editPlans.ts
@@ -281,24 +281,6 @@ export interface CoverResult {
* 前端 UI 类型(EditingPlanner 组件依赖,保留兼容)
* ============================================================ */
-/** 剪辑计划中的片段(UI 层类型) */
-export interface EditPlanClip {
- id: string;
- template_segment_id: string;
- /** 素材库中的素材 ID */
- media_asset_id?: string;
- /** 素材类型 */
- material_type: "video" | "image" | "audio" | "voiceover";
- /** 片段文案 */
- script_text: string;
- /** 实际时长(秒) */
- duration: number;
- /** 转场效果 */
- transition?: TransitionEffect;
- /** 排序 */
- order: number;
-}
-
/** 转场效果(14 种预设) */
export interface TransitionEffect {
type:
diff --git a/apps/web/src/pages/edit-plans/PlanClipsManager.tsx b/apps/web/src/pages/edit-plans/PlanClipsManager.tsx
index 9c92339d6..920535ed6 100755
--- a/apps/web/src/pages/edit-plans/PlanClipsManager.tsx
+++ b/apps/web/src/pages/edit-plans/PlanClipsManager.tsx
@@ -42,7 +42,6 @@ import {
reorderEditPlanClips,
createClipsFromAssets,
getMediaAssets,
- type EditPlan,
type EditPlanClip,
type EditPlanClipStatus,
} from "@/api/editPlans";
diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
index b8a6e1c44..ccc714d0a 100755
--- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx
+++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
@@ -1435,11 +1435,11 @@ const EditingPlanner: React.FC = () => {
onSelect={handleClipSelect}
onMoveUp={(clipId) => {
const idx = clips.findIndex((c) => c.id === clipId);
- if (idx > 0) handleClipReorder(clipId, idx - 1);
+ if (idx > 0) handleClipReorder(idx, idx - 1);
}}
onMoveDown={(clipId) => {
const idx = clips.findIndex((c) => c.id === clipId);
- if (idx < clips.length - 1) handleClipReorder(clipId, idx + 1);
+ if (idx < clips.length - 1) handleClipReorder(idx, idx + 1);
}}
onRemove={handleClipRemove}
onAdd={handleAddClip}
--
2.54.0
From beb447d602a08a76d459a5ad5eac93d562bc655c Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Thu, 16 Jul 2026 21:55:34 +0800
Subject: [PATCH 13/17] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=85ClipStatusItem?=
=?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AF=BC=E5=85=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/web/src/pages/editing-planner/EditingPlanner.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
index ccc714d0a..c57a46603 100755
--- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx
+++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
@@ -43,6 +43,7 @@ import {
batchDeleteEditPlanClips,
type EditPlanClip,
type CreateEditPlanClipRequest,
+ type ClipStatusItem,
} from "@/api/editPlans";
import { useUndoRedo } from "./hooks/useUndoRedo";
import type {
--
2.54.0
From dbc8cb9fb26239e2ffbd300387cfa9f040af7426 Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Fri, 17 Jul 2026 07:41:06 +0800
Subject: [PATCH 14/17] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=89=8D?=
=?UTF-8?q?=E7=AB=AFlint=E5=92=8C=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- STATUS_CONFIG补充cancelled状态定义
- ClipData类型增加media_asset_id字段
- EditorClipList的onAdd包一层默认参数,修复类型不匹配
- handleClipUpdate改用useCallback包裹,修复exhaustive-deps
- 修复no-explicit-any警告
- 字幕类型和常量抽离到types/subtitle.ts,满足react-refresh规则
---
apps/web/src/pages/edit-plans/EditPlans.tsx | 6 +++
.../pages/editing-planner/EditingPlanner.tsx | 33 +++++++------
.../components/SubtitleStylePanel.tsx | 42 +----------------
apps/web/src/pages/editing-planner/types.ts | 2 +
.../pages/editing-planner/types/subtitle.ts | 46 +++++++++++++++++++
5 files changed, 75 insertions(+), 54 deletions(-)
mode change 100644 => 100755 apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx
mode change 100644 => 100755 apps/web/src/pages/editing-planner/types.ts
create mode 100755 apps/web/src/pages/editing-planner/types/subtitle.ts
diff --git a/apps/web/src/pages/edit-plans/EditPlans.tsx b/apps/web/src/pages/edit-plans/EditPlans.tsx
index 26fc1f736..4ac6116e0 100755
--- a/apps/web/src/pages/edit-plans/EditPlans.tsx
+++ b/apps/web/src/pages/edit-plans/EditPlans.tsx
@@ -26,6 +26,7 @@ import {
ThunderboltOutlined,
CopyOutlined,
UnorderedListOutlined,
+ StopOutlined,
} from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import {
@@ -82,6 +83,11 @@ const STATUS_CONFIG: Record<
color: "error",
icon: ,
},
+ cancelled: {
+ label: "已取消",
+ color: "default",
+ icon: ,
+ },
};
/* ──────────── 工具函数 ──────────── */
diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
index c57a46603..e2635d2f7 100755
--- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx
+++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
@@ -89,8 +89,8 @@ import ClipPropertiesPanel from "./components/ClipPropertiesPanel";
import EditorClipList from "./components/EditorClipList";
import BgmSelector from "./components/BgmSelector";
import SubtitleStylePanel from "./components/SubtitleStylePanel";
-import type { SubtitleStyleConfig } from "./components/SubtitleStylePanel";
-import { DEFAULT_SUBTITLE_STYLE } from "./components/SubtitleStylePanel";
+import type { SubtitleStyleConfig } from "./types/subtitle";
+import { DEFAULT_SUBTITLE_STYLE } from "./types/subtitle";
import TransitionSelector from "./components/TransitionSelector";
import SpeedPanel from "./components/SpeedPanel";
import TtsPanel from "./components/TtsPanel";
@@ -626,11 +626,14 @@ const EditingPlanner: React.FC = () => {
if (selectedClipId === clipId) setSelectedClipId(null);
};
- const handleClipUpdate = (clipId: string, data: Partial) => {
- setClips((prev) =>
- prev.map((c) => (c.id === clipId ? { ...c, ...data } : c)),
- );
- };
+ const handleClipUpdate = useCallback(
+ (clipId: string, data: Partial) => {
+ setClips((prev) =>
+ prev.map((c) => (c.id === clipId ? { ...c, ...data } : c)),
+ );
+ },
+ [setClips],
+ );
/**
* 添加片段(不绑定任何素材)
@@ -738,7 +741,7 @@ const EditingPlanner: React.FC = () => {
}
// 同时更新全局默认转场(供新片段使用)
},
- [transitionTargetClipId],
+ [transitionTargetClipId, handleClipUpdate],
);
/* ── 打开转场选择器 ── */
@@ -754,7 +757,7 @@ const EditingPlanner: React.FC = () => {
handleClipUpdate(speedTargetClipId, { speed: config });
}
},
- [speedTargetClipId],
+ [speedTargetClipId, handleClipUpdate],
);
/* ── 打开调速面板 ── */
@@ -769,7 +772,7 @@ const EditingPlanner: React.FC = () => {
if (!ttsTargetClipId) return;
handleClipUpdate(ttsTargetClipId, { tts_config: ttsConfig });
},
- [ttsTargetClipId],
+ [ttsTargetClipId, handleClipUpdate],
);
/* ── 打开 TTS 配音面板 ── */
@@ -782,7 +785,7 @@ const EditingPlanner: React.FC = () => {
const handleApplySpeedAll = useCallback((config: SpeedConfig) => {
setClips((prev) => prev.map((c) => ({ ...c, speed: { ...config } })));
message.success("已应用到所有片段");
- }, []);
+ }, [setClips]);
/* ── 水印配置变更 ── */
const handleWatermarkChange = useCallback((config: WatermarkConfig) => {
@@ -1214,9 +1217,11 @@ const EditingPlanner: React.FC = () => {
message.info("取消请求已提交,正在停止...");
// 停止本地轮询,等待服务端状态通过轮询或下一次检查返回 cancelled
// 这里不立即停止,让轮询检测到 cancelled 状态后再收尾,以确保服务端确实处理了
- } catch (err: any) {
+ } catch (err: unknown) {
console.error("[取消生成失败]", err);
- const msg = err?.response?.data?.detail || "取消失败,请稍后重试";
+ const msg =
+ (err as { response?: { data?: { detail?: string } } })?.response
+ ?.data?.detail || "取消失败,请稍后重试";
message.error(msg);
}
},
@@ -1443,7 +1448,7 @@ const EditingPlanner: React.FC = () => {
if (idx < clips.length - 1) handleClipReorder(idx, idx + 1);
}}
onRemove={handleClipRemove}
- onAdd={handleAddClip}
+ onAdd={() => handleAddClip("pip", 3)}
/>
)}
diff --git a/apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx b/apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx
old mode 100644
new mode 100755
index 5f75536cf..40aeff7d0
--- a/apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx
+++ b/apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx
@@ -5,46 +5,8 @@
import React from "react";
import { Drawer, Slider, ColorPicker, Select } from "antd";
import type { Color } from "antd/es/color-picker";
-
-/* ──────────── 类型 ──────────── */
-
-export type SubtitleMode = "manual" | "asr";
-
-export interface SubtitleStyleConfig {
- /** 是否启用字幕 */
- enabled: boolean;
- /** 字幕模式:手动输入 / ASR 自动识别 */
- mode: SubtitleMode;
- /** 字体大小 px */
- fontSize: number;
- /** 字体颜色 */
- fontColor: string;
- /** 描边 */
- stroke: boolean;
- /** 阴影 */
- shadow: boolean;
- /** 字幕位置 */
- position: "top" | "center" | "bottom";
- /** 字体 */
- font: string;
- /** 动画效果 */
- animation: string;
- /** ASR 语言(仅 ASR 模式) */
- asrLanguage: "zh" | "en";
-}
-
-export const DEFAULT_SUBTITLE_STYLE: SubtitleStyleConfig = {
- enabled: true,
- mode: "asr",
- fontSize: 16,
- fontColor: "#ffffff",
- stroke: true,
- shadow: false,
- position: "bottom",
- font: "思源黑体",
- animation: "none",
- asrLanguage: "zh",
-};
+import type { SubtitleMode, SubtitleStyleConfig } from "../types/subtitle";
+import { DEFAULT_SUBTITLE_STYLE } from "../types/subtitle";
/* ──────────── 选项常量 ──────────── */
diff --git a/apps/web/src/pages/editing-planner/types.ts b/apps/web/src/pages/editing-planner/types.ts
old mode 100644
new mode 100755
index 110bffcdb..3271f0e9b
--- a/apps/web/src/pages/editing-planner/types.ts
+++ b/apps/web/src/pages/editing-planner/types.ts
@@ -512,6 +512,8 @@ export interface ClipData {
type: ClipType; // 片段类型:voice(口播)或 pip(画中画)
duration: number; // 时长(秒)
startOffset: number; // 仅 voice 类型:在口播素材中的起始时间(秒)
+ /** 素材库素材 ID(main/pip 类型片段使用) */
+ media_asset_id?: string;
// 保留兼容字段(后端序列化需要)
template_segment_id?: string;
script_text?: string;
diff --git a/apps/web/src/pages/editing-planner/types/subtitle.ts b/apps/web/src/pages/editing-planner/types/subtitle.ts
new file mode 100755
index 000000000..336effe1f
--- /dev/null
+++ b/apps/web/src/pages/editing-planner/types/subtitle.ts
@@ -0,0 +1,46 @@
+/**
+ * 字幕样式相关类型与常量
+ * 单独抽离以满足 react-refresh/only-export-components 规则
+ */
+
+/* ──────────── 类型 ──────────── */
+
+export type SubtitleMode = "manual" | "asr";
+
+export interface SubtitleStyleConfig {
+ /** 是否启用字幕 */
+ enabled: boolean;
+ /** 字幕模式:手动输入 / ASR 自动识别 */
+ mode: SubtitleMode;
+ /** 字体大小 px */
+ fontSize: number;
+ /** 字体颜色 */
+ fontColor: string;
+ /** 描边 */
+ stroke: boolean;
+ /** 阴影 */
+ shadow: boolean;
+ /** 字幕位置 */
+ position: "top" | "center" | "bottom";
+ /** 字体 */
+ font: string;
+ /** 动画效果 */
+ animation: string;
+ /** ASR 语言(仅 ASR 模式) */
+ asrLanguage: "zh" | "en";
+}
+
+/* ──────────── 默认值 ──────────── */
+
+export const DEFAULT_SUBTITLE_STYLE: SubtitleStyleConfig = {
+ enabled: true,
+ mode: "asr",
+ fontSize: 16,
+ fontColor: "#ffffff",
+ stroke: true,
+ shadow: false,
+ position: "bottom",
+ font: "思源黑体",
+ animation: "none",
+ asrLanguage: "zh",
+};
--
2.54.0
From cff4ffda832b35fbb4450ca4c6dab05103e2c1a4 Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Fri, 17 Jul 2026 07:46:18 +0800
Subject: [PATCH 15/17] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E6=9C=AA?=
=?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9A=84SubtitleMode=E5=92=8CDEFAULT=5FSUBTI?=
=?UTF-8?q?TLE=5FSTYLE=E5=AF=BC=E5=85=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pages/editing-planner/components/SubtitleStylePanel.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx b/apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx
index 40aeff7d0..27f252da0 100755
--- a/apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx
+++ b/apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx
@@ -5,8 +5,7 @@
import React from "react";
import { Drawer, Slider, ColorPicker, Select } from "antd";
import type { Color } from "antd/es/color-picker";
-import type { SubtitleMode, SubtitleStyleConfig } from "../types/subtitle";
-import { DEFAULT_SUBTITLE_STYLE } from "../types/subtitle";
+import type { SubtitleStyleConfig } from "../types/subtitle";
/* ──────────── 选项常量 ──────────── */
--
2.54.0
From a3423c0efaf509adae620a34f1eee2771edea185 Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Fri, 17 Jul 2026 07:54:42 +0800
Subject: [PATCH 16/17] =?UTF-8?q?style:=20prettier=E6=A0=BC=E5=BC=8F?=
=?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/web/src/api/editPlans.ts | 17 ++--------
.../pages/editing-planner/EditingPlanner.tsx | 33 +++++++++++++------
2 files changed, 26 insertions(+), 24 deletions(-)
diff --git a/apps/web/src/api/editPlans.ts b/apps/web/src/api/editPlans.ts
index 84d8ea9fc..5c894f75a 100755
--- a/apps/web/src/api/editPlans.ts
+++ b/apps/web/src/api/editPlans.ts
@@ -20,12 +20,7 @@ import type {
/** 剪辑计划状态枚举 */
export type EditPlanStatus =
- | "draft"
- | "editing"
- | "rendering"
- | "completed"
- | "failed"
- | "cancelled";
+ "draft" | "editing" | "rendering" | "completed" | "failed" | "cancelled";
/** 标题配置(对齐后端 title_config) */
export interface TitleConfig {
@@ -441,9 +436,7 @@ export async function getGenerationTaskResults(
}
/** 取消生成任务 */
-export async function cancelGenerationTask(
- taskId: string,
-): Promise {
+export async function cancelGenerationTask(taskId: string): Promise {
await apiClient.post(`/generation/tasks/${taskId}/cancel`);
}
@@ -452,11 +445,7 @@ export async function cancelGenerationTask(
* ============================================================ */
/** 片段状态 */
-export type EditPlanClipStatus =
- | "pending"
- | "processing"
- | "ready"
- | "failed";
+export type EditPlanClipStatus = "pending" | "processing" | "ready" | "failed";
/** 剪辑片段(后端响应) */
export interface EditPlanClip {
diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
index e2635d2f7..ab55be640 100755
--- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx
+++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx
@@ -245,7 +245,9 @@ const EditingPlanner: React.FC = () => {
});
const [coverDrawerOpen, setCoverDrawerOpen] = useState(false);
/* ── 右侧栏 Tab ── */
- const [rightTab, setRightTab] = useState<"properties" | "clips">("properties");
+ const [rightTab, setRightTab] = useState<"properties" | "clips">(
+ "properties",
+ );
/* ── 保存弹窗 ── */
const [saveModalOpen, setSaveModalOpen] = useState(false);
@@ -488,9 +490,7 @@ const EditingPlanner: React.FC = () => {
const backendClips = clipsRes?.items || [];
if (backendClips.length > 0) {
// 从后端 clips 表还原
- const sorted = [...backendClips].sort(
- (a, b) => a.order - b.order,
- );
+ const sorted = [...backendClips].sort((a, b) => a.order - b.order);
const mapped: ClipData[] = sorted.map((clip) => ({
id: clip.id,
template_segment_id:
@@ -782,10 +782,13 @@ const EditingPlanner: React.FC = () => {
}, []);
/* ── 调速应用到所有片段 ── */
- const handleApplySpeedAll = useCallback((config: SpeedConfig) => {
- setClips((prev) => prev.map((c) => ({ ...c, speed: { ...config } })));
- message.success("已应用到所有片段");
- }, [setClips]);
+ const handleApplySpeedAll = useCallback(
+ (config: SpeedConfig) => {
+ setClips((prev) => prev.map((c) => ({ ...c, speed: { ...config } })));
+ message.success("已应用到所有片段");
+ },
+ [setClips],
+ );
/* ── 水印配置变更 ── */
const handleWatermarkChange = useCallback((config: WatermarkConfig) => {
@@ -1559,7 +1562,11 @@ const EditingPlanner: React.FC = () => {
]
: generating
? [
-
>
@@ -423,7 +433,11 @@ const PlanClipsManager: React.FC = () => {
>
调整顺序
- } onClick={handleNewClip}>
+ }
+ onClick={handleNewClip}
+ >
添加片段
>
@@ -465,7 +479,9 @@ const PlanClipsManager: React.FC = () => {
{clip.text_content || clip.asset_id || "无内容"}
- {clip.duration.toFixed(1)}s
+
+ {clip.duration.toFixed(1)}s
+
{
-
+
@@ -548,15 +568,28 @@ const PlanClipsManager: React.FC = () => {
-
+
-
+
-
+
@@ -574,7 +607,8 @@ const PlanClipsManager: React.FC = () => {
loading={importLoading}
disabled={selectedAssetIds.length === 0}
>
- 导入 {selectedAssetIds.length > 0 ? `(${selectedAssetIds.length})` : ""}
+ 导入{" "}
+ {selectedAssetIds.length > 0 ? `(${selectedAssetIds.length})` : ""}
}
>
diff --git a/apps/web/src/pages/editing-planner/components/EditorClipList.tsx b/apps/web/src/pages/editing-planner/components/EditorClipList.tsx
index 1d5582f7c..015391fa2 100755
--- a/apps/web/src/pages/editing-planner/components/EditorClipList.tsx
+++ b/apps/web/src/pages/editing-planner/components/EditorClipList.tsx
@@ -123,7 +123,10 @@ const EditorClipList: React.FC = ({
)}
{/* 操作按钮 */}
- e.stopPropagation()}>
+
e.stopPropagation()}
+ >