From ba27d48d102232393f3cb3ede85f5f58f5c9e723 Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:05:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/routes_admin.go | 3 + internal/handler/scraper_queue.go | 11 + internal/handler/strm.go | 22 ++ internal/repository/scrape_task_repository.go | 10 + internal/repository/strm_repository.go | 22 ++ internal/service/scraper_queue.go | 4 + internal/service/strm_queue.go | 10 + web/src/api/scraper.ts | 3 + web/src/api/strm.ts | 6 + web/src/components/GlobalEvents.tsx | 2 +- web/src/pages/ScraperQueuePage.tsx | 196 +++++++++++++- web/src/pages/StrmQueuePage.tsx | 256 +++++++++++++++++- web/src/pages/useAdminLibraryPanel.ts | 4 +- web/src/pages/useLibraryScanStatus.ts | 19 +- 14 files changed, 557 insertions(+), 11 deletions(-) diff --git a/internal/handler/routes_admin.go b/internal/handler/routes_admin.go index 351bb35..262d501 100644 --- a/internal/handler/routes_admin.go +++ b/internal/handler/routes_admin.go @@ -37,6 +37,7 @@ func registerAdminScraperRoutes(admin *gin.RouterGroup, svc *service.Container) admin.POST("/scraper/queue/clear-done", clearDoneScrapeTasksHandler(svc)) admin.POST("/scraper/queue/clear-finished", clearFinishedScrapeTasksHandler(svc)) admin.POST("/scraper/queue/clear-canceled", clearCanceledScrapeTasksHandler(svc)) + admin.POST("/scraper/queue/clear-failed", clearFailedScrapeTasksHandler(svc)) admin.POST("/scraper/queue/retry-failed", retryAllFailedScrapeTasksHandler(svc)) admin.POST("/scraper/queue/cancel-pending", cancelPendingScrapeTasksHandler(svc)) admin.POST("/scraper/queue/enqueue-library/:id", enqueueLibraryScrapeHandler(svc)) @@ -87,6 +88,7 @@ func registerAdminStrmRoutes(admin *gin.RouterGroup, svc *service.Container) { admin.POST("/strm/downloads/clear-done", clearDoneDownloadsHandler(svc)) admin.POST("/strm/downloads/clear-finished", clearFinishedDownloadsHandler(svc)) admin.POST("/strm/downloads/clear-canceled", clearCanceledDownloadsHandler(svc)) + admin.POST("/strm/downloads/clear-failed", clearFailedDownloadsHandler(svc)) admin.POST("/strm/downloads/retry-failed", retryAllFailedDownloadsHandler(svc)) admin.POST("/strm/downloads/cancel-pending", cancelPendingDownloadsHandler(svc)) admin.GET("/strm/uploads", uploadQueueHandler(svc)) @@ -97,6 +99,7 @@ func registerAdminStrmRoutes(admin *gin.RouterGroup, svc *service.Container) { admin.POST("/strm/uploads/clear-done", clearDoneUploadsHandler(svc)) admin.POST("/strm/uploads/clear-finished", clearFinishedUploadsHandler(svc)) admin.POST("/strm/uploads/clear-canceled", clearCanceledUploadsHandler(svc)) + admin.POST("/strm/uploads/clear-failed", clearFailedUploadsHandler(svc)) admin.POST("/strm/uploads/retry-failed", retryAllFailedUploadsHandler(svc)) admin.POST("/strm/uploads/cancel-pending", cancelPendingUploadsHandler(svc)) } diff --git a/internal/handler/scraper_queue.go b/internal/handler/scraper_queue.go index 7f870db..0962b12 100644 --- a/internal/handler/scraper_queue.go +++ b/internal/handler/scraper_queue.go @@ -101,6 +101,17 @@ func clearCanceledScrapeTasksHandler(svc *service.Container) gin.HandlerFunc { } } +func clearFailedScrapeTasksHandler(svc *service.Container) gin.HandlerFunc { + return func(c *gin.Context) { + n, err := svc.Scraper.ClearFailedScrapeTasks(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"deleted": n}) + } +} + func retryAllFailedScrapeTasksHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { n, err := svc.Scraper.RetryAllFailedScrapeTasks(c.Request.Context()) diff --git a/internal/handler/strm.go b/internal/handler/strm.go index e92a03b..06fb741 100644 --- a/internal/handler/strm.go +++ b/internal/handler/strm.go @@ -526,6 +526,17 @@ func clearCanceledDownloadsHandler(svc *service.Container) gin.HandlerFunc { } } +func clearFailedDownloadsHandler(svc *service.Container) gin.HandlerFunc { + return func(c *gin.Context) { + n, err := svc.Strm.ClearFailedDownloadTasks(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"deleted": n}) + } +} + func clearCanceledUploadsHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { n, err := svc.Strm.ClearCanceledUploadTasks(c.Request.Context()) @@ -537,6 +548,17 @@ func clearCanceledUploadsHandler(svc *service.Container) gin.HandlerFunc { } } +func clearFailedUploadsHandler(svc *service.Container) gin.HandlerFunc { + return func(c *gin.Context) { + n, err := svc.Strm.ClearFailedUploadTasks(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"deleted": n}) + } +} + func clearDoneUploadsHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { n, err := svc.Strm.ClearDoneUploadTasks(c.Request.Context()) diff --git a/internal/repository/scrape_task_repository.go b/internal/repository/scrape_task_repository.go index 6b50714..4140625 100644 --- a/internal/repository/scrape_task_repository.go +++ b/internal/repository/scrape_task_repository.go @@ -236,6 +236,16 @@ func (r *ScrapeTaskRepository) ClearCanceled(ctx context.Context) (int64, error) return count, err } +func (r *ScrapeTaskRepository) ClearFailed(ctx context.Context) (int64, error) { + var count int64 + err := withSQLiteBusyRetry(ctx, func() error { + res := r.db.WithContext(ctx).Unscoped().Where("status = ?", model.ScrapeTaskFailed).Delete(&model.ScrapeTask{}) + count = res.RowsAffected + return res.Error + }) + return count, err +} + func (r *ScrapeTaskRepository) RetryAllFailed(ctx context.Context) (int64, error) { var count int64 err := withSQLiteBusyRetry(ctx, func() error { diff --git a/internal/repository/strm_repository.go b/internal/repository/strm_repository.go index fe3de4e..bbf411e 100644 --- a/internal/repository/strm_repository.go +++ b/internal/repository/strm_repository.go @@ -402,6 +402,17 @@ func (r *StrmDownloadTaskRepository) ClearCanceled(ctx context.Context) (int64, return count, err } +// ClearFailed 清空全部已失败下载任务。 +func (r *StrmDownloadTaskRepository) ClearFailed(ctx context.Context) (int64, error) { + var count int64 + err := withSQLiteBusyRetry(ctx, func() error { + res := r.db.WithContext(ctx).Unscoped().Where("status = ?", model.StrmTaskFailed).Delete(&model.StrmDownloadTask{}) + count = res.RowsAffected + return res.Error + }) + return count, err +} + // RetryAllFailed 把所有失败任务重置回待处理,清空错误与重试计数。 func (r *StrmDownloadTaskRepository) RetryAllFailed(ctx context.Context) (int64, error) { var count int64 @@ -709,6 +720,17 @@ func (r *StrmUploadTaskRepository) ClearCanceled(ctx context.Context) (int64, er return count, err } +// ClearFailed 清空全部已失败上传任务。 +func (r *StrmUploadTaskRepository) ClearFailed(ctx context.Context) (int64, error) { + var count int64 + err := withSQLiteBusyRetry(ctx, func() error { + res := r.db.WithContext(ctx).Unscoped().Where("status = ?", model.StrmTaskFailed).Delete(&model.StrmUploadTask{}) + count = res.RowsAffected + return res.Error + }) + return count, err +} + // RetryAllFailed 把所有失败任务重置回待处理,清空错误与重试计数。 func (r *StrmUploadTaskRepository) RetryAllFailed(ctx context.Context) (int64, error) { var count int64 diff --git a/internal/service/scraper_queue.go b/internal/service/scraper_queue.go index 143c826..04408c7 100644 --- a/internal/service/scraper_queue.go +++ b/internal/service/scraper_queue.go @@ -345,6 +345,10 @@ func (s *ScraperService) ClearCanceledScrapeTasks(ctx context.Context) (int64, e return s.repo.ScrapeTask.ClearCanceled(ctx) } +func (s *ScraperService) ClearFailedScrapeTasks(ctx context.Context) (int64, error) { + return s.repo.ScrapeTask.ClearFailed(ctx) +} + func (s *ScraperService) RetryAllFailedScrapeTasks(ctx context.Context) (int64, error) { return s.repo.ScrapeTask.RetryAllFailed(ctx) } diff --git a/internal/service/strm_queue.go b/internal/service/strm_queue.go index b656e62..fe5fac1 100644 --- a/internal/service/strm_queue.go +++ b/internal/service/strm_queue.go @@ -610,11 +610,21 @@ func (s *StrmService) ClearCanceledDownloadTasks(ctx context.Context) (int64, er return s.repo.StrmDownload.ClearCanceled(ctx) } +// ClearFailedDownloadTasks 清空全部已失败的下载记录,返回删除数量。 +func (s *StrmService) ClearFailedDownloadTasks(ctx context.Context) (int64, error) { + return s.repo.StrmDownload.ClearFailed(ctx) +} + // ClearCanceledUploadTasks 清空全部已取消的上传记录,返回删除数量。 func (s *StrmService) ClearCanceledUploadTasks(ctx context.Context) (int64, error) { return s.repo.StrmUpload.ClearCanceled(ctx) } +// ClearFailedUploadTasks 清空全部已失败的上传记录,返回删除数量。 +func (s *StrmService) ClearFailedUploadTasks(ctx context.Context) (int64, error) { + return s.repo.StrmUpload.ClearFailed(ctx) +} + // ClearDoneUploadTasks 清空全部已完成上传记录,返回删除数量。 func (s *StrmService) ClearDoneUploadTasks(ctx context.Context) (int64, error) { return s.repo.StrmUpload.ClearDone(ctx) diff --git a/web/src/api/scraper.ts b/web/src/api/scraper.ts index 5fd4108..80b1dbd 100644 --- a/web/src/api/scraper.ts +++ b/web/src/api/scraper.ts @@ -39,6 +39,9 @@ export const scraperAPI = { clearCanceled: () => api.post<{ deleted: number }>('/admin/scraper/queue/clear-canceled').then((r) => r.data), + clearFailed: () => + api.post<{ deleted: number }>('/admin/scraper/queue/clear-failed').then((r) => r.data), + retryFailed: () => api.post<{ retried: number }>('/admin/scraper/queue/retry-failed').then((r) => r.data), diff --git a/web/src/api/strm.ts b/web/src/api/strm.ts index 46680bb..26ab058 100644 --- a/web/src/api/strm.ts +++ b/web/src/api/strm.ts @@ -159,6 +159,9 @@ export const strmAPI = { clearCanceledDownloads: () => api.post<{ deleted: number }>('/admin/strm/downloads/clear-canceled').then((r) => r.data), + clearFailedDownloads: () => + api.post<{ deleted: number }>('/admin/strm/downloads/clear-failed').then((r) => r.data), + retryFailedDownloads: () => api.post<{ retried: number }>('/admin/strm/downloads/retry-failed').then((r) => r.data), @@ -196,6 +199,9 @@ export const strmAPI = { clearCanceledUploads: () => api.post<{ deleted: number }>('/admin/strm/uploads/clear-canceled').then((r) => r.data), + clearFailedUploads: () => + api.post<{ deleted: number }>('/admin/strm/uploads/clear-failed').then((r) => r.data), + retryFailedUploads: () => api.post<{ retried: number }>('/admin/strm/uploads/retry-failed').then((r) => r.data), } \ No newline at end of file diff --git a/web/src/components/GlobalEvents.tsx b/web/src/components/GlobalEvents.tsx index 074a34f..9bc5ff5 100644 --- a/web/src/components/GlobalEvents.tsx +++ b/web/src/components/GlobalEvents.tsx @@ -26,7 +26,7 @@ export function GlobalEvents() { return } if (p.queued) { - toast.loading(String(p.message ?? '云盘扫描已加入后台队列,会自动入库'), { id }) + toast.loading(String(p.message ?? '媒体库扫描已加入后台队列,会自动入库'), { id }) return } if (p.cloud && p.stage) { diff --git a/web/src/pages/ScraperQueuePage.tsx b/web/src/pages/ScraperQueuePage.tsx index b861b17..e852e0b 100644 --- a/web/src/pages/ScraperQueuePage.tsx +++ b/web/src/pages/ScraperQueuePage.tsx @@ -227,8 +227,13 @@ export function ScraperQueuePage({ embedded = false }: { embedded?: boolean }) { }, [tasks, filter, search]) const counts = snapshot?.counts - const activeTaskCount = (counts?.pending ?? 0) + (counts?.running ?? 0) + const pendingCount = counts?.pending ?? 0 + const runningCount = counts?.running ?? 0 + const activeTaskCount = pendingCount + runningCount + const doneCount = counts?.done ?? 0 const failedCount = counts?.failed ?? 0 + const canceledCount = counts?.canceled ?? 0 + const finishedCount = doneCount + failedCount + canceledCount const allCurrentChecked = filteredTasks.length > 0 && filteredTasks.every((t) => selectedIds.has(t.id)) @@ -450,7 +455,7 @@ export function ScraperQueuePage({ embedded = false }: { embedded?: boolean }) { )} - {selectedIds.size > 0 && ( + {selectedIds.size > 0 ? (
已选中 {selectedIds.size} 项
@@ -490,6 +495,193 @@ export function ScraperQueuePage({ embedded = false }: { embedded?: boolean }) {
+ ) : ( +
+ {/* 当前状态专属快捷批量按钮 */} + {filter === 'all' && ( + <> + {failedCount > 0 && ( + + )} + {activeTaskCount > 0 && ( + + )} + {finishedCount > 0 && ( + + )} + + )} + + {(filter === 'pending' || filter === 'running') && ( + + )} + + {filter === 'done' && ( + + )} + + {filter === 'failed' && ( + <> + + + + )} + + {filter === 'canceled' && ( + + )} + + {/* 下拉批量操作菜单:随时可做任意全局操作 */} +
+ + + 批量清理 + +
+ {failedCount > 0 && ( + + )} + {activeTaskCount > 0 && ( + + )} +
+ + + + +
+
+
)}
diff --git a/web/src/pages/StrmQueuePage.tsx b/web/src/pages/StrmQueuePage.tsx index 391b558..0d95114 100644 --- a/web/src/pages/StrmQueuePage.tsx +++ b/web/src/pages/StrmQueuePage.tsx @@ -224,8 +224,13 @@ export function StrmQueuePanel({ }, [tasks, filter, search]) const counts = snapshot?.counts - const activeTaskCount = (counts?.pending ?? 0) + (counts?.running ?? 0) + const pendingCount = counts?.pending ?? 0 + const runningCount = counts?.running ?? 0 + const activeTaskCount = pendingCount + runningCount + const doneCount = counts?.done ?? 0 const failedCount = counts?.failed ?? 0 + const canceledCount = counts?.canceled ?? 0 + const finishedCount = doneCount + failedCount + canceledCount const allCurrentChecked = filteredTasks.length > 0 && filteredTasks.every((t) => selectedIds.has(t.id)) @@ -475,8 +480,8 @@ export function StrmQueuePanel({ )} - {/* Selected Batch Toolbar */} - {selectedIds.size > 0 && ( + {/* Batch Actions Toolbar */} + {selectedIds.size > 0 ? (
已选中 {selectedIds.size} 项
@@ -516,6 +521,251 @@ export function StrmQueuePanel({
+ ) : ( +
+ {/* 当前状态专属快捷批量按钮 */} + {filter === 'all' && ( + <> + {failedCount > 0 && ( + + )} + {activeTaskCount > 0 && ( + + )} + {finishedCount > 0 && ( + + )} + + )} + + {(filter === 'pending' || filter === 'running') && ( + + )} + + {filter === 'done' && ( + + )} + + {filter === 'failed' && ( + <> + + + + )} + + {filter === 'canceled' && ( + + )} + + {/* 下拉批量操作菜单:随时可做任意全局操作 */} +
+ + + 批量清理 + +
+ {failedCount > 0 && ( + + )} + {activeTaskCount > 0 && ( + + )} +
+ + + + +
+
+
)}
diff --git a/web/src/pages/useAdminLibraryPanel.ts b/web/src/pages/useAdminLibraryPanel.ts index f4bdf60..57b7c21 100644 --- a/web/src/pages/useAdminLibraryPanel.ts +++ b/web/src/pages/useAdminLibraryPanel.ts @@ -163,8 +163,8 @@ function useEditableLibraryRootActions(refresh: () => Promise, drafts: Edi function useLibraryActions(refresh: () => Promise) { const scanLibrary = async (library: Library) => { const result = await libraryAPI.scan(library.id) - if (result.queued) toast.success('云盘扫描已加入后台队列,会自动入库') - else toast.success(`扫描完成,新增 ${result.added},更新 ${result.updated ?? 0}`) + if (result.queued) toast.success(result.message || '媒体库扫描已加入后台队列,会自动入库') + else toast.success(`扫描完成,新增 ${result.added ?? 0},更新 ${result.updated ?? 0}`) } const toggleCarouselLibrary = async (library: Library) => { diff --git a/web/src/pages/useLibraryScanStatus.ts b/web/src/pages/useLibraryScanStatus.ts index 648bec0..cfca84b 100644 --- a/web/src/pages/useLibraryScanStatus.ts +++ b/web/src/pages/useLibraryScanStatus.ts @@ -40,6 +40,13 @@ export function useLibraryScanStatus({ setScanProgress(String(event.message ?? '扫描已排队,后台会自动入库')) return } + if (typeof event.visited === 'number' && !event.finished) { + setScanning(true) + const added = Number(event.added ?? 0) + const updated = Number(event.updated ?? 0) + setScanProgress(`正在扫描:已扫描 ${event.visited} 项 · 新增 ${added} · 更新 ${updated}`) + return + } }, [isAdmin, libraryID, onLibraryChanged]) useWebSocket(onRealtimeEvent) @@ -49,13 +56,19 @@ export function useLibraryScanStatus({ setScanProgress('正在提交扫描任务…') try { const result = await libraryAPI.scan(libraryID) - toast.success(`扫描完成:新增 ${result.added} 项,更新 ${result.updated ?? 0} 项`) - setScanProgress(`扫描完成:新增 ${result.added} · 更新 ${result.updated ?? 0}`) + if (result.queued) { + const msg = result.message || '扫描任务已在后台启动,正在扫描…' + toast.success(msg) + setScanProgress(msg) + return + } + toast.success(`扫描完成:新增 ${result.added ?? 0} 项,更新 ${result.updated ?? 0} 项`) + setScanProgress(`扫描完成:新增 ${result.added ?? 0} · 更新 ${result.updated ?? 0}`) + setScanning(false) onLibraryChanged() } catch { toast.error('扫描失败') setScanProgress('扫描失败,请查看日志或稍后重试') - } finally { setScanning(false) } }, [libraryID, onLibraryChanged])