diff --git a/backend/services/download-executor.js b/backend/services/download-executor.js index 2139614..bc76eb4 100644 --- a/backend/services/download-executor.js +++ b/backend/services/download-executor.js @@ -25,6 +25,12 @@ class DownloadExecutor { break; } + // 检查是否应该暂停 + if (this.shouldPause(task.id)) { + console.log('任务已暂停,停止下载:', task.id); + break; + } + // 从图片对象中获取指定尺寸的URL const imageObj = images[index]; let imageUrl; @@ -158,6 +164,12 @@ class DownloadExecutor { break; } + // 检查是否应该暂停 + if (this.shouldPause(task.id)) { + console.log('批量下载任务已暂停,停止下载:', task.id); + break; + } + const batch = artworkIds.slice(i, i + concurrent); const batchPromises = batch.map(async artworkId => { try { @@ -503,6 +515,56 @@ class DownloadExecutor { const match = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/); return match ? match[1] : 'jpg'; } + + /** + * 恢复暂停的任务 + */ + async resumeTask(taskId) { + const task = this.taskManager.getTask(taskId); + if (!task) { + throw new Error('任务不存在'); + } + + if (task.status !== 'paused') { + throw new Error('任务状态不是暂停状态'); + } + + // 根据任务类型重新开始下载 + if (task.type === 'artwork') { + // 重新获取作品信息和图片URL + const artworkResult = await this.downloadService.artworkService.getArtwork(task.artwork_id); + if (!artworkResult.success) { + throw new Error(`获取作品信息失败: ${artworkResult.error}`); + } + + const imagesResult = await this.downloadService.artworkService.getArtworkImages(task.artwork_id, 'original'); + if (!imagesResult.success) { + throw new Error(`获取图片URL失败: ${imagesResult.error}`); + } + + const artwork = artworkResult.data; + const images = imagesResult.data.images; + const artworkDir = await this.fileManager.getArtworkDirectory(artwork); + + // 重新开始下载 + this.executeArtworkDownload(task, images, 'original', artworkDir, artwork); + } else if (task.type === 'batch' || task.type === 'artist') { + // 批量下载和作者下载的恢复逻辑 + // 这里需要根据具体实现来恢复 + console.log('恢复批量下载任务:', taskId); + // TODO: 实现批量下载的恢复逻辑 + } + + return { success: true }; + } + + /** + * 检查任务是否应该暂停 + */ + shouldPause(taskId) { + const task = this.taskManager.getTask(taskId); + return task && task.status === 'paused'; + } } module.exports = DownloadExecutor; diff --git a/backend/services/download.js b/backend/services/download.js index eb3293d..1bbd1a2 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -134,11 +134,17 @@ class DownloadService { return { success: false, error: '任务状态不是暂停状态' }; } + // 更新任务状态为下载中 await this.taskManager.updateTask(taskId, { status: 'downloading' }); - this.progressManager.notifyProgressUpdate(taskId, task); + + // 通知进度更新 + const updatedTask = this.taskManager.getTask(taskId); + this.progressManager.notifyProgressUpdate(taskId, updatedTask); - // 重新开始下载 - return this.downloadArtwork(task.artwork_id, { skipExisting: false }); + // 重新开始下载执行 + this.downloadExecutor.resumeTask(taskId); + + return { success: true, data: updatedTask }; } // 代理方法 - 历史记录管理 diff --git a/ui/src/views/DownloadsView.vue b/ui/src/views/DownloadsView.vue index 824af2b..81ff890 100644 --- a/ui/src/views/DownloadsView.vue +++ b/ui/src/views/DownloadsView.vue @@ -62,7 +62,10 @@

- {{ getTaskTitle(task) }} + + {{ getTaskTitle(task) }} + + {{ getTaskTitle(task) }}

{{ getStatusText(task.status) }} @@ -72,6 +75,12 @@ + +
@@ -205,7 +214,7 @@ const history = ref([]); // SSE连接管理 const sseConnections = ref void>>(new Map()); -// 计算属性:只显示活跃任务 +// 计算属性:显示活跃任务和暂停任务 const activeTasks = computed(() => { return tasks.value.filter(task => ['downloading', 'paused'].includes(task.status) @@ -298,9 +307,9 @@ const fetchTasks = async () => { if (response.success) { tasks.value = response.data || []; - // 为活跃任务建立SSE连接 + // 只为正在下载的任务建立SSE连接,避免为暂停任务建立连接 activeTasks.value.forEach(task => { - if (!sseConnections.value.has(task.id)) { + if (task.status === 'downloading' && !sseConnections.value.has(task.id)) { startTaskStreaming(task.id); } }); @@ -337,6 +346,12 @@ const startTaskStreaming = (taskId: string) => { console.log('开始SSE监听任务进度:', taskId); + // 添加超时处理 + const timeoutId = setTimeout(() => { + console.warn('SSE连接超时,关闭连接:', taskId); + stopTaskStreaming(taskId); + }, 30000); // 30秒超时 + const closeConnection = downloadService.streamTaskProgress( taskId, (task) => { @@ -348,25 +363,31 @@ const startTaskStreaming = (taskId: string) => { total: task.total_files }); + // 清除超时 + clearTimeout(timeoutId); + // 更新任务状态 const index = tasks.value.findIndex(t => t.id === taskId); if (index !== -1) { tasks.value[index] = task; } - // 如果任务完成,清理连接 - if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) { - console.log('任务完成,关闭SSE连接:', taskId); + // 如果任务完成或暂停,清理连接 + if (['completed', 'failed', 'cancelled', 'partial', 'paused'].includes(task.status)) { + console.log('任务状态变更,关闭SSE连接:', taskId); stopTaskStreaming(taskId); // 延迟刷新历史记录 - setTimeout(() => { - fetchHistory(); - }, 1000); + if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) { + setTimeout(() => { + fetchHistory(); + }, 1000); + } } }, () => { console.log('SSE连接完成:', taskId); + clearTimeout(timeoutId); stopTaskStreaming(taskId); } ); @@ -382,11 +403,35 @@ const stopTaskStreaming = (taskId: string) => { } }; +// 管理SSE连接 +const manageSSEConnections = () => { + // 清理不需要的连接 + const currentTaskIds = new Set(activeTasks.value.map(task => task.id)); + + // 关闭已不存在的任务的连接 + sseConnections.value.forEach((closeConnection, taskId) => { + if (!currentTaskIds.has(taskId)) { + console.log('清理已不存在的任务连接:', taskId); + closeConnection(); + sseConnections.value.delete(taskId); + } + }); + + // 为正在下载的任务建立连接 + activeTasks.value.forEach(task => { + if (task.status === 'downloading' && !sseConnections.value.has(task.id)) { + startTaskStreaming(task.id); + } + }); +}; + // 取消任务 const cancelTask = async (taskId: string) => { try { const response = await downloadService.cancelTask(taskId); if (response.success) { + // 立即停止SSE连接 + stopTaskStreaming(taskId); await fetchTasks(); } else { throw new Error(response.error || '取消任务失败'); @@ -397,6 +442,23 @@ const cancelTask = async (taskId: string) => { } }; +// 恢复任务 +const resumeTask = async (taskId: string) => { + try { + const response = await downloadService.resumeTask(taskId); + if (response.success) { + await fetchTasks(); + // 重新管理SSE连接 + manageSSEConnections(); + } else { + throw new Error(response.error || '恢复任务失败'); + } + } catch (err) { + error.value = err instanceof Error ? err.message : '恢复任务失败'; + console.error('恢复任务失败:', err); + } +}; + // 清理历史记录 const cleanupHistory = async () => { if (confirm('确定要清理下载历史吗?这将保留最新的500条记录。')) { @@ -455,7 +517,16 @@ const cleanupSSEConnections = () => { onMounted(async () => { loading.value = true; try { - await refreshData(); + // 先获取数据,不阻塞页面渲染 + await Promise.all([ + fetchTasks(), + fetchHistory() + ]); + + // 数据加载完成后,异步管理SSE连接 + setTimeout(() => { + manageSSEConnections(); + }, 100); } catch (err) { error.value = err instanceof Error ? err.message : '加载数据失败'; } finally { @@ -662,6 +733,17 @@ onUnmounted(() => { margin: 0 0 0.25rem 0; } +.task-title .task-link { + color: #3b82f6; + text-decoration: none; + transition: color 0.2s ease; +} + +.task-title .task-link:hover { + color: #2563eb; + text-decoration: underline; +} + .task-status, .history-status { display: inline-block; @@ -707,6 +789,13 @@ onUnmounted(() => { color: #d97706; } +.task-actions { + display: flex; + gap: 0.5rem; + align-items: center; + flex-wrap: wrap; +} + .task-progress { margin-bottom: 1rem; }