From e8f71b04ac5f066fac2cf5644009aa3b9b60c840 Mon Sep 17 00:00:00 2001 From: yw1573 Date: Tue, 7 Apr 2026 17:38:52 +0800 Subject: [PATCH] feat(task): add batch delete and cancel API support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持批量删除和取消任务的接口: - deleteTask 接口支持 POST 方式传入 ids 数组 - cancelTask 接口支持 POST 方式传入 ids 数组 - 前端批量操作改为调用批量接口,减少请求数量 Co-Authored-By: AI --- internal/router/task.go | 145 ++++++++++++++++++++++++++++++++-------- web/src/task/data.ts | 20 ++++++ web/src/task/index.ts | 24 +++---- 3 files changed, 148 insertions(+), 41 deletions(-) diff --git a/internal/router/task.go b/internal/router/task.go index 68fc8a7..0e1c550 100644 --- a/internal/router/task.go +++ b/internal/router/task.go @@ -138,49 +138,138 @@ func showFile(w http.ResponseWriter, r *http.Request) { } func deleteTask(w http.ResponseWriter, r *http.Request) { - taskIDStr := r.FormValue("id") - taskID, err := strconv.Atoi(taskIDStr) - if err != nil { + // 支持单个删除(GET 参数 id)和批量删除(POST 参数 ids) + var taskIDs []int + + if r.Method == http.MethodPost { + defer r.Body.Close() + var body struct { + IDs []int `json:"ids"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + util.Res{Success: false, Message: "参数错误"}.Write(w) + return + } + taskIDs = body.IDs + } else { + taskIDStr := r.FormValue("id") + taskID, err := strconv.Atoi(taskIDStr) + if err != nil { + util.Res{Success: false, Message: "参数错误"}.Write(w) + return + } + taskIDs = []int{taskID} + } + + if len(taskIDs) == 0 { util.Res{Success: false, Message: "参数错误"}.Write(w) return } + db := util.MustGetDB() defer db.Close() - _task, err := task.GetTask(db, taskID) - if err == sql.ErrNoRows { - util.Res{Success: true, Message: "数据库中没有该条记录,所以本次操作被忽略,可以算作成功。"}.Write(w) - return - } - if err != nil { - util.Res{Success: false, Message: fmt.Sprintf("task.GetTask: %v", err)}.Write(w) - return - } - filePath := _task.FilePath() - err = os.Remove(filePath) - if err != nil && !os.IsNotExist(err) { - util.Res{Success: false, Message: fmt.Sprintf("文件删除失败 os.Remove: %v", err)}.Write(w) - return + // 批量删除结果 + successCount := 0 + failedTasks := []struct { + ID int + Error string + }{} + + for _, taskID := range taskIDs { + _task, err := task.GetTask(db, taskID) + if err == sql.ErrNoRows { + // 数据库中没有该条记录,忽略 + successCount++ + continue + } + if err != nil { + failedTasks = append(failedTasks, struct { + ID int + Error string + }{ID: taskID, Error: fmt.Sprintf("获取任务失败: %v", err)}) + continue + } + filePath := _task.FilePath() + err = os.Remove(filePath) + if err != nil && !os.IsNotExist(err) { + failedTasks = append(failedTasks, struct { + ID int + Error string + }{ID: taskID, Error: fmt.Sprintf("文件删除失败: %v", err)}) + continue + } + + err = task.DeleteTask(db, taskID) + if err != nil { + failedTasks = append(failedTasks, struct { + ID int + Error string + }{ID: taskID, Error: fmt.Sprintf("数据库删除失败: %v", err)}) + continue + } + successCount++ } - err = task.DeleteTask(db, taskID) - if err != nil { - util.Res{Success: false, Message: fmt.Sprintf("task.DeleteTask: %v", err)}.Write(w) - return + if len(failedTasks) == 0 { + util.Res{Success: true, Message: fmt.Sprintf("成功删除 %d 个任务", successCount)}.Write(w) + } else { + util.Res{ + Success: successCount > 0, + Message: fmt.Sprintf("成功删除 %d 个任务,失败 %d 个", successCount, len(failedTasks)), + Data: failedTasks, + }.Write(w) } - util.Res{Success: true, Message: "删除成功"}.Write(w) } func cancelTask(w http.ResponseWriter, r *http.Request) { - taskIDStr := r.FormValue("id") - taskID, err := strconv.ParseInt(taskIDStr, 10, 64) - if err != nil { + // 支持单个取消(GET 参数 id)和批量取消(POST 参数 ids) + var taskIDs []int64 + + if r.Method == http.MethodPost { + defer r.Body.Close() + var body struct { + IDs []int64 `json:"ids"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + util.Res{Success: false, Message: "参数错误"}.Write(w) + return + } + taskIDs = body.IDs + } else { + taskIDStr := r.FormValue("id") + taskID, err := strconv.ParseInt(taskIDStr, 10, 64) + if err != nil { + util.Res{Success: false, Message: "参数错误"}.Write(w) + return + } + taskIDs = []int64{taskID} + } + + if len(taskIDs) == 0 { util.Res{Success: false, Message: "参数错误"}.Write(w) return } - if task.CancelTask(taskID) { - util.Res{Success: true, Message: "任务已取消"}.Write(w) + + // 批量取消结果 + successCount := 0 + failedIDs := []int64{} + + for _, taskID := range taskIDs { + if task.CancelTask(taskID) { + successCount++ + } else { + failedIDs = append(failedIDs, taskID) + } + } + + if len(failedIDs) == 0 { + util.Res{Success: true, Message: fmt.Sprintf("成功取消 %d 个任务", successCount)}.Write(w) } else { - util.Res{Success: false, Message: "任务不存在或已完成"}.Write(w) + util.Res{ + Success: successCount > 0, + Message: fmt.Sprintf("成功取消 %d 个任务,失败 %d 个", successCount, len(failedIDs)), + Data: failedIDs, + }.Write(w) } } diff --git a/web/src/task/data.ts b/web/src/task/data.ts index 36b3f69..034d01d 100644 --- a/web/src/task/data.ts +++ b/web/src/task/data.ts @@ -73,7 +73,27 @@ export const deleteTask = async (id: number) => { if (!res.success) throw new Error(res.message) } +/** 批量删除任务 */ +export const deleteTasks = async (ids: number[]) => { + const res = await fetch('/api/deleteTask', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(res => res.json()) as ResJSON + if (!res.success) throw new Error(res.message) +} + export const cancelTask = async (id: number) => { const res = await fetch(`/api/cancelTask?id=${id}`).then(res => res.json()) as ResJSON if (!res.success) throw new Error(res.message) +} + +/** 批量取消任务 */ +export const cancelTasks = async (ids: number[]) => { + const res = await fetch('/api/cancelTask', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(res => res.json()) as ResJSON + if (!res.success) throw new Error(res.message) } \ No newline at end of file diff --git a/web/src/task/index.ts b/web/src/task/index.ts index 32f329d..38c33a4 100644 --- a/web/src/task/index.ts +++ b/web/src/task/index.ts @@ -1,7 +1,7 @@ import van, { State } from 'vanjs-core' import { Route, goto, now } from 'vanjs-router' import { checkLogin, GLOBAL_HAS_LOGIN, GLOBAL_HIDE_PAGE, ResJSON, VanComponent } from '../mixin' -import { deleteTask, getActiveTask, getTaskList, showFile, cancelTask } from './data' +import { deleteTask, getActiveTask, getTaskList, showFile, cancelTask, deleteTasks, cancelTasks } from './data' import { TaskInDB, TaskStatus } from '../work/type' import { LoadingBox } from '../view' import { PlayerModalComp } from './playerModal' @@ -86,13 +86,14 @@ export class TaskRoute implements VanComponent { hidden: () => _that.selectedCancellable.val.length == 0, onclick() { if (!confirm(`确定要取消选中的 ${_that.selectedCancellable.val.length} 个任务吗?`)) return - _that.selectedCancellable.val.forEach(t => { - cancelTask(t.id).then(() => { + const ids = _that.selectedCancellable.val.map(t => t.id) + cancelTasks(ids).then(() => { + _that.selectedCancellable.val.forEach(t => { t.statusState.val = 'error' t.selected.val = false - }).catch(error => { - alert(error.message) }) + }).catch(error => { + alert(error.message) }) } }, () => `批量取消 (${_that.selectedCancellable.val.length})`), @@ -101,14 +102,11 @@ export class TaskRoute implements VanComponent { hidden: () => _that.selectedDeletable.val.length == 0, onclick() { if (!confirm(`确定要删除选中的 ${_that.selectedDeletable.val.length} 个任务吗?`)) return - _that.selectedDeletable.val.forEach(t => { - t.deleting.val = true - deleteTask(t.id).then(() => { - _that.taskList.val = _that.taskList.val.filter(taskInDB => taskInDB.id != t.id) - }).catch(error => { - t.deleting.val = false - alert(error.message) - }) + const ids = _that.selectedDeletable.val.map(t => t.id) + deleteTasks(ids).then(() => { + _that.taskList.val = _that.taskList.val.filter(taskInDB => !ids.includes(taskInDB.id)) + }).catch(error => { + alert(error.message) }) } }, () => `批量删除 (${_that.selectedDeletable.val.length})`),