From 2e37e72c0f6ae5a109a478a198d82fa3cc0c2f45 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 01:00:59 +0800 Subject: [PATCH 1/4] =?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=E5=8A=9F=E8=83=BD=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API层:新增 cancelGeneration 接口,扩展 cancelled 状态枚举 - 列表页:渲染中状态增加取消按钮,已取消状态支持重新生成 - 生成历史弹窗:新增操作列,进行中任务可取消 - 编辑器生成弹窗:生成中增加取消按钮 - 轮询检测 cancelled 状态,自动停止轮询并提示 对应 Issue: #411 --- apps/web/src/api/editPlans.ts | 8 +- apps/web/src/pages/edit-plans/EditPlans.tsx | 47 ++++++++++- .../pages/editing-planner/EditingPlanner.css | 26 ++++++ .../pages/editing-planner/EditingPlanner.tsx | 82 +++++++++++++++++++ .../components/GenerationHistoryModal.tsx | 22 +++++ 5 files changed, 182 insertions(+), 3 deletions(-) mode change 100644 => 100755 apps/web/src/api/editPlans.ts mode change 100644 => 100755 apps/web/src/pages/edit-plans/EditPlans.tsx mode change 100644 => 100755 apps/web/src/pages/editing-planner/EditingPlanner.css mode change 100644 => 100755 apps/web/src/pages/editing-planner/components/GenerationHistoryModal.tsx diff --git a/apps/web/src/api/editPlans.ts b/apps/web/src/api/editPlans.ts old mode 100644 new mode 100755 index 93bc13477..414f563e8 --- a/apps/web/src/api/editPlans.ts +++ b/apps/web/src/api/editPlans.ts @@ -20,7 +20,7 @@ import type { /** 剪辑计划状态枚举 */ export type EditPlanStatus = - "draft" | "editing" | "rendering" | "completed" | "failed"; + "draft" | "editing" | "rendering" | "completed" | "failed" | "cancelled"; /** 标题配置(对齐后端 title_config) */ export interface TitleConfig { @@ -453,6 +453,11 @@ export async function getGenerationTaskResults( return response.data.items || response.data || []; } +/** 取消生成任务 */ +export async function cancelGeneration(planId: string): Promise { + await apiClient.post(`/edit-plans/${planId}/cancel`); +} + /** * 获取素材库列表 — 调用 GET /api/v1/assets?library_id=xxx * 将后端 AssetResponse 映射为前端 MediaAsset 类型 @@ -553,6 +558,7 @@ export const PLAN_STATUS_LABELS: Record = { rendering: "渲染中", completed: "已完成", failed: "失败", + cancelled: "已取消", }; /** 质量分筛选选项 */ diff --git a/apps/web/src/pages/edit-plans/EditPlans.tsx b/apps/web/src/pages/edit-plans/EditPlans.tsx old mode 100644 new mode 100755 index 87283db3d..899a2f28e --- a/apps/web/src/pages/edit-plans/EditPlans.tsx +++ b/apps/web/src/pages/edit-plans/EditPlans.tsx @@ -24,12 +24,14 @@ import { DeleteOutlined, FileTextOutlined, ThunderboltOutlined, + StopOutlined, } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import { getEditPlans, deleteEditPlan, generateEditPlan, + cancelGeneration, type EditPlan, type EditPlanStatus, type EditPlanListParams, @@ -47,6 +49,7 @@ const STATUS_TABS: { key: EditPlanStatus | "all"; label: string }[] = [ { key: "rendering", label: "渲染中" }, { key: "completed", label: "已完成" }, { key: "failed", label: "失败" }, + { key: "cancelled", label: "已取消" }, ]; /** 状态标签配置 */ @@ -79,6 +82,11 @@ const STATUS_CONFIG: Record< color: "error", icon: , }, + cancelled: { + label: "已取消", + color: "default", + icon: , + }, }; /* ──────────── 工具函数 ──────────── */ @@ -184,6 +192,18 @@ export default function EditPlans() { }, }); + // 取消生成 + const cancelMutation = useMutation({ + mutationFn: cancelGeneration, + onSuccess: () => { + message.success("已提交取消请求"); + queryClient.invalidateQueries({ queryKey: ["edit-plans"] }); + }, + onError: () => { + message.error("取消失败,请稍后重试"); + }, + }); + // 跳转到剪辑编辑器 const handleEdit = useCallback( (plan: EditPlan) => { @@ -285,7 +305,7 @@ export default function EditPlans() { { title: "操作", key: "action", - width: 180, + width: 240, fixed: "right", render: (_: unknown, record: EditPlan) => (
@@ -298,7 +318,30 @@ export default function EditPlans() { > 编辑 - {(record.status === "failed" || record.status === "completed") && ( + {record.status === "rendering" && ( + cancelMutation.mutate(record.id)} + okText="确定" + cancelText="再等等" + okButtonProps={{ danger: true }} + > + + + )} + {(record.status === "failed" || + record.status === "completed" || + record.status === "cancelled") && ( { const [generated, setGenerated] = useState(false); const [generatedVideos, setGeneratedVideos] = useState([]); const [genError, setGenError] = useState(null); + const [cancelling, setCancelling] = useState(false); const genTimerRef = useRef | null>(null); /* ── 播放 ── */ @@ -1041,6 +1043,13 @@ const EditingPlanner: React.FC = () => { return; // 停止轮询 } + if (status.plan_status === "cancelled") { + setGenerating(false); + setGenError("生成已取消"); + message.info("生成任务已取消"); + return; // 停止轮询 + } + // 继续轮询 genTimerRef.current = setTimeout(poll, 2000); } catch (err) { @@ -1060,6 +1069,33 @@ const EditingPlanner: React.FC = () => { }; }, []); + /** 取消生成任务 */ + const handleCancelGeneration = async () => { + const targetId = loadedPlanId; + if (!targetId) return; + + Modal.confirm({ + title: "确认取消生成", + content: "取消后已开始的生成任务,已生成的片段不会保留。确定要取消吗?", + okText: "确认取消", + cancelText: "继续生成", + okButtonProps: { danger: true }, + onOk: async () => { + try { + setCancelling(true); + await cancelGeneration(targetId); + message.success("已提交取消请求"); + // 轮询会继续运行直到检测到 cancelled 状态 + } catch (err) { + console.error("[取消失败]", err); + message.error("取消失败,请稍后重试"); + } finally { + setCancelling(false); + } + }, + }); + }; + /* 查看生成历史 */ const handleViewGenHistory = async () => { const targetId = loadedPlanId || loadedTemplateId; @@ -1284,6 +1320,28 @@ const EditingPlanner: React.FC = () => { loading={genHistoryLoading} history={genHistory} onClose={() => setGenHistoryOpen(false)} + onCancel={async (taskId) => { + Modal.confirm({ + title: "确认取消生成", + content: "确定要取消这个生成任务吗?此操作不可恢复。", + okText: "确认取消", + cancelText: "再等等", + okButtonProps: { danger: true }, + onOk: async () => { + if (!loadedPlanId) return; + try { + await cancelGeneration(loadedPlanId); + message.success("已提交取消请求"); + // 刷新历史列表 + handleViewGenHistory(); + } catch (err) { + console.error("[取消失败]", err); + message.error("取消失败,请稍后重试"); + } + }, + }); + }} + cancelLoading={cancelling} /> {/* ═══ 生成进度弹窗 ═══ */} @@ -1298,6 +1356,7 @@ const EditingPlanner: React.FC = () => { onClick={() => { setGenerated(false); setGenerating(false); + setGenError(null); }} > 关闭 @@ -1324,6 +1383,29 @@ const EditingPlanner: React.FC = () => { ), ] + : generating + ? [ + , + ] + : genError + ? [ + , + ] : null } closable={!generating} diff --git a/apps/web/src/pages/editing-planner/components/GenerationHistoryModal.tsx b/apps/web/src/pages/editing-planner/components/GenerationHistoryModal.tsx old mode 100644 new mode 100755 index ccd330788..7c4ce926f --- a/apps/web/src/pages/editing-planner/components/GenerationHistoryModal.tsx +++ b/apps/web/src/pages/editing-planner/components/GenerationHistoryModal.tsx @@ -12,6 +12,8 @@ interface GenerationHistoryModalProps { loading: boolean; history: EditPlanGeneration[]; onClose: () => void; + onCancel?: (taskId: string) => void; + cancelLoading?: boolean; } const GenerationHistoryModal: React.FC = ({ @@ -19,6 +21,8 @@ const GenerationHistoryModal: React.FC = ({ loading, history, onClose, + onCancel, + cancelLoading, }) => { if (!open) return null; @@ -57,11 +61,14 @@ const GenerationHistoryModal: React.FC = ({ 状态 创建时间 更新时间 + {onCancel && 操作} {history.map((gen) => { const statusClass = `ep-gh-status-tag--${gen.status}`; + const canCancel = + gen.status === "rendering" || gen.status === "editing"; return ( @@ -82,6 +89,21 @@ const GenerationHistoryModal: React.FC = ({ ? new Date(gen.updated_at).toLocaleString("zh-CN") : "—"} + {onCancel && ( + + {canCancel ? ( + + ) : ( + + )} + + )} ); })} -- 2.54.0 From 5084b7b4d89d528cc7cf278b67a4349ef1f19178 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 07:32:22 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?lint=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除未使用的taskId参数(TS6133) - handleClipUpdate改用useCallback包裹,修复exhaustive-deps - 字幕类型和常量抽离到types/subtitle.ts,满足react-refresh规则 --- .../pages/editing-planner/EditingPlanner.tsx | 27 ++++++----- .../components/SubtitleStylePanel.tsx | 42 +---------------- .../pages/editing-planner/types/subtitle.ts | 46 +++++++++++++++++++ 3 files changed, 63 insertions(+), 52 deletions(-) mode change 100644 => 100755 apps/web/src/pages/editing-planner/components/SubtitleStylePanel.tsx create mode 100755 apps/web/src/pages/editing-planner/types/subtitle.ts diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index 78f0780e2..6f2fc0dc7 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -82,8 +82,8 @@ import TimelinePanel from "./components/TimelinePanel"; import ClipPropertiesPanel from "./components/ClipPropertiesPanel"; 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"; @@ -560,11 +560,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], + ); /** * 添加片段(不绑定任何素材) @@ -672,7 +675,7 @@ const EditingPlanner: React.FC = () => { } // 同时更新全局默认转场(供新片段使用) }, - [transitionTargetClipId], + [transitionTargetClipId, handleClipUpdate], ); /* ── 打开转场选择器 ── */ @@ -688,7 +691,7 @@ const EditingPlanner: React.FC = () => { handleClipUpdate(speedTargetClipId, { speed: config }); } }, - [speedTargetClipId], + [speedTargetClipId, handleClipUpdate], ); /* ── 打开调速面板 ── */ @@ -703,7 +706,7 @@ const EditingPlanner: React.FC = () => { if (!ttsTargetClipId) return; handleClipUpdate(ttsTargetClipId, { tts_config: ttsConfig }); }, - [ttsTargetClipId], + [ttsTargetClipId, handleClipUpdate], ); /* ── 打开 TTS 配音面板 ── */ @@ -716,7 +719,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) => { @@ -1320,7 +1323,7 @@ const EditingPlanner: React.FC = () => { loading={genHistoryLoading} history={genHistory} onClose={() => setGenHistoryOpen(false)} - onCancel={async (taskId) => { + onCancel={async () => { Modal.confirm({ title: "确认取消生成", content: "确定要取消这个生成任务吗?此操作不可恢复。", 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/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 f52a33e0f9bddf625d08a9b6648523a9474e2bfe Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 07:43:53 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4SubtitleStylePanel?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=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 40f7ceb4ddfbafd77cc146823737acedcd30a283 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 07:56:54 +0800 Subject: [PATCH 4/4] =?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 --- .../pages/editing-planner/EditingPlanner.tsx | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index 6f2fc0dc7..9cff3cbbe 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -716,10 +716,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) => { @@ -1387,29 +1390,29 @@ const EditingPlanner: React.FC = () => { ), ] : generating - ? [ - , - ] - : genError - ? [ - , - ] - : null + ? [ + , + ] + : genError + ? [ + , + ] + : null } closable={!generating} maskClosable={false} -- 2.54.0