From 8258d7c63bf9da3d2eefc7e5744cd848d37d9002 Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Sat, 6 Sep 2025 10:03:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BE=85=E7=9C=8B=E5=90=8D?= =?UTF-8?q?=E5=8D=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/README.md | 10 + backend/config/watchlist-manager.js | 275 +++++ backend/config/watchlist.json | 348 ++++++ backend/routes/watchlist.js | 180 +++ backend/server.js | 5 +- ui/src/App.vue | 4 + ui/src/components/common/WatchlistWidget.vue | 1138 ++++++++++++++++++ ui/src/services/watchlist.ts | 55 + ui/src/stores/watchlist.ts | 139 +++ 9 files changed, 2151 insertions(+), 3 deletions(-) create mode 100644 backend/config/watchlist-manager.js create mode 100644 backend/config/watchlist.json create mode 100644 backend/routes/watchlist.js create mode 100644 ui/src/components/common/WatchlistWidget.vue create mode 100644 ui/src/services/watchlist.ts create mode 100644 ui/src/stores/watchlist.ts diff --git a/backend/README.md b/backend/README.md index a75ac8a..b472ff7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -145,6 +145,15 @@ backend/ - `POST /api/repository/migrate-old-to-new` - 从旧目录迁移到新目录 - 参数: `oldDir` (旧目录路径), `newDir` (新目录路径) +### 待看名单相关 + +- `GET /api/watchlist` - 获取所有待看项目 +- `POST /api/watchlist` - 添加待看项目 + - 参数: `url` (必填), `title` (可选,不提供则自动生成) +- `PUT /api/watchlist/:id` - 更新待看项目 + - 参数: `title` (项目标题) +- `DELETE /api/watchlist/:id` - 删除待看项目 + ## 🔧 配置说明 ### 代理配置 @@ -178,6 +187,7 @@ backend/ - **download.js**: 下载相关路由 - **repository.js**: 仓库管理路由 - **proxy.js**: 代理服务路由 +- **watchlist.js**: 待看名单路由 ### 服务层 diff --git a/backend/config/watchlist-manager.js b/backend/config/watchlist-manager.js new file mode 100644 index 0000000..3079c07 --- /dev/null +++ b/backend/config/watchlist-manager.js @@ -0,0 +1,275 @@ +const fs = require('fs').promises +const path = require('path') +const { defaultLogger } = require('../utils/logger'); + +// 创建logger实例 +const logger = defaultLogger.child('WatchlistManager'); + +/** + * 待看名单管理器 + * 负责管理用户的待看名单数据 + */ +class WatchlistManager { + constructor() { + // 检测是否在pkg打包环境中运行 + const isPkg = process.pkg !== undefined; + + if (isPkg) { + // 在打包环境中,使用可执行文件所在目录 + this.configDir = path.join(process.cwd(), 'data', 'watchlist.json') + } else { + // 在开发环境中,使用相对路径 + this.configDir = path.join(__dirname, 'watchlist.json') + } + + // 确保配置目录存在 + this.ensureConfigDir() + + this.defaultData = { + items: [], + lastUpdated: new Date().toISOString() + } + } + + /** + * 确保配置目录存在 + */ + ensureConfigDir() { + try { + const configDirPath = path.dirname(this.configDir) + if (!require('fs').existsSync(configDirPath)) { + require('fs').mkdirSync(configDirPath, { recursive: true }) + logger.info('待看名单目录创建成功:', configDirPath) + } + } catch (error) { + logger.error('创建待看名单目录失败:', error) + } + } + + /** + * 初始化待看名单文件 + * 如果文件不存在,则创建默认数据 + */ + async initialize() { + try { + // 检查文件是否存在 + await fs.access(this.configDir) + logger.info('待看名单文件已存在') + } catch (error) { + // 文件不存在,创建默认文件 + logger.info('创建默认待看名单文件...') + await this.createDefaultData() + } + } + + /** + * 创建默认数据文件 + */ + async createDefaultData() { + try { + // 确保配置目录存在 + const configDirPath = path.dirname(this.configDir) + await fs.mkdir(configDirPath, { recursive: true }) + + // 检查目录是否创建成功 + try { + await fs.access(configDirPath) + logger.info('待看名单目录确认存在:', configDirPath) + } catch (accessError) { + logger.error('待看名单目录访问失败:', accessError) + throw new Error(`无法访问配置目录: ${configDirPath}`) + } + + // 写入默认数据 + const dataContent = JSON.stringify(this.defaultData, null, 2) + await fs.writeFile(this.configDir, dataContent, 'utf8') + + // 验证文件是否写入成功 + try { + await fs.access(this.configDir) + logger.info('默认待看名单文件创建成功:', this.configDir) + } catch (verifyError) { + logger.error('待看名单文件验证失败:', verifyError) + throw new Error('待看名单文件创建后无法访问') + } + } catch (error) { + logger.error('创建默认待看名单文件失败:', error) + throw error + } + } + + /** + * 读取待看名单数据 + */ + async readData() { + try { + // 首先检查文件是否存在 + const exists = await this.dataExists() + if (!exists) { + logger.info('待看名单文件不存在,创建默认数据...') + await this.createDefaultData() + } + + const dataContent = await fs.readFile(this.configDir, 'utf8') + const data = JSON.parse(dataContent) + + // 合并默认数据,确保所有必要的字段都存在 + return { ...this.defaultData, ...data } + } catch (error) { + logger.error('读取待看名单文件失败:', error) + logger.info('使用默认数据...') + // 如果读取失败,尝试创建默认数据 + try { + await this.createDefaultData() + return { ...this.defaultData } + } catch (createError) { + logger.error('创建默认数据也失败:', createError) + // 最后返回内存中的默认数据 + return { ...this.defaultData } + } + } + } + + /** + * 保存待看名单数据 + */ + async saveData(data) { + try { + // 添加更新时间 + const dataToSave = { + ...data, + lastUpdated: new Date().toISOString() + } + + await fs.writeFile( + this.configDir, + JSON.stringify(dataToSave, null, 2), + 'utf8' + ) + + logger.info('待看名单文件保存成功') + return true + } catch (error) { + logger.error('保存待看名单文件失败:', error) + throw error + } + } + + /** + * 添加待看项目 + */ + async addItem(item) { + try { + const data = await this.readData() + + // 检查是否已存在相同的URL + const existingIndex = data.items.findIndex(existingItem => existingItem.url === item.url) + + if (existingIndex !== -1) { + // 如果存在,更新现有项目 + data.items[existingIndex] = { + ...data.items[existingIndex], + ...item, + updatedAt: new Date().toISOString() + } + } else { + // 添加新项目 + const newItem = { + id: Date.now().toString(), + ...item, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + } + data.items.push(newItem) + } + + await this.saveData(data) + return data.items + } catch (error) { + logger.error('添加待看项目失败:', error) + throw error + } + } + + /** + * 删除待看项目 + */ + async removeItem(id) { + try { + const data = await this.readData() + const initialLength = data.items.length + + data.items = data.items.filter(item => item.id !== id) + + if (data.items.length === initialLength) { + throw new Error('待看项目不存在') + } + + await this.saveData(data) + return data.items + } catch (error) { + logger.error('删除待看项目失败:', error) + throw error + } + } + + /** + * 更新待看项目 + */ + async updateItem(id, updates) { + try { + const data = await this.readData() + const itemIndex = data.items.findIndex(item => item.id === id) + + if (itemIndex === -1) { + throw new Error('待看项目不存在') + } + + data.items[itemIndex] = { + ...data.items[itemIndex], + ...updates, + updatedAt: new Date().toISOString() + } + + await this.saveData(data) + return data.items[itemIndex] + } catch (error) { + logger.error('更新待看项目失败:', error) + throw error + } + } + + /** + * 获取所有待看项目 + */ + async getAllItems() { + try { + const data = await this.readData() + return data.items + } catch (error) { + logger.error('获取待看项目失败:', error) + throw error + } + } + + /** + * 获取配置文件路径 + */ + getConfigPath() { + return this.configDir + } + + /** + * 检查配置文件是否存在 + */ + async dataExists() { + try { + await fs.access(this.configDir) + return true + } catch (error) { + return false + } + } +} + +module.exports = WatchlistManager \ No newline at end of file diff --git a/backend/config/watchlist.json b/backend/config/watchlist.json new file mode 100644 index 0000000..7a15fda --- /dev/null +++ b/backend/config/watchlist.json @@ -0,0 +1,348 @@ +{ + "items": [ + { + "id": "1757123368005", + "url": "http://localhost:3001/artist/72143697", + "title": "作者 72143697", + "createdAt": "2025-09-06T01:49:28.005Z", + "updatedAt": "2025-09-06T01:49:28.005Z" + }, + { + "id": "1757123801822", + "url": "http://localhost:3001/artist/103047332", + "title": "作者管理", + "createdAt": "2025-09-06T01:56:41.822Z", + "updatedAt": "2025-09-06T01:56:41.822Z" + }, + { + "id": "1757124092574", + "url": "http://localhost:3001/artist/113088709", + "title": "作者 113088709", + "createdAt": "2025-09-06T02:01:32.574Z", + "updatedAt": "2025-09-06T02:01:32.574Z" + }, + { + "id": "1757124092579", + "url": "http://localhost:3001/artist/116491647", + "title": "作者 116491647", + "createdAt": "2025-09-06T02:01:32.579Z", + "updatedAt": "2025-09-06T02:01:32.579Z" + }, + { + "id": "1757124092583", + "url": "http://localhost:3001/artist/20002274", + "title": "作者 20002274", + "createdAt": "2025-09-06T02:01:32.583Z", + "updatedAt": "2025-09-06T02:01:32.583Z" + }, + { + "id": "1757124092586", + "url": "http://localhost:3001/artist/102068964", + "title": "作者 102068964", + "createdAt": "2025-09-06T02:01:32.586Z", + "updatedAt": "2025-09-06T02:01:32.586Z" + }, + { + "id": "1757124092589", + "url": "http://localhost:3001/artist/57881733", + "title": "作者 57881733", + "createdAt": "2025-09-06T02:01:32.589Z", + "updatedAt": "2025-09-06T02:01:32.589Z" + }, + { + "id": "1757124092595", + "url": "http://localhost:3001/artist/106498479", + "title": "作者 106498479", + "createdAt": "2025-09-06T02:01:32.595Z", + "updatedAt": "2025-09-06T02:01:32.595Z" + }, + { + "id": "1757124092598", + "url": "http://localhost:3001/artist/87347255?page=21", + "title": "作者 87347255 - 第21页", + "createdAt": "2025-09-06T02:01:32.598Z", + "updatedAt": "2025-09-06T02:01:32.598Z" + }, + { + "id": "1757124092601", + "url": "http://localhost:3001/artist/112391670", + "title": "作者 112391670", + "createdAt": "2025-09-06T02:01:32.601Z", + "updatedAt": "2025-09-06T02:01:32.601Z" + }, + { + "id": "1757124092605", + "url": "http://localhost:3001/artist/97349140", + "title": "作者 97349140", + "createdAt": "2025-09-06T02:01:32.605Z", + "updatedAt": "2025-09-06T02:01:32.605Z" + }, + { + "id": "1757124092608", + "url": "http://localhost:3001/artist/32222272", + "title": "作者 32222272", + "createdAt": "2025-09-06T02:01:32.608Z", + "updatedAt": "2025-09-06T02:01:32.608Z" + }, + { + "id": "1757124092612", + "url": "http://localhost:3001/artist/16315304", + "title": "作者 16315304", + "createdAt": "2025-09-06T02:01:32.612Z", + "updatedAt": "2025-09-06T02:01:32.612Z" + }, + { + "id": "1757124092615", + "url": "http://localhost:3001/artist/20420220", + "title": "作者 20420220", + "createdAt": "2025-09-06T02:01:32.615Z", + "updatedAt": "2025-09-06T02:01:32.615Z" + }, + { + "id": "1757124092619", + "url": "http://localhost:3001/artist/95485582", + "title": "作者 95485582", + "createdAt": "2025-09-06T02:01:32.619Z", + "updatedAt": "2025-09-06T02:01:32.619Z" + }, + { + "id": "1757124092622", + "url": "http://localhost:3001/artwork/99046180", + "title": "作品 99046180", + "createdAt": "2025-09-06T02:01:32.622Z", + "updatedAt": "2025-09-06T02:01:32.622Z" + }, + { + "id": "1757124092625", + "url": "http://localhost:3001/artist/92969522", + "title": "作者 92969522", + "createdAt": "2025-09-06T02:01:32.625Z", + "updatedAt": "2025-09-06T02:01:32.625Z" + }, + { + "id": "1757124092629", + "url": "http://localhost:3001/artist/35790899", + "title": "作者 35790899", + "createdAt": "2025-09-06T02:01:32.629Z", + "updatedAt": "2025-09-06T02:01:32.629Z" + }, + { + "id": "1757124092632", + "url": "http://localhost:3001/artist/115689478?page=11", + "title": "作者 115689478 - 第11页", + "createdAt": "2025-09-06T02:01:32.632Z", + "updatedAt": "2025-09-06T02:01:32.632Z" + }, + { + "id": "1757124092635", + "url": "http://localhost:3001/artist/54082094", + "title": "作者 54082094", + "createdAt": "2025-09-06T02:01:32.635Z", + "updatedAt": "2025-09-06T02:01:32.635Z" + }, + { + "id": "1757124092639", + "url": "http://localhost:3001/artist/113605557", + "title": "作者 113605557", + "createdAt": "2025-09-06T02:01:32.639Z", + "updatedAt": "2025-09-06T02:01:32.639Z" + }, + { + "id": "1757124092642", + "url": "http://localhost:3001/artist/12288015", + "title": "作者 12288015", + "createdAt": "2025-09-06T02:01:32.642Z", + "updatedAt": "2025-09-06T02:01:32.642Z" + }, + { + "id": "1757124092646", + "url": "http://localhost:3001/artist/107444384", + "title": "作者 107444384", + "createdAt": "2025-09-06T02:01:32.646Z", + "updatedAt": "2025-09-06T02:01:32.646Z" + }, + { + "id": "1757124092649", + "url": "http://localhost:3001/artist/98929998", + "title": "作者 98929998", + "createdAt": "2025-09-06T02:01:32.649Z", + "updatedAt": "2025-09-06T02:01:32.649Z" + }, + { + "id": "1757124092653", + "url": "http://localhost:3001/artist/35227306", + "title": "作者 35227306", + "createdAt": "2025-09-06T02:01:32.653Z", + "updatedAt": "2025-09-06T02:01:32.653Z" + }, + { + "id": "1757124092656", + "url": "http://localhost:3001/artist/55914620", + "title": "作者 55914620", + "createdAt": "2025-09-06T02:01:32.656Z", + "updatedAt": "2025-09-06T02:01:32.656Z" + }, + { + "id": "1757124092662", + "url": "http://localhost:3001/artist/112826987", + "title": "作者 112826987", + "createdAt": "2025-09-06T02:01:32.662Z", + "updatedAt": "2025-09-06T02:01:32.662Z" + }, + { + "id": "1757124092665", + "url": "http://localhost:3001/artist/117505514", + "title": "作者 117505514", + "createdAt": "2025-09-06T02:01:32.665Z", + "updatedAt": "2025-09-06T02:01:32.665Z" + }, + { + "id": "1757124092669", + "url": "http://localhost:3001/artist/116034471?page=11", + "title": "作者 116034471 - 第11页", + "createdAt": "2025-09-06T02:01:32.669Z", + "updatedAt": "2025-09-06T02:01:32.669Z" + }, + { + "id": "1757124092672", + "url": "http://localhost:3001/artist/116826562", + "title": "作者 116826562", + "createdAt": "2025-09-06T02:01:32.672Z", + "updatedAt": "2025-09-06T02:01:32.672Z" + }, + { + "id": "1757124092675", + "url": "http://localhost:3001/artist/119419938", + "title": "作者 119419938", + "createdAt": "2025-09-06T02:01:32.675Z", + "updatedAt": "2025-09-06T02:01:32.675Z" + }, + { + "id": "1757124092679", + "url": "http://localhost:3001/artist/105818043", + "title": "作者 105818043", + "createdAt": "2025-09-06T02:01:32.679Z", + "updatedAt": "2025-09-06T02:01:32.679Z" + }, + { + "id": "1757124092683", + "url": "http://localhost:3001/artist/3328617", + "title": "作者 3328617", + "createdAt": "2025-09-06T02:01:32.683Z", + "updatedAt": "2025-09-06T02:01:32.683Z" + }, + { + "id": "1757124092686", + "url": "http://localhost:3001/artist/114508563", + "title": "作者 114508563", + "createdAt": "2025-09-06T02:01:32.686Z", + "updatedAt": "2025-09-06T02:01:32.686Z" + }, + { + "id": "1757124092689", + "url": "http://localhost:3001/artist/48447420", + "title": "作者 48447420", + "createdAt": "2025-09-06T02:01:32.689Z", + "updatedAt": "2025-09-06T02:01:32.689Z" + }, + { + "id": "1757124092696", + "url": "http://localhost:3001/artist/18898196", + "title": "作者 18898196", + "createdAt": "2025-09-06T02:01:32.696Z", + "updatedAt": "2025-09-06T02:01:32.696Z" + }, + { + "id": "1757124092699", + "url": "http://localhost:3001/artist/24503943", + "title": "作者 24503943", + "createdAt": "2025-09-06T02:01:32.699Z", + "updatedAt": "2025-09-06T02:01:32.699Z" + }, + { + "id": "1757124092703", + "url": "http://localhost:3001/artist/83735825", + "title": "作者 83735825", + "createdAt": "2025-09-06T02:01:32.703Z", + "updatedAt": "2025-09-06T02:01:32.703Z" + }, + { + "id": "1757124092706", + "url": "http://localhost:3001/artist/109989392", + "title": "作者 109989392", + "createdAt": "2025-09-06T02:01:32.706Z", + "updatedAt": "2025-09-06T02:01:32.706Z" + }, + { + "id": "1757124092712", + "url": "http://localhost:3001/artist/119068816", + "title": "作者 119068816", + "createdAt": "2025-09-06T02:01:32.712Z", + "updatedAt": "2025-09-06T02:01:32.712Z" + }, + { + "id": "1757124092718", + "url": "http://localhost:3001/artist/8934406", + "title": "作者 8934406", + "createdAt": "2025-09-06T02:01:32.718Z", + "updatedAt": "2025-09-06T02:01:32.718Z" + }, + { + "id": "1757124092725", + "url": "http://localhost:3001/artist/118710458", + "title": "作者 118710458", + "createdAt": "2025-09-06T02:01:32.725Z", + "updatedAt": "2025-09-06T02:01:32.725Z" + }, + { + "id": "1757124092733", + "url": "http://localhost:3001/artist/111705416", + "title": "作者 111705416", + "createdAt": "2025-09-06T02:01:32.733Z", + "updatedAt": "2025-09-06T02:01:32.733Z" + }, + { + "id": "1757124092737", + "url": "http://localhost:3001/artist/114537113", + "title": "作者 114537113", + "createdAt": "2025-09-06T02:01:32.737Z", + "updatedAt": "2025-09-06T02:01:32.737Z" + }, + { + "id": "1757124092741", + "url": "http://localhost:3001/artist/117665623", + "title": "作者 117665623", + "createdAt": "2025-09-06T02:01:32.741Z", + "updatedAt": "2025-09-06T02:01:32.741Z" + }, + { + "id": "1757124092745", + "url": "http://localhost:3001/artist/17745716", + "title": "作者 17745716", + "createdAt": "2025-09-06T02:01:32.745Z", + "updatedAt": "2025-09-06T02:01:32.745Z" + }, + { + "id": "1757124092749", + "url": "http://localhost:3001/artist/113801960", + "title": "作者 113801960", + "createdAt": "2025-09-06T02:01:32.749Z", + "updatedAt": "2025-09-06T02:01:32.749Z" + }, + { + "id": "1757124092752", + "url": "http://localhost:3001/artist/116034471", + "title": "作者 116034471", + "createdAt": "2025-09-06T02:01:32.752Z", + "updatedAt": "2025-09-06T02:01:32.752Z" + }, + { + "id": "1757124092756", + "url": "http://localhost:3001/artist/117814316", + "title": "作者 117814316", + "createdAt": "2025-09-06T02:01:32.756Z", + "updatedAt": "2025-09-06T02:01:32.756Z" + } + ], + "lastUpdated": "2025-09-06T02:01:32.756Z" +} \ No newline at end of file diff --git a/backend/routes/watchlist.js b/backend/routes/watchlist.js new file mode 100644 index 0000000..1b01570 --- /dev/null +++ b/backend/routes/watchlist.js @@ -0,0 +1,180 @@ +const express = require('express'); +const WatchlistManager = require('../config/watchlist-manager'); +const ResponseUtil = require('../utils/response'); +const { defaultLogger } = require('../utils/logger'); + +const router = express.Router(); +const logger = defaultLogger.child('WatchlistRouter'); +const watchlistManager = new WatchlistManager(); + +// 初始化待看名单 +watchlistManager.initialize().catch(error => { + logger.error('待看名单初始化失败:', error); +}); + +/** + * 获取所有待看项目 + * GET /api/watchlist + */ +router.get('/', async (req, res) => { + try { + const items = await watchlistManager.getAllItems(); + res.json(ResponseUtil.success(items)); + } catch (error) { + logger.error('获取待看名单失败:', error); + res.status(500).json(ResponseUtil.error(error.message)); + } +}); + +/** + * 添加待看项目 + * POST /api/watchlist + */ +router.post('/', async (req, res) => { + try { + const { url, title } = req.body; + + if (!url) { + return res.status(400).json(ResponseUtil.error('URL是必填项')); + } + + // 如果没有提供标题,尝试从URL生成默认标题 + let defaultTitle = title; + if (!defaultTitle) { + try { + // 从URL路径生成默认标题 + const urlObj = new URL(url); + const pathSegments = urlObj.pathname.split('/').filter(segment => segment); + + if (pathSegments.length >= 1) { + const type = pathSegments[0]; // 例如 "artist" 或 "artwork" + + if (pathSegments.length >= 2) { + const id = pathSegments[1]; + + if (type === 'artist') { + defaultTitle = `作者 ${id}`; + } else if (type === 'artwork') { + defaultTitle = `作品 ${id}`; + } else if (type === 'search') { + // 处理搜索页面 + const keywordMatch = urlObj.search.match(/keyword=([^&]+)/); + if (keywordMatch) { + defaultTitle = `搜索: ${decodeURIComponent(keywordMatch[1])}`; + } else { + defaultTitle = '搜索页面'; + } + } else { + defaultTitle = `${type} ${id}`; + } + + // 如果有页面参数,加上页面信息 + if (urlObj.search.includes('page=')) { + const pageMatch = urlObj.search.match(/page=(\d+)/); + if (pageMatch) { + defaultTitle += ` - 第${pageMatch[1]}页`; + } + } + } else { + // 只有一级路径的情况 + switch (type) { + case 'search': + const keywordMatch = urlObj.search.match(/keyword=([^&]+)/); + if (keywordMatch) { + defaultTitle = `搜索: ${decodeURIComponent(keywordMatch[1])}`; + } else { + defaultTitle = '搜索页面'; + } + break; + case 'ranking': + const modeMatch = urlObj.search.match(/mode=([^&]+)/); + if (modeMatch) { + const modeMap = { day: '日榜', week: '周榜', month: '月榜' }; + defaultTitle = `排行榜 - ${modeMap[modeMatch[1]] || modeMatch[1]}`; + } else { + defaultTitle = '排行榜'; + } + break; + case 'bookmarks': + defaultTitle = '我的收藏'; + break; + case 'artists': + defaultTitle = '作者管理'; + break; + case 'downloads': + defaultTitle = '下载管理'; + break; + case 'repository': + defaultTitle = '仓库管理'; + break; + default: + defaultTitle = `${type}页面`; + } + } + } else { + defaultTitle = '首页'; + } + } catch (error) { + // 如果URL解析失败,使用简单的默认标题 + defaultTitle = '待看页面'; + } + } + + const item = { + url, + title: defaultTitle + }; + + const items = await watchlistManager.addItem(item); + res.json(ResponseUtil.success(items)); + } catch (error) { + logger.error('添加待看项目失败:', error); + res.status(500).json(ResponseUtil.error(error.message)); + } +}); + +/** + * 更新待看项目 + * PUT /api/watchlist/:id + */ +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + + // 移除不应该被更新的字段 + delete updates.id; + delete updates.createdAt; + + const updatedItem = await watchlistManager.updateItem(id, updates); + res.json(ResponseUtil.success(updatedItem)); + } catch (error) { + logger.error('更新待看项目失败:', error); + if (error.message === '待看项目不存在') { + res.status(404).json(ResponseUtil.error(error.message)); + } else { + res.status(500).json(ResponseUtil.error(error.message)); + } + } +}); + +/** + * 删除待看项目 + * DELETE /api/watchlist/:id + */ +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + const items = await watchlistManager.removeItem(id); + res.json(ResponseUtil.success(items)); + } catch (error) { + logger.error('删除待看项目失败:', error); + if (error.message === '待看项目不存在') { + res.status(404).json(ResponseUtil.error(error.message)); + } else { + res.status(500).json(ResponseUtil.error(error.message)); + } + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index f914e89..428a2e8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -14,6 +14,7 @@ const downloadRoutes = require('./routes/download'); const proxyRoutes = require('./routes/proxy'); const repositoryRoutes = require('./routes/repository'); const rankingRoutes = require('./routes/ranking'); +const watchlistRoutes = require('./routes/watchlist'); // 导入中间件 - 临时注释掉来定位问题 const { errorHandler } = require('./middleware/errorHandler'); @@ -123,9 +124,6 @@ function customLogger(req, res, next) { second: '2-digit', }); - // 构建日志消息 - const logMessage = `${statusColor}${statusIcon} ${methodIcon} ${method} ${url} ${statusCode} ${duration}ms\x1b[0m`; - // 输出日志 logger.info(`${statusIcon} ${methodIcon} ${method} ${url} ${statusCode} ${duration}ms`); @@ -228,6 +226,7 @@ class PixivServer { this.app.use('/api/ranking', authMiddleware, rankingRoutes); this.app.use('/api/repository', repositoryRoutes); // 仓库管理,不需要认证 this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证 + this.app.use('/api/watchlist', authMiddleware, watchlistRoutes); // 待看名单,需要认证 // 404 处理 this.app.use((req, res) => { diff --git a/ui/src/App.vue b/ui/src/App.vue index 21c3b3f..300ec26 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -6,6 +6,7 @@ import { useAuthStore } from '@/stores/auth' import { useDownloadStore } from '@/stores/download' import SettingsWidget from '@/components/common/SettingsWidget.vue' import DownloadProgressWidget from '@/components/common/DownloadProgressWidget.vue' +import WatchlistWidget from '@/components/common/WatchlistWidget.vue' const route = useRoute() const authStore = useAuthStore() @@ -92,6 +93,9 @@ onMounted(async () => { + + + diff --git a/ui/src/components/common/WatchlistWidget.vue b/ui/src/components/common/WatchlistWidget.vue new file mode 100644 index 0000000..ffaac45 --- /dev/null +++ b/ui/src/components/common/WatchlistWidget.vue @@ -0,0 +1,1138 @@ + + + + + \ No newline at end of file diff --git a/ui/src/services/watchlist.ts b/ui/src/services/watchlist.ts new file mode 100644 index 0000000..9393766 --- /dev/null +++ b/ui/src/services/watchlist.ts @@ -0,0 +1,55 @@ +import { apiService } from './api'; +import type { ApiResponse } from '@/types'; + +// 待看名单项目接口 +export interface WatchlistItem { + id: string; + url: string; + title: string; + createdAt: string; + updatedAt: string; +} + +// 添加项目的参数接口 +export interface AddWatchlistItemParams { + url: string; + title?: string; +} + +// 更新项目的参数接口 +export interface UpdateWatchlistItemParams { + title?: string; +} + +class WatchlistService { + /** + * 获取所有待看项目 + */ + async getItems(): Promise> { + return apiService.get('/api/watchlist'); + } + + /** + * 添加待看项目 + */ + async addItem(params: AddWatchlistItemParams): Promise> { + return apiService.post('/api/watchlist', params); + } + + /** + * 更新待看项目 + */ + async updateItem(id: string, params: UpdateWatchlistItemParams): Promise> { + return apiService.put(`/api/watchlist/${id}`, params); + } + + /** + * 删除待看项目 + */ + async deleteItem(id: string): Promise> { + return apiService.delete(`/api/watchlist/${id}`); + } +} + +export const watchlistService = new WatchlistService(); +export default watchlistService; \ No newline at end of file diff --git a/ui/src/stores/watchlist.ts b/ui/src/stores/watchlist.ts new file mode 100644 index 0000000..25f6a2d --- /dev/null +++ b/ui/src/stores/watchlist.ts @@ -0,0 +1,139 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; +import { watchlistService, type WatchlistItem, type AddWatchlistItemParams, type UpdateWatchlistItemParams } from '@/services/watchlist'; + +export const useWatchlistStore = defineStore('watchlist', () => { + // 状态 + const items = ref([]); + const loading = ref(false); + const error = ref(null); + + // 计算属性 + const itemCount = computed(() => items.value.length); + const hasItems = computed(() => items.value.length > 0); + + // 通用错误处理 + const handleError = (err: any, defaultMessage: string) => { + console.error(defaultMessage, err); + error.value = err.response?.data?.error || err.message || defaultMessage; + }; + + // 获取所有待看项目 + const fetchItems = async () => { + try { + loading.value = true; + error.value = null; + + const response = await watchlistService.getItems(); + if (response.success && response.data) { + items.value = response.data; + } else { + throw new Error(response.error || '获取待看名单失败'); + } + } catch (err) { + handleError(err, '获取待看名单失败'); + } finally { + loading.value = false; + } + }; + + // 添加待看项目 + const addItem = async (params: AddWatchlistItemParams) => { + try { + loading.value = true; + error.value = null; + + const response = await watchlistService.addItem(params); + if (response.success && response.data) { + items.value = response.data; + return true; + } else { + throw new Error(response.error || '添加待看项目失败'); + } + } catch (err) { + handleError(err, '添加待看项目失败'); + return false; + } finally { + loading.value = false; + } + }; + + // 更新待看项目 + const updateItem = async (id: string, params: UpdateWatchlistItemParams) => { + try { + loading.value = true; + error.value = null; + + const response = await watchlistService.updateItem(id, params); + if (response.success && response.data) { + // 更新本地数据 + const index = items.value.findIndex(item => item.id === id); + if (index !== -1) { + items.value[index] = response.data; + } + return true; + } else { + throw new Error(response.error || '更新待看项目失败'); + } + } catch (err) { + handleError(err, '更新待看项目失败'); + return false; + } finally { + loading.value = false; + } + }; + + // 删除待看项目 + const deleteItem = async (id: string) => { + try { + loading.value = true; + error.value = null; + + const response = await watchlistService.deleteItem(id); + if (response.success && response.data) { + items.value = response.data; + return true; + } else { + throw new Error(response.error || '删除待看项目失败'); + } + } catch (err) { + handleError(err, '删除待看项目失败'); + return false; + } finally { + loading.value = false; + } + }; + + // 检查URL是否已存在 + const hasUrl = (url: string) => { + return items.value.some(item => item.url === url); + }; + + // 根据URL查找项目 + const findByUrl = (url: string) => { + return items.value.find(item => item.url === url); + }; + + // 清除错误 + const clearError = () => { + error.value = null; + }; + + return { + // 状态 + items, + loading, + error, + // 计算属性 + itemCount, + hasItems, + // 方法 + fetchItems, + addItem, + updateItem, + deleteItem, + hasUrl, + findByUrl, + clearError + }; +}); \ No newline at end of file