From 2e37e72c0f6ae5a109a478a198d82fa3cc0c2f45 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 17 Jul 2026 01:00:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E5=8A=9F=E8=83=BD=E5=89=8D=E7=AB=AF=E5=AF=B9?= =?UTF-8?q?=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 ? ( + + ) : ( + + )} + + )} ); })}