feat: 生成任务取消功能前端对接
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 13s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 36s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped

- API层:新增 cancelGeneration 接口,扩展 cancelled 状态枚举
- 列表页:渲染中状态增加取消按钮,已取消状态支持重新生成
- 生成历史弹窗:新增操作列,进行中任务可取消
- 编辑器生成弹窗:生成中增加取消按钮
- 轮询检测 cancelled 状态,自动停止轮询并提示

对应 Issue: #411
This commit is contained in:
2026-07-17 01:00:59 +08:00
parent 68d15d8731
commit 2e37e72c0f
5 changed files with 182 additions and 3 deletions
Regular → Executable
+7 -1
View File
@@ -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<void> {
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<EditPlanStatus, string> = {
rendering: "渲染中",
completed: "已完成",
failed: "失败",
cancelled: "已取消",
};
/** 质量分筛选选项 */
+45 -2
View File
@@ -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: <CloseCircleOutlined />,
},
cancelled: {
label: "已取消",
color: "default",
icon: <StopOutlined />,
},
};
/* ──────────── 工具函数 ──────────── */
@@ -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) => (
<div className="plan-actions">
@@ -298,7 +318,30 @@ export default function EditPlans() {
>
</Button>
{(record.status === "failed" || record.status === "completed") && (
{record.status === "rendering" && (
<Popconfirm
title="确认取消生成"
description="确定要取消当前生成任务吗?此操作不可恢复。"
onConfirm={() => cancelMutation.mutate(record.id)}
okText="确定"
cancelText="再等等"
okButtonProps={{ danger: true }}
>
<Button
type="link"
size="small"
danger
icon={<StopOutlined />}
loading={cancelMutation.isPending}
className="plan-action-btn plan-cancel-btn"
>
</Button>
</Popconfirm>
)}
{(record.status === "failed" ||
record.status === "completed" ||
record.status === "cancelled") && (
<Popconfirm
title="确认重新生成"
description="确定要重新生成这个剪辑计划吗?"
+26
View File
@@ -6048,3 +6048,29 @@
color: #ef4444;
background: #fef2f2;
}
/* ── 生成历史取消按钮 ── */
.ep-gh-td-action {
width: 60px;
text-align: center;
}
.ep-gh-cancel-btn {
background: none;
border: none;
color: var(--color-error, #ff4d4f);
cursor: pointer;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
transition: background 0.2s;
}
.ep-gh-cancel-btn:hover:not(:disabled) {
background: rgba(255, 77, 79, 0.1);
}
.ep-gh-cancel-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ep-gh-action-placeholder {
color: var(--text-tertiary, #bfbfbf);
}
@@ -37,6 +37,7 @@ import {
generateEditPlan,
getGenerationStatus,
getGenerationTaskResults,
cancelGeneration,
} from "@/api/editPlans";
import { useUndoRedo } from "./hooks/useUndoRedo";
import type {
@@ -270,6 +271,7 @@ const EditingPlanner: React.FC = () => {
const [generated, setGenerated] = useState(false);
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([]);
const [genError, setGenError] = useState<string | null>(null);
const [cancelling, setCancelling] = useState(false);
const genTimerRef = useRef<ReturnType<typeof setTimeout> | 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 = () => {
</Button>
),
]
: generating
? [
<Button
key="cancel"
danger
loading={cancelling}
onClick={handleCancelGeneration}
>
</Button>,
]
: genError
? [
<Button
key="close"
onClick={() => {
setGenError(null);
setGenerating(false);
}}
>
</Button>,
]
: null
}
closable={!generating}
+22
View File
@@ -12,6 +12,8 @@ interface GenerationHistoryModalProps {
loading: boolean;
history: EditPlanGeneration[];
onClose: () => void;
onCancel?: (taskId: string) => void;
cancelLoading?: boolean;
}
const GenerationHistoryModal: React.FC<GenerationHistoryModalProps> = ({
@@ -19,6 +21,8 @@ const GenerationHistoryModal: React.FC<GenerationHistoryModalProps> = ({
loading,
history,
onClose,
onCancel,
cancelLoading,
}) => {
if (!open) return null;
@@ -57,11 +61,14 @@ const GenerationHistoryModal: React.FC<GenerationHistoryModalProps> = ({
<th className="ep-gh-th"></th>
<th className="ep-gh-th"></th>
<th className="ep-gh-th"></th>
{onCancel && <th className="ep-gh-th"></th>}
</tr>
</thead>
<tbody>
{history.map((gen) => {
const statusClass = `ep-gh-status-tag--${gen.status}`;
const canCancel =
gen.status === "rendering" || gen.status === "editing";
return (
<tr key={gen.id} className="ep-gh-table-row">
<td className="ep-gh-td ep-gh-td-id">
@@ -82,6 +89,21 @@ const GenerationHistoryModal: React.FC<GenerationHistoryModalProps> = ({
? new Date(gen.updated_at).toLocaleString("zh-CN")
: "—"}
</td>
{onCancel && (
<td className="ep-gh-td ep-gh-td-action">
{canCancel ? (
<button
className="ep-gh-cancel-btn"
onClick={() => onCancel(gen.id)}
disabled={cancelLoading}
>
</button>
) : (
<span className="ep-gh-action-placeholder"></span>
)}
</td>
)}
</tr>
);
})}