diff --git a/backend/README.md b/backend/README.md index 49d2893..6d3e77f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -85,6 +85,16 @@ backend/ - `GET /api/download/history` - 获取下载历史 - 参数: `offset`, `limit` +#### 任务管理API(优化版本) + +- `GET /api/download/tasks` - 获取所有任务(完整数据) +- `GET /api/download/tasks/active` - 获取活跃任务(下载中或暂停) +- `GET /api/download/tasks/summary` - 获取任务摘要(快速状态检查) +- `GET /api/download/tasks/changes` - 获取任务变更(增量更新) + - 参数: `since` (时间戳,获取指定时间后的变更) +- `GET /api/download/tasks/completed` - 获取已完成任务(分页) + - 参数: `offset`, `limit` + ### 代理相关 - `GET /api/proxy/image` - 图片代理服务 diff --git a/backend/auth.js b/backend/auth.js index b1f34fa..3e40d09 100644 --- a/backend/auth.js +++ b/backend/auth.js @@ -18,9 +18,14 @@ class PixivAuth { this.refreshToken = null; this.user = null; this.proxy = proxy; + this.isRefreshing = false; + this.failedQueue = []; // 创建 axios 实例,支持代理 this.axiosInstance = this.createAxiosInstance(); + + // 设置响应拦截器,自动处理token刷新 + this.setupResponseInterceptor(); } /** @@ -48,12 +53,102 @@ class PixivAuth { return axios.create(config); } + /** + * 设置响应拦截器,自动处理token刷新 + */ + setupResponseInterceptor() { + this.axiosInstance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + // 如果是401错误且不是刷新token的请求,尝试自动刷新 + if (error.response?.status === 401 && + !originalRequest._retry && + !originalRequest.url.includes('/auth/token') && + this.refreshToken) { + + if (this.isRefreshing) { + // 如果正在刷新,将请求加入队列 + return new Promise((resolve, reject) => { + this.failedQueue.push({ resolve, reject }); + }).then(() => { + return this.axiosInstance(originalRequest); + }).catch((err) => { + return Promise.reject(err); + }); + } + + originalRequest._retry = true; + this.isRefreshing = true; + + try { + console.log('检测到token过期,正在自动刷新...'); + const result = await this.refreshAccessToken(this.refreshToken); + + if (result.success) { + // 更新token + this.accessToken = result.access_token; + this.refreshToken = result.refresh_token; + if (result.user) { + this.user = result.user; + } + + // 处理队列中的请求 + this.processQueue(null, result.access_token); + + // 重试原始请求 + originalRequest.headers['Authorization'] = `Bearer ${result.access_token}`; + return this.axiosInstance(originalRequest); + } else { + throw new Error('Token刷新失败'); + } + } catch (refreshError) { + console.error('自动刷新token失败:', refreshError.message); + this.processQueue(refreshError, null); + return Promise.reject(refreshError); + } finally { + this.isRefreshing = false; + } + } + + return Promise.reject(error); + } + ); + } + + /** + * 处理失败的请求队列 + */ + processQueue(error, token = null) { + this.failedQueue.forEach(({ resolve, reject }) => { + if (error) { + reject(error); + } else { + resolve(token); + } + }); + this.failedQueue = []; + } + /** * 设置代理 */ setProxy(proxy) { this.proxy = proxy; this.axiosInstance = this.createAxiosInstance(); + this.setupResponseInterceptor(); + } + + /** + * 同步token状态(从外部配置更新) + */ + syncTokens(accessToken, refreshToken, user = null) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + if (user) { + this.user = user; + } } /** diff --git a/backend/core.js b/backend/core.js index a1ac9fd..a13f495 100644 --- a/backend/core.js +++ b/backend/core.js @@ -41,6 +41,15 @@ class PixivBackend { // 创建认证实例,传入代理配置 this.auth = new PixivAuth(this.config.proxy); + // 同步已保存的token状态 + if (this.config.access_token && this.config.refresh_token) { + this.auth.syncTokens( + this.config.access_token, + this.config.refresh_token, + this.config.user + ); + } + // 创建下载服务实例 this.downloadService = new DownloadService(this.auth); await this.downloadService.init(); @@ -53,9 +62,26 @@ class PixivBackend { console.log('未检测到登录信息,需要先登录'); } + // 启动token同步定时任务 + this.startTokenSyncTask(); + return this; } + /** + * 启动token同步定时任务 + */ + startTokenSyncTask() { + // 每5分钟同步一次token状态到配置文件 + setInterval(() => { + if (this.auth && this.isLoggedIn) { + this.syncTokensToConfig(); + } + }, 5 * 60 * 1000); // 5分钟 + + console.log('Token同步定时任务已启动'); + } + /** * 初始化配置文件 */ @@ -173,6 +199,13 @@ class PixivBackend { this.config.user = result.user; } + // 同步到auth实例 + this.auth.syncTokens( + result.access_token, + result.refresh_token, + result.user + ); + this.saveConfig(); this.isLoggedIn = true; @@ -260,12 +293,24 @@ class PixivBackend { } /** - * 获取认证实例(用于后续API调用) + * 获取认证实例 */ getAuth() { return this.auth; } + /** + * 同步token状态到配置 + */ + syncTokensToConfig() { + if (this.auth) { + this.config.access_token = this.auth.accessToken; + this.config.refresh_token = this.auth.refreshToken; + this.config.user = this.auth.user; + this.saveConfig(); + } + } + /** * 获取下载服务实例 */ diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 1961fc6..030fe23 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -121,4 +121,43 @@ router.post('/logout', (req, res) => { } }); +/** + * 手动刷新token + * POST /api/auth/refresh-token + */ +router.post('/refresh-token', async (req, res) => { + try { + if (!req.backend.config.refresh_token) { + return res.status(400).json({ + success: false, + error: '没有可用的刷新令牌' + }); + } + + const result = await req.backend.relogin(); + + if (result.success) { + res.json({ + success: true, + message: 'Token刷新成功', + data: { + isLoggedIn: req.backend.isLoggedIn, + username: req.backend.config.user?.account + } + }); + } else { + res.status(400).json({ + success: false, + error: result.error + }); + } + } catch (error) { + console.error('手动刷新token失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/download.js b/backend/routes/download.js index 95bb2a5..fb333fb 100644 --- a/backend/routes/download.js +++ b/backend/routes/download.js @@ -273,6 +273,92 @@ router.get('/tasks', async (req, res) => { } }); +/** + * 获取活跃任务(下载中或暂停) + * GET /api/download/tasks/active + */ +router.get('/tasks/active', async (req, res) => { + try { + const downloadService = req.backend.getDownloadService(); + const result = await downloadService.getActiveTasks(); + + res.json({ + success: true, + data: result.data + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 获取任务摘要(用于快速状态检查) + * GET /api/download/tasks/summary + */ +router.get('/tasks/summary', async (req, res) => { + try { + const downloadService = req.backend.getDownloadService(); + const result = await downloadService.getTasksSummary(); + + res.json({ + success: true, + data: result.data + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 获取任务变更(增量更新) + * GET /api/download/tasks/changes?since=timestamp + */ +router.get('/tasks/changes', async (req, res) => { + try { + const { since } = req.query; + const downloadService = req.backend.getDownloadService(); + const result = await downloadService.getTasksChanges(since ? parseInt(since) : null); + + res.json({ + success: true, + data: result.data + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 获取已完成任务(分页) + * GET /api/download/tasks/completed?offset=0&limit=50 + */ +router.get('/tasks/completed', async (req, res) => { + try { + const { offset = 0, limit = 50 } = req.query; + const downloadService = req.backend.getDownloadService(); + const result = await downloadService.getCompletedTasks(parseInt(offset), parseInt(limit)); + + res.json({ + success: true, + data: result.data + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + /** * 暂停任务 * POST /api/download/pause/:taskId @@ -548,6 +634,11 @@ router.get('/stream/:taskId', async (req, res) => { // 创建进度监听器 const progressListener = (task) => { if (task.id === taskId) { + // 使用setImmediate避免阻塞事件循环 + setImmediate(() => { + try { + // 检查连接是否仍然有效 + if (!res.destroyed) { res.write(`data: ${JSON.stringify({ type: 'progress', data: task @@ -562,6 +653,13 @@ router.get('/stream/:taskId', async (req, res) => { res.end(); downloadService.removeProgressListener(taskId, progressListener); } + } + } catch (error) { + console.error('SSE写入失败:', error); + // 连接可能已断开,清理监听器 + downloadService.removeProgressListener(taskId, progressListener); + } + }); } }; diff --git a/backend/server.js b/backend/server.js index d2c843a..c4fc511 100644 --- a/backend/server.js +++ b/backend/server.js @@ -42,8 +42,11 @@ function customLogger(req, res, next) { // 过滤掉图片代理请求 const isImageProxy = req.path === '/api/proxy/image'; - // 只记录API请求和重要请求,排除静态资源和图片代理 - if (!isStaticResource && !isImageProxy) { + // 过滤掉下载任务状态查询请求 + const isDownloadTasksQuery = req.path === '/api/download/tasks'; + + // 只记录API请求和重要请求,排除静态资源、图片代理和下载任务查询 + if (!isStaticResource && !isImageProxy && !isDownloadTasksQuery) { const start = Date.now(); // 原始响应结束方法 diff --git a/backend/services/artist.js b/backend/services/artist.js index ac60002..4fc3304 100644 --- a/backend/services/artist.js +++ b/backend/services/artist.js @@ -370,8 +370,8 @@ class ArtistService { } try { - // 发送API请求 - const response = await axios(config); + // 使用auth实例的axiosInstance发送请求,这样可以利用自动token刷新机制 + const response = await this.auth.axiosInstance(config); const responseData = response.data; // 对于GET请求,将响应数据缓存 diff --git a/backend/services/artwork.js b/backend/services/artwork.js index 307a4a8..8ff3fae 100644 --- a/backend/services/artwork.js +++ b/backend/services/artwork.js @@ -407,8 +407,8 @@ class ArtworkService { } } - // 发送API请求 - const response = await axios(config); + // 使用auth实例的axiosInstance发送请求,这样可以利用自动token刷新机制 + const response = await this.auth.axiosInstance(config); const responseData = response.data; // 对于GET请求,将响应数据缓存 diff --git a/backend/services/download.js b/backend/services/download.js index 1bbd1a2..9bacd86 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -98,6 +98,106 @@ class DownloadService { }; } + /** + * 获取活跃任务(下载中或暂停) + */ + async getActiveTasks() { + return { + success: true, + data: this.taskManager.getActiveTasks(), + }; + } + + /** + * 获取任务摘要(用于快速状态检查) + */ + async getTasksSummary() { + const allTasks = this.taskManager.getAllTasks(); + const activeTasks = this.taskManager.getActiveTasks(); + + const summary = { + total: allTasks.length, + active: activeTasks.length, + downloading: activeTasks.filter(t => t.status === 'downloading').length, + paused: activeTasks.filter(t => t.status === 'paused').length, + completed: allTasks.filter(t => t.status === 'completed').length, + failed: allTasks.filter(t => t.status === 'failed').length, + cancelled: allTasks.filter(t => t.status === 'cancelled').length, + partial: allTasks.filter(t => t.status === 'partial').length, + lastUpdate: Date.now() + }; + + return { + success: true, + data: summary, + }; + } + + /** + * 获取任务变更(增量更新) + */ + async getTasksChanges(since = null) { + const allTasks = this.taskManager.getAllTasks(); + + if (!since) { + // 如果没有since参数,返回所有活跃任务 + return { + success: true, + data: { + tasks: this.taskManager.getActiveTasks(), + lastUpdate: Date.now() + }, + }; + } + + // 过滤出自指定时间后有变更的任务 + const changedTasks = allTasks.filter(task => { + const lastModified = Math.max( + new Date(task.created_at).getTime(), + task.updated_at ? new Date(task.updated_at).getTime() : 0, + task.end_time ? new Date(task.end_time).getTime() : 0 + ); + return lastModified > since; + }); + + return { + success: true, + data: { + tasks: changedTasks, + lastUpdate: Date.now() + }, + }; + } + + /** + * 获取已完成任务(分页) + */ + async getCompletedTasks(offset = 0, limit = 50) { + const allTasks = this.taskManager.getAllTasks(); + const completedTasks = allTasks.filter(task => + ['completed', 'failed', 'cancelled', 'partial'].includes(task.status) + ); + + // 按完成时间倒序排列 + completedTasks.sort((a, b) => { + const timeA = a.end_time ? new Date(a.end_time).getTime() : 0; + const timeB = b.end_time ? new Date(b.end_time).getTime() : 0; + return timeB - timeA; + }); + + const paginatedTasks = completedTasks.slice(offset, offset + limit); + + return { + success: true, + data: { + tasks: paginatedTasks, + total: completedTasks.length, + offset, + limit + }, + }; + } + async cancelTask(taskId) { const task = this.taskManager.getTask(taskId); if (!task) { diff --git a/backend/services/progress-manager.js b/backend/services/progress-manager.js index e2ad1b8..dede373 100644 --- a/backend/services/progress-manager.js +++ b/backend/services/progress-manager.js @@ -5,6 +5,10 @@ class ProgressManager { constructor() { // 进度监听器: taskId -> listeners[] this.progressListeners = new Map(); + // 节流控制: taskId -> { lastUpdate, pending } + this.throttleControl = new Map(); + // 节流间隔(毫秒) + this.throttleInterval = 100; } /** @@ -29,14 +33,56 @@ class ProgressManager { } if (listeners.length === 0) { this.progressListeners.delete(taskId); + // 清理节流控制 + this.throttleControl.delete(taskId); } } } /** - * 通知进度更新 + * 通知进度更新(带节流) */ notifyProgressUpdate(taskId, task) { + if (!this.progressListeners.has(taskId)) { + return; + } + + const now = Date.now(); + const throttleInfo = this.throttleControl.get(taskId); + + // 如果是重要状态变更(完成、失败、取消),立即通知 + const isImportantStatus = ['completed', 'failed', 'cancelled', 'partial', 'paused'].includes(task.status); + + if (isImportantStatus) { + // 立即通知重要状态变更 + this._executeListeners(taskId, task); + // 清理节流控制 + this.throttleControl.delete(taskId); + return; + } + + // 对于普通进度更新,使用节流 + if (!throttleInfo || (now - throttleInfo.lastUpdate) >= this.throttleInterval) { + // 立即通知 + this._executeListeners(taskId, task); + this.throttleControl.set(taskId, { lastUpdate: now, pending: false }); + } else if (!throttleInfo.pending) { + // 延迟通知 + throttleInfo.pending = true; + setTimeout(() => { + const currentThrottleInfo = this.throttleControl.get(taskId); + if (currentThrottleInfo && currentThrottleInfo.pending) { + this._executeListeners(taskId, task); + this.throttleControl.delete(taskId); + } + }, this.throttleInterval - (now - throttleInfo.lastUpdate)); + } + } + + /** + * 执行监听器(内部方法) + */ + _executeListeners(taskId, task) { if (this.progressListeners.has(taskId)) { const listeners = this.progressListeners.get(taskId); listeners.forEach(listener => { diff --git a/ui/src/App.vue b/ui/src/App.vue index ef37623..21c3b3f 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -1,16 +1,33 @@ @@ -72,6 +89,9 @@ onMounted(async () => { + + + diff --git a/ui/src/components/common/DownloadProgressWidget.vue b/ui/src/components/common/DownloadProgressWidget.vue new file mode 100644 index 0000000..0c9512b --- /dev/null +++ b/ui/src/components/common/DownloadProgressWidget.vue @@ -0,0 +1,591 @@ + + + + + \ No newline at end of file diff --git a/ui/src/services/download.ts b/ui/src/services/download.ts index 596204e..54f9454 100644 --- a/ui/src/services/download.ts +++ b/ui/src/services/download.ts @@ -28,6 +28,37 @@ class DownloadService { return apiService.get('/api/download/tasks'); } + /** + * 获取活跃任务(下载中或暂停) + */ + async getActiveTasks() { + return apiService.get('/api/download/tasks/active'); + } + + /** + * 获取任务摘要(用于快速状态检查) + */ + async getTasksSummary() { + return apiService.get('/api/download/tasks/summary'); + } + + /** + * 获取任务变更(增量更新) + */ + async getTasksChanges(since?: number) { + const params = since ? { since } : {}; + return apiService.get('/api/download/tasks/changes', { params }); + } + + /** + * 获取已完成任务(分页) + */ + async getCompletedTasks(offset = 0, limit = 50) { + return apiService.get('/api/download/tasks/completed', { + params: { offset, limit } + }); + } + /** * 暂停任务 */ diff --git a/ui/src/stores/download.ts b/ui/src/stores/download.ts new file mode 100644 index 0000000..64ed1f2 --- /dev/null +++ b/ui/src/stores/download.ts @@ -0,0 +1,413 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; +import downloadService from '@/services/download'; +import type { DownloadTask } from '@/types'; + +export const useDownloadStore = defineStore('download', () => { + // 状态 + const tasks = ref([]); + const loading = ref(false); + const error = ref(null); + const lastUpdate = ref(0); + + // SSE连接管理 + const sseConnections = ref void>>(new Map()); + + // 计算属性:显示活跃任务和暂停任务 + const activeTasks = computed(() => { + return tasks.value.filter(task => + ['downloading', 'paused'].includes(task.status) + ); + }); + + // 计算属性:正在下载的任务 + const downloadingTasks = computed(() => { + return tasks.value.filter(task => task.status === 'downloading'); + }); + + // 计算属性:暂停的任务 + const pausedTasks = computed(() => { + return tasks.value.filter(task => task.status === 'paused'); + }); + + // 获取指定任务 + const getTask = (taskId: string) => { + return tasks.value.find(task => task.id === taskId) || null; + }; + + // 获取指定作品的任务 + const getArtworkTask = (artworkId: number) => { + return tasks.value.find(task => + task.artwork_id === artworkId && + ['downloading', 'paused'].includes(task.status) + ) || null; + }; + + // 获取任务列表(优化版本) + const fetchTasks = async () => { + try { + loading.value = true; + error.value = null; + + // 使用增量更新API + const response = await downloadService.getTasksChanges(lastUpdate.value); + if (response.success) { + const { tasks: changedTasks, lastUpdate: newLastUpdate } = response.data; + + // 更新任务列表 + changedTasks.forEach((changedTask: DownloadTask) => { + const index = tasks.value.findIndex((t: DownloadTask) => t.id === changedTask.id); + if (index !== -1) { + tasks.value[index] = changedTask; + } else { + tasks.value.push(changedTask); + } + }); + + lastUpdate.value = newLastUpdate; + + // 管理SSE连接 + manageSSEConnections(); + } else { + throw new Error(response.error || '获取任务列表失败'); + } + } catch (err) { + error.value = err instanceof Error ? err.message : '获取任务列表失败'; + console.error('获取任务列表失败:', err); + } finally { + loading.value = false; + } + }; + + // 获取活跃任务(轻量级) + const fetchActiveTasks = async () => { + try { + const response = await downloadService.getActiveTasks(); + if (response.success) { + // 只更新活跃任务 + const activeTaskIds = new Set(response.data.map((t: DownloadTask) => t.id)); + + // 移除已完成的活跃任务 + tasks.value = tasks.value.filter((task: DownloadTask) => + !activeTaskIds.has(task.id) || ['downloading', 'paused'].includes(task.status) + ); + + // 更新或添加活跃任务 + response.data.forEach((activeTask: DownloadTask) => { + const index = tasks.value.findIndex((t: DownloadTask) => t.id === activeTask.id); + if (index !== -1) { + tasks.value[index] = activeTask; + } else { + tasks.value.push(activeTask); + } + }); + + // 管理SSE连接 + manageSSEConnections(); + } + } catch (err) { + console.error('获取活跃任务失败:', err); + } + }; + + // 获取任务摘要(用于快速状态检查) + const fetchTasksSummary = async () => { + try { + const response = await downloadService.getTasksSummary(); + if (response.success) { + // 可以用于快速检查是否有新任务完成 + return response.data; + } + } catch (err) { + console.error('获取任务摘要失败:', err); + } + return null; + }; + + // 开始SSE监听任务进度 + const startTaskStreaming = (taskId: string) => { + // 如果已经有连接,先关闭 + if (sseConnections.value.has(taskId)) { + sseConnections.value.get(taskId)!(); + } + + console.log('开始SSE监听任务进度:', taskId); + + // 添加超时处理 + const timeoutId = setTimeout(() => { + console.warn('SSE连接超时,关闭连接:', taskId); + stopTaskStreaming(taskId); + }, 30000); // 30秒超时 + + const closeConnection = downloadService.streamTaskProgress( + taskId, + (task) => { + // console.log('收到SSE进度更新:', { + // taskId, + // status: task.status, + // progress: task.progress, + // completed: task.completed_files, + // total: task.total_files + // }); + + // 清除超时 + clearTimeout(timeoutId); + + // 更新任务状态 + const index = tasks.value.findIndex(t => t.id === taskId); + if (index !== -1) { + tasks.value[index] = task; + } else { + // 如果是新任务,添加到列表 + tasks.value.push(task); + } + + // 如果任务完成或暂停,清理连接 + if (['completed', 'failed', 'cancelled', 'partial', 'paused'].includes(task.status)) { + console.log('任务状态变更,关闭SSE连接:', taskId); + stopTaskStreaming(taskId); + } + }, + () => { + console.log('SSE连接完成:', taskId); + clearTimeout(timeoutId); + stopTaskStreaming(taskId); + } + ); + + sseConnections.value.set(taskId, closeConnection); + }; + + // 停止SSE监听 + const stopTaskStreaming = (taskId: string) => { + if (sseConnections.value.has(taskId)) { + sseConnections.value.get(taskId)!(); + sseConnections.value.delete(taskId); + } + }; + + // 管理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); + } + }); + }; + + // 清理所有SSE连接 + const cleanupSSEConnections = () => { + sseConnections.value.forEach(closeConnection => { + closeConnection(); + }); + sseConnections.value.clear(); + }; + + // 定期刷新任务列表 + let refreshInterval: number | null = null; + let summaryInterval: number | null = null; + + const startRefreshInterval = () => { + if (refreshInterval) return; + + // 主要刷新:每5秒获取活跃任务(轻量级) + refreshInterval = window.setInterval(() => { + fetchActiveTasks(); + }, 5000); + + // 摘要检查:每30秒检查一次任务摘要,如果有变化则获取详细信息 + summaryInterval = window.setInterval(async () => { + const summary = await fetchTasksSummary(); + if (summary && summary.active > 0) { + // 如果有活跃任务,确保获取最新状态 + fetchActiveTasks(); + } + }, 30000); + }; + + const stopRefreshInterval = () => { + if (refreshInterval) { + clearInterval(refreshInterval); + refreshInterval = null; + } + if (summaryInterval) { + clearInterval(summaryInterval); + summaryInterval = null; + } + }; + + // 添加新任务(用于立即显示) + const addTask = (task: DownloadTask) => { + // 检查是否已存在 + const existingIndex = tasks.value.findIndex(t => t.id === task.id); + if (existingIndex !== -1) { + tasks.value[existingIndex] = task; + } else { + tasks.value.push(task); + } + + // 如果是下载中的任务,立即建立SSE连接 + if (task.status === 'downloading') { + startTaskStreaming(task.id); + } + }; + + // 更新任务状态 + const updateTask = (taskId: string, updates: Partial) => { + const index = tasks.value.findIndex(t => t.id === taskId); + if (index !== -1) { + tasks.value[index] = { ...tasks.value[index], ...updates }; + } + }; + + // 移除任务 + const removeTask = (taskId: string) => { + const index = tasks.value.findIndex(t => t.id === taskId); + if (index !== -1) { + tasks.value.splice(index, 1); + } + stopTaskStreaming(taskId); + }; + + // 取消任务 + 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 || '取消任务失败'); + } + } catch (err) { + error.value = err instanceof Error ? err.message : '取消任务失败'; + console.error('取消任务失败:', err); + throw err; + } + }; + + // 恢复任务 + 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); + throw err; + } + }; + + // 暂停任务 + const pauseTask = async (taskId: string) => { + try { + const response = await downloadService.pauseTask(taskId); + if (response.success) { + await fetchTasks(); + } else { + throw new Error(response.error || '暂停任务失败'); + } + } catch (err) { + error.value = err instanceof Error ? err.message : '暂停任务失败'; + console.error('暂停任务失败:', err); + throw err; + } + }; + + // 清理已完成的任务 + const cleanupCompletedTasks = async (keepCount = 100) => { + try { + const response = await downloadService.cleanupTasks(true, keepCount); + if (response.success) { + await fetchTasks(); + } else { + throw new Error(response.error || '清理任务失败'); + } + } catch (err) { + error.value = err instanceof Error ? err.message : '清理任务失败'; + console.error('清理任务失败:', err); + throw err; + } + }; + + // 清理历史记录 + const cleanupHistory = async (keepCount = 500) => { + try { + const response = await downloadService.cleanupHistory(keepCount); + if (response.success) { + // 历史记录清理不影响当前任务状态 + return response.data; + } else { + throw new Error(response.error || '清理历史失败'); + } + } catch (err) { + error.value = err instanceof Error ? err.message : '清理历史失败'; + console.error('清理历史失败:', err); + throw err; + } + }; + + // 清除错误 + const clearError = () => { + error.value = null; + }; + + return { + // 状态 + tasks, + loading, + error, + lastUpdate, + + // 计算属性 + activeTasks, + downloadingTasks, + pausedTasks, + + // 方法 + getTask, + getArtworkTask, + fetchTasks, + fetchActiveTasks, + fetchTasksSummary, + addTask, + updateTask, + removeTask, + cancelTask, + resumeTask, + pauseTask, + cleanupCompletedTasks, + cleanupHistory, + clearError, + + // SSE管理 + startTaskStreaming, + stopTaskStreaming, + manageSSEConnections, + cleanupSSEConnections, + + // 定期刷新管理 + startRefreshInterval, + stopRefreshInterval + }; +}); \ No newline at end of file diff --git a/ui/src/views/ArtistView.vue b/ui/src/views/ArtistView.vue index 4926540..2c3bc67 100644 --- a/ui/src/views/ArtistView.vue +++ b/ui/src/views/ArtistView.vue @@ -60,12 +60,30 @@

作品列表

-
- +
+
+ +
+ + +
+ + {{ currentPage }} / {{ totalPages }} + +
@@ -704,6 +722,7 @@ onMounted(async () => { border-radius: 1rem; padding: 2rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + position: relative; } .section-header { @@ -720,6 +739,12 @@ onMounted(async () => { margin: 0; } +.header-controls { + display: flex; + align-items: center; + gap: 1rem; +} + .artwork-filters { display: flex; gap: 1rem; @@ -734,6 +759,58 @@ onMounted(async () => { color: #374151; } +/* 顶部分页导航样式 */ +.simple-pagination { + display: flex; + align-items: center; + gap: 0.5rem; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 0.5rem 0.75rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.simple-page-btn { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + background: white; + color: #374151; + cursor: pointer; + transition: all 0.2s; + padding: 0; +} + +.simple-page-btn:hover:not(:disabled) { + background: #f3f4f6; + border-color: #9ca3af; + transform: translateY(-1px); +} + +.simple-page-btn:disabled { + opacity: 0.4; + cursor: not-allowed; + transform: none; +} + +.simple-page-icon { + width: 1.25rem; + height: 1.25rem; +} + +.simple-page-info { + font-size: 0.875rem; + color: #6b7280; + font-weight: 500; + min-width: 3rem; + text-align: center; +} + .artworks-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); @@ -901,6 +978,12 @@ onMounted(async () => { align-items: stretch; } + /* 移动端简洁分页导航样式调整 */ + .simple-pagination { + justify-content: center; + margin-bottom: 1rem; + } + .artist-profile { flex-direction: column; text-align: center; @@ -930,6 +1013,12 @@ onMounted(async () => { align-items: stretch; } + .header-controls { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + .artworks-grid { grid-template-columns: 1fr; } diff --git a/ui/src/views/ArtworkView.vue b/ui/src/views/ArtworkView.vue index 3d48689..5d6dfb8 100644 --- a/ui/src/views/ArtworkView.vue +++ b/ui/src/views/ArtworkView.vue @@ -25,7 +25,7 @@
@@ -38,6 +38,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useAuthStore } from '@/stores/auth'; import { useRepositoryStore } from '@/stores/repository'; +import { useDownloadStore } from '@/stores/download'; import artworkService from '@/services/artwork'; import artistService from '@/services/artist'; import downloadService from '@/services/download'; @@ -53,6 +54,7 @@ const route = useRoute(); const router = useRouter(); const authStore = useAuthStore(); const repositoryStore = useRepositoryStore(); +const downloadStore = useDownloadStore(); // 状态 const artwork = ref(null); @@ -62,9 +64,11 @@ const currentPage = ref(0); const downloading = ref(false); const isDownloaded = ref(false); -// 下载任务状态 -const currentTask = ref(null); -const sseConnection = ref<(() => void) | null>(null); +// 下载任务状态 - 使用Pinia store +const currentTask = computed(() => { + if (!artwork.value) return null; + return downloadStore.getArtworkTask(artwork.value.id); +}); // 收藏错误状态 const bookmarkError = ref(null); @@ -105,10 +109,8 @@ const fetchArtworkDetail = async () => { loading.value = true; error.value = null; - // 立即清理所有下载相关状态 - currentTask.value = null; + // 清理下载状态 downloading.value = false; - stopTaskStreaming(); const response = await artworkService.getArtworkDetail(artworkId); @@ -137,19 +139,19 @@ const checkDownloadStatus = async (artworkId: number, retryCount = 0) => { try { const response = await repositoryStore.checkArtworkDownloaded(artworkId); - console.log('下载状态检查响应:', response); + // console.log('下载状态检查响应:', response); // repository store的apiCall返回的是data.data,所以response直接是数据对象 if (response && typeof response === 'object') { const newStatus = response.is_downloaded || false; // 如果状态发生变化,记录日志 - if (isDownloaded.value !== newStatus) { - console.log(`作品下载状态变化: ${isDownloaded.value} -> ${newStatus}`); - } + // if (isDownloaded.value !== newStatus) { + // console.log(`作品下载状态变化: ${isDownloaded.value} -> ${newStatus}`); + // } isDownloaded.value = newStatus; - console.log('作品下载状态:', isDownloaded.value); + // console.log('作品下载状态:', isDownloaded.value); } } catch (err) { console.error('检查下载状态失败:', err); @@ -170,8 +172,7 @@ const handleDownload = async () => { if (!artwork.value) return; try { - // 清理之前的任务状态 - currentTask.value = null; + // 清理下载状态 downloading.value = true; // 如果已经下载过,则强制重新下载(跳过现有文件检查) @@ -191,10 +192,10 @@ const handleDownload = async () => { return; } - // 如果是新任务,立即创建任务状态并开始监听进度 + // 如果是新任务,立即添加到store if (response.data.task_id) { // 立即创建任务状态,让进度条立即显示 - currentTask.value = { + const newTask: DownloadTask = { id: response.data.task_id, type: 'artwork', status: 'downloading', @@ -208,8 +209,8 @@ const handleDownload = async () => { start_time: new Date().toISOString() }; - // 立即开始SSE监听任务进度 - startTaskStreaming(response.data.task_id); + // 添加到store,store会自动管理SSE连接 + downloadStore.addTask(newTask); } } else { throw new Error(response.error || '下载失败'); @@ -222,93 +223,25 @@ const handleDownload = async () => { } }; -// 开始SSE监听任务进度 -const startTaskStreaming = (taskId: string) => { - // 清除之前的连接 - if (sseConnection.value) { - sseConnection.value(); - } - - console.log('开始SSE监听任务进度:', taskId); - - // 建立SSE连接 - sseConnection.value = downloadService.streamTaskProgress( - taskId, - (task) => { - console.log('收到SSE进度更新:', { - taskId, - status: task.status, - progress: task.progress, - completed: task.completed_files, - total: task.total_files - }); - - // 立即更新任务状态,让进度条立即显示 - currentTask.value = task; - - // 如果任务完成,清理连接并检查下载状态 - if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) { - console.log('任务完成,关闭SSE连接'); - stopTaskStreaming(); - - // 延迟检查下载状态,确保文件写入完成 - // 减少延迟时间,提高响应速度 - const delay = task.total_files > 1 ? 1500 : 1000; // 多文件延迟1.5秒,单文件延迟1秒 - - setTimeout(async () => { - // 检查当前页面是否还是同一个作品,避免页面切换后的状态更新 - if (artwork.value && artwork.value.id === task.artwork_id) { - console.log(`延迟 ${delay}ms 后检查下载状态`); - await checkDownloadStatus(artwork.value.id); - - // 如果任务完成但状态检查显示未下载,再次延迟检查 - if (task.status === 'completed' && !isDownloaded.value) { - console.log('任务完成但状态检查失败,再次延迟检查'); - setTimeout(async () => { - if (artwork.value && artwork.value.id === task.artwork_id) { - await checkDownloadStatus(artwork.value.id); - } - }, 1000); - } - - // 清理任务状态,显示下载完成状态 - currentTask.value = null; - } - }, delay); - } - }, - () => { - console.log('SSE连接完成'); - stopTaskStreaming(); +// 监听任务完成状态 +watch(currentTask, (newTask, oldTask) => { + if (oldTask && !newTask) { + // 任务被移除,检查下载状态 + if (artwork.value) { + setTimeout(async () => { + await checkDownloadStatus(artwork.value!.id); + }, 1000); + } + } else if (newTask && ['completed', 'failed', 'cancelled', 'partial'].includes(newTask.status)) { + // 任务完成,延迟检查下载状态 + if (artwork.value && artwork.value.id === newTask.artwork_id) { + const delay = newTask.total_files > 1 ? 1500 : 1000; + setTimeout(async () => { + await checkDownloadStatus(artwork.value!.id); + }, delay); } - ); -}; - - - -// 停止SSE监听 -const stopTaskStreaming = () => { - if (sseConnection.value) { - sseConnection.value(); - sseConnection.value = null; } - // 确保清理任务状态 - currentTask.value = null; - downloading.value = false; -}; - -// 更新任务状态 -const updateTask = (task: DownloadTask) => { - currentTask.value = task; -}; - -// 移除任务 -const removeTask = (taskId: string) => { - if (currentTask.value?.id === taskId) { - currentTask.value = null; - stopTaskStreaming(); - } -}; +}, { immediate: true }); // 收藏/取消收藏 const handleBookmark = async () => { @@ -386,10 +319,8 @@ const fetchArtistArtworks = async () => { // 导航到上一个作品 const navigateToPrevious = () => { if (previousArtwork.value && !loading.value) { - // 立即清理下载任务状态 - currentTask.value = null; + // 清理下载状态 downloading.value = false; - stopTaskStreaming(); // 立即设置加载状态 loading.value = true; @@ -409,10 +340,8 @@ const navigateToPrevious = () => { // 导航到下一个作品 const navigateToNext = () => { if (nextArtwork.value && !loading.value) { - // 立即清理下载任务状态 - currentTask.value = null; + // 清理下载状态 downloading.value = false; - stopTaskStreaming(); // 立即设置加载状态 loading.value = true; @@ -538,10 +467,8 @@ watch(() => route.params.id, (newId, oldId) => { // 确保页面滚动到顶部 window.scrollTo(0, 0); - // 立即清理所有下载相关状态 - currentTask.value = null; - downloading.value = false; - stopTaskStreaming(); + // 清理下载状态 + downloading.value = false; // 重新获取作品详情 fetchArtworkDetail(); @@ -583,12 +510,11 @@ onMounted(() => { document.addEventListener('keyup', handleKeyUp); }); -// 组件卸载时移除事件监听和清理SSE连接 +// 组件卸载时移除事件监听 onUnmounted(() => { document.removeEventListener('keydown', handleKeydown); document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keyup', handleKeyUp); - stopTaskStreaming(); }); diff --git a/ui/src/views/DownloadsView.vue b/ui/src/views/DownloadsView.vue index 81ff890..d3a2856 100644 --- a/ui/src/views/DownloadsView.vue +++ b/ui/src/views/DownloadsView.vue @@ -197,22 +197,23 @@