待看名单支持数据库同步
This commit is contained in:
@@ -69,7 +69,7 @@ Pixiv 下载浏览管理器是一个基于 Web 的应用程序,提供以下功
|
|||||||
```
|
```
|
||||||
|
|
||||||
5. **访问应用**
|
5. **访问应用**
|
||||||
- 打开浏览器访问:http://localhost:3001 (默认端口,可在 config.json 中修改)
|
- 打开浏览器访问:http://localhost:3000 (默认端口,可在 config.json 中修改)
|
||||||
|
|
||||||
## 🌐 代理配置
|
## 🌐 代理配置
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ class CacheConfigManager {
|
|||||||
// 存储模式配置
|
// 存储模式配置
|
||||||
storageMode: 'json', // 存储模式:'json' 或 'database'
|
storageMode: 'json', // 存储模式:'json' 或 'database'
|
||||||
},
|
},
|
||||||
|
// 待看名单配置
|
||||||
|
watchlist: {
|
||||||
|
// 存储模式:'json' 或 'database'
|
||||||
|
storageMode: 'json'
|
||||||
|
},
|
||||||
// 新增Windows特定配置
|
// 新增Windows特定配置
|
||||||
windows: {
|
windows: {
|
||||||
skipInUseFiles: true, // 跳过被占用的文件
|
skipInUseFiles: true, // 跳过被占用的文件
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
const { defaultLogger } = require('../utils/logger');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const logger = defaultLogger.child('WatchlistDatabase');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库版本的待看名单
|
||||||
|
* 提供与JSON版本相同的接口,但使用MySQL数据库存储
|
||||||
|
*/
|
||||||
|
class WatchlistDatabase {
|
||||||
|
constructor(databaseManager) {
|
||||||
|
this.db = databaseManager;
|
||||||
|
this.loaded = false;
|
||||||
|
this.tableName = 'watchlist_items';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化数据库表
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
// 如果表不存在则创建
|
||||||
|
if (!(await this.db.tableExists(this.tableName))) {
|
||||||
|
const createSQL = `
|
||||||
|
CREATE TABLE ${this.tableName} (
|
||||||
|
id VARCHAR(64) PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
url_hash CHAR(64) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uniq_url_hash (url_hash)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='待看名单表';
|
||||||
|
`;
|
||||||
|
await this.db.createTable(this.tableName, createSQL);
|
||||||
|
}
|
||||||
|
this.loaded = true;
|
||||||
|
logger.info('待看名单数据库表初始化完成');
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('初始化待看名单数据库失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有待看项目
|
||||||
|
*/
|
||||||
|
async getAllItems() {
|
||||||
|
try {
|
||||||
|
const result = await this.db.query(`
|
||||||
|
SELECT id, title, url, created_at, updated_at
|
||||||
|
FROM ${this.tableName}
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
`);
|
||||||
|
return result.data.map(row => ({
|
||||||
|
id: row.id.toString(),
|
||||||
|
title: row.title,
|
||||||
|
url: row.url,
|
||||||
|
createdAt: (row.created_at instanceof Date) ? row.created_at.toISOString() : row.created_at,
|
||||||
|
updatedAt: (row.updated_at instanceof Date) ? row.updated_at.toISOString() : row.updated_at
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('获取待看项目失败(数据库):', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加或更新待看项目(按URL唯一)
|
||||||
|
*/
|
||||||
|
async addItem(item) {
|
||||||
|
try {
|
||||||
|
const { url, title } = item;
|
||||||
|
if (!url) throw new Error('URL是必填项');
|
||||||
|
const urlHash = crypto.createHash('sha256').update(url).digest('hex');
|
||||||
|
|
||||||
|
// 先查是否存在
|
||||||
|
const exists = await this.db.select(this.tableName, { url_hash: urlHash });
|
||||||
|
if (exists.data && exists.data.length > 0) {
|
||||||
|
// 更新标题
|
||||||
|
await this.db.update(this.tableName, { title }, { url_hash: urlHash });
|
||||||
|
} else {
|
||||||
|
const id = (item.id?.toString()) || Date.now().toString();
|
||||||
|
await this.db.insert(this.tableName, {
|
||||||
|
id,
|
||||||
|
title: title || '待看页面',
|
||||||
|
url,
|
||||||
|
url_hash: urlHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await this.getAllItems();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('添加待看项目失败(数据库):', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除待看项目
|
||||||
|
*/
|
||||||
|
async removeItem(id) {
|
||||||
|
try {
|
||||||
|
const result = await this.db.delete(this.tableName, { id });
|
||||||
|
if (result.affectedRows === 0) {
|
||||||
|
throw new Error('待看项目不存在');
|
||||||
|
}
|
||||||
|
return await this.getAllItems();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('删除待看项目失败(数据库):', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新待看项目
|
||||||
|
*/
|
||||||
|
async updateItem(id, updates) {
|
||||||
|
try {
|
||||||
|
const allowed = {};
|
||||||
|
if (typeof updates.title === 'string') allowed.title = updates.title;
|
||||||
|
if (typeof updates.url === 'string') {
|
||||||
|
allowed.url = updates.url;
|
||||||
|
allowed.url_hash = crypto.createHash('sha256').update(updates.url).digest('hex');
|
||||||
|
}
|
||||||
|
if (Object.keys(allowed).length === 0) {
|
||||||
|
// 没有可更新字段则返回原项
|
||||||
|
const rows = await this.db.select(this.tableName, { id });
|
||||||
|
if (!rows.data || rows.data.length === 0) throw new Error('待看项目不存在');
|
||||||
|
const row = rows.data[0];
|
||||||
|
return {
|
||||||
|
id: row.id.toString(),
|
||||||
|
title: row.title,
|
||||||
|
url: row.url,
|
||||||
|
createdAt: (row.created_at instanceof Date) ? row.created_at.toISOString() : row.created_at,
|
||||||
|
updatedAt: (row.updated_at instanceof Date) ? row.updated_at.toISOString() : row.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await this.db.update(this.tableName, allowed, { id });
|
||||||
|
const rows = await this.db.select(this.tableName, { id });
|
||||||
|
const row = rows.data[0];
|
||||||
|
return {
|
||||||
|
id: row.id.toString(),
|
||||||
|
title: row.title,
|
||||||
|
url: row.url,
|
||||||
|
createdAt: (row.created_at instanceof Date) ? row.created_at.toISOString() : row.created_at,
|
||||||
|
updatedAt: (row.updated_at instanceof Date) ? row.updated_at.toISOString() : row.updated_at
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('更新待看项目失败(数据库):', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空表
|
||||||
|
*/
|
||||||
|
async clearAll() {
|
||||||
|
await this.db.query(`DELETE FROM ${this.tableName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量插入
|
||||||
|
*/
|
||||||
|
async batchInsert(items) {
|
||||||
|
const rows = items.map(i => {
|
||||||
|
const url = i.url;
|
||||||
|
const url_hash = url ? crypto.createHash('sha256').update(url).digest('hex') : null;
|
||||||
|
return {
|
||||||
|
id: i.id?.toString() || Date.now().toString(),
|
||||||
|
title: i.title || '待看页面',
|
||||||
|
url,
|
||||||
|
url_hash
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return await this.db.batchInsert(this.tableName, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已有URL集合
|
||||||
|
*/
|
||||||
|
async getExistingUrlSet() {
|
||||||
|
const result = await this.db.query(`SELECT url FROM ${this.tableName}`);
|
||||||
|
return new Set(result.data.map(r => r.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WatchlistDatabase;
|
||||||
+246
-4
@@ -2,10 +2,37 @@ const express = require('express');
|
|||||||
const WatchlistManager = require('../config/watchlist-manager');
|
const WatchlistManager = require('../config/watchlist-manager');
|
||||||
const ResponseUtil = require('../utils/response');
|
const ResponseUtil = require('../utils/response');
|
||||||
const { defaultLogger } = require('../utils/logger');
|
const { defaultLogger } = require('../utils/logger');
|
||||||
|
const CacheConfigManager = require('../config/cache-config');
|
||||||
|
const WatchlistDatabase = require('../database/watchlist-database');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const logger = defaultLogger.child('WatchlistRouter');
|
const logger = defaultLogger.child('WatchlistRouter');
|
||||||
const watchlistManager = new WatchlistManager();
|
const watchlistManager = new WatchlistManager();
|
||||||
|
const cacheConfigManager = new CacheConfigManager();
|
||||||
|
let watchlistDb = null; // 延迟初始化
|
||||||
|
|
||||||
|
// 获取当前存储模式
|
||||||
|
async function getStorageMode() {
|
||||||
|
try {
|
||||||
|
await cacheConfigManager.initialize();
|
||||||
|
const config = await cacheConfigManager.loadConfig();
|
||||||
|
return config.watchlist?.storageMode === 'database' ? 'database' : 'json';
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('读取待看名单存储模式失败,回退到json:', e.message);
|
||||||
|
return 'json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据库适配器(复用Registry连接)
|
||||||
|
async function getDbAdapter(req) {
|
||||||
|
const dbm = req.backend?.databaseManager;
|
||||||
|
if (!dbm) return null;
|
||||||
|
if (!watchlistDb) {
|
||||||
|
watchlistDb = new WatchlistDatabase(dbm);
|
||||||
|
await watchlistDb.init();
|
||||||
|
}
|
||||||
|
return watchlistDb;
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化待看名单
|
// 初始化待看名单
|
||||||
watchlistManager.initialize().catch(error => {
|
watchlistManager.initialize().catch(error => {
|
||||||
@@ -18,6 +45,12 @@ watchlistManager.initialize().catch(error => {
|
|||||||
*/
|
*/
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const mode = await getStorageMode();
|
||||||
|
if (mode === 'database') {
|
||||||
|
const db = await getDbAdapter(req);
|
||||||
|
const items = await db.getAllItems();
|
||||||
|
return res.json(ResponseUtil.success(items));
|
||||||
|
}
|
||||||
const items = await watchlistManager.getAllItems();
|
const items = await watchlistManager.getAllItems();
|
||||||
res.json(ResponseUtil.success(items));
|
res.json(ResponseUtil.success(items));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -120,10 +153,14 @@ router.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = {
|
const item = { url, title: defaultTitle };
|
||||||
url,
|
|
||||||
title: defaultTitle
|
const mode = await getStorageMode();
|
||||||
};
|
if (mode === 'database') {
|
||||||
|
const db = await getDbAdapter(req);
|
||||||
|
const items = await db.addItem(item);
|
||||||
|
return res.json(ResponseUtil.success(items));
|
||||||
|
}
|
||||||
|
|
||||||
const items = await watchlistManager.addItem(item);
|
const items = await watchlistManager.addItem(item);
|
||||||
res.json(ResponseUtil.success(items));
|
res.json(ResponseUtil.success(items));
|
||||||
@@ -133,6 +170,40 @@ router.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新待看名单存储配置(仅切换,不做迁移)
|
||||||
|
* PUT /api/watchlist/config
|
||||||
|
* body: { storageMode: 'json' | 'database' }
|
||||||
|
*
|
||||||
|
* 注意:必须放在 PUT '/:id' 之前,避免被动态路由拦截!
|
||||||
|
*/
|
||||||
|
router.put('/config', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { storageMode } = req.body;
|
||||||
|
if (!['json', 'database'].includes(storageMode)) {
|
||||||
|
return res.status(400).json(ResponseUtil.error('存储模式必须是 json 或 database'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
const current = await cacheConfigManager.loadConfig();
|
||||||
|
const updated = await cacheConfigManager.updateConfig({
|
||||||
|
...current,
|
||||||
|
watchlist: { ...(current.watchlist || {}), storageMode }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果切换到数据库模式,确保表已初始化(复用 Registry 的连接)
|
||||||
|
if (storageMode === 'database' && req.backend?.databaseManager) {
|
||||||
|
const db = await getDbAdapter(req);
|
||||||
|
await db.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(ResponseUtil.success({ storageMode: updated.watchlist?.storageMode || 'json' }));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('更新待看名单配置失败:', error);
|
||||||
|
res.status(500).json(ResponseUtil.error(error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新待看项目
|
* 更新待看项目
|
||||||
* PUT /api/watchlist/:id
|
* PUT /api/watchlist/:id
|
||||||
@@ -146,6 +217,13 @@ router.put('/:id', async (req, res) => {
|
|||||||
delete updates.id;
|
delete updates.id;
|
||||||
delete updates.createdAt;
|
delete updates.createdAt;
|
||||||
|
|
||||||
|
const mode = await getStorageMode();
|
||||||
|
if (mode === 'database') {
|
||||||
|
const db = await getDbAdapter(req);
|
||||||
|
const updatedItem = await db.updateItem(id, updates);
|
||||||
|
return res.json(ResponseUtil.success(updatedItem));
|
||||||
|
}
|
||||||
|
|
||||||
const updatedItem = await watchlistManager.updateItem(id, updates);
|
const updatedItem = await watchlistManager.updateItem(id, updates);
|
||||||
res.json(ResponseUtil.success(updatedItem));
|
res.json(ResponseUtil.success(updatedItem));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -165,6 +243,13 @@ router.put('/:id', async (req, res) => {
|
|||||||
router.delete('/:id', async (req, res) => {
|
router.delete('/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
const mode = await getStorageMode();
|
||||||
|
if (mode === 'database') {
|
||||||
|
const db = await getDbAdapter(req);
|
||||||
|
const items = await db.removeItem(id);
|
||||||
|
return res.json(ResponseUtil.success(items));
|
||||||
|
}
|
||||||
|
|
||||||
const items = await watchlistManager.removeItem(id);
|
const items = await watchlistManager.removeItem(id);
|
||||||
res.json(ResponseUtil.success(items));
|
res.json(ResponseUtil.success(items));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -177,4 +262,161 @@ router.delete('/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出待看名单
|
||||||
|
* GET /api/watchlist/export
|
||||||
|
*/
|
||||||
|
router.get('/export', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const mode = await getStorageMode();
|
||||||
|
let exportItems = [];
|
||||||
|
if (mode === 'database') {
|
||||||
|
const db = await getDbAdapter(req);
|
||||||
|
exportItems = await db.getAllItems();
|
||||||
|
} else {
|
||||||
|
const data = await watchlistManager.readData();
|
||||||
|
exportItems = Array.isArray(data.items) ? data.items : [];
|
||||||
|
}
|
||||||
|
// 组装导出结构,向后兼容
|
||||||
|
const exportData = {
|
||||||
|
version: '1.0',
|
||||||
|
exportTime: new Date().toISOString(),
|
||||||
|
items: exportItems
|
||||||
|
};
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="watchlist.json"');
|
||||||
|
res.json(ResponseUtil.success(exportData));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('导出待看名单失败:', error);
|
||||||
|
res.status(500).json(ResponseUtil.error(error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入待看名单
|
||||||
|
* POST /api/watchlist/import
|
||||||
|
* body: { watchlistData: { items: [...] }, importMode: 'merge' | 'overwrite' }
|
||||||
|
*/
|
||||||
|
router.post('/import', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { watchlistData, importMode = 'merge' } = req.body;
|
||||||
|
|
||||||
|
if (!watchlistData || !Array.isArray(watchlistData.items)) {
|
||||||
|
return res.status(400).json(ResponseUtil.error('缺少有效的待看名单数据'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeItem = (item) => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
id: item.id?.toString() || Date.now().toString(),
|
||||||
|
title: item.title || '待看页面',
|
||||||
|
url: item.url,
|
||||||
|
createdAt: item.createdAt || now,
|
||||||
|
updatedAt: item.updatedAt || now
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const importItems = watchlistData.items
|
||||||
|
.filter(i => i && i.url)
|
||||||
|
.map(normalizeItem);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let skipCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
const mode = await getStorageMode();
|
||||||
|
let updatedItems = [];
|
||||||
|
|
||||||
|
if (mode === 'database') {
|
||||||
|
const db = await getDbAdapter(req);
|
||||||
|
if (importMode === 'overwrite') {
|
||||||
|
// 覆盖:清空后批量插入
|
||||||
|
const before = await db.getAllItems();
|
||||||
|
deletedCount = before.length;
|
||||||
|
await db.clearAll();
|
||||||
|
await db.batchInsert(importItems);
|
||||||
|
successCount = importItems.length;
|
||||||
|
} else {
|
||||||
|
// 合并:按URL去重,仅新增不存在的
|
||||||
|
const existingUrlSet = await db.getExistingUrlSet();
|
||||||
|
const toInsert = [];
|
||||||
|
for (const item of importItems) {
|
||||||
|
try {
|
||||||
|
if (existingUrlSet.has(item.url)) {
|
||||||
|
skipCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toInsert.push(item);
|
||||||
|
successCount++;
|
||||||
|
} catch (e) {
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.batchInsert(toInsert);
|
||||||
|
}
|
||||||
|
updatedItems = await db.getAllItems();
|
||||||
|
} else {
|
||||||
|
const currentData = await watchlistManager.readData();
|
||||||
|
const existingItems = Array.isArray(currentData.items) ? currentData.items : [];
|
||||||
|
if (importMode === 'overwrite') {
|
||||||
|
// 覆盖:直接保存导入数据为新清单
|
||||||
|
deletedCount = existingItems.length;
|
||||||
|
const newData = { items: importItems, lastUpdated: new Date().toISOString() };
|
||||||
|
await watchlistManager.saveData(newData);
|
||||||
|
successCount = importItems.length;
|
||||||
|
} else {
|
||||||
|
// 合并:按URL去重,仅新增不存在的
|
||||||
|
const existingUrlSet = new Set(existingItems.map(i => i.url));
|
||||||
|
const merged = [...existingItems];
|
||||||
|
for (const item of importItems) {
|
||||||
|
try {
|
||||||
|
if (existingUrlSet.has(item.url)) {
|
||||||
|
skipCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
merged.push(item);
|
||||||
|
existingUrlSet.add(item.url);
|
||||||
|
successCount++;
|
||||||
|
} catch (e) {
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await watchlistManager.saveData({ items: merged, lastUpdated: new Date().toISOString() });
|
||||||
|
}
|
||||||
|
updatedItems = await watchlistManager.getAllItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = importMode === 'overwrite'
|
||||||
|
? `覆盖导入完成:删除 ${deletedCount} 项,成功添加 ${successCount} 项,失败 ${errorCount} 项`
|
||||||
|
: `重合导入完成:成功 ${successCount} 项,跳过 ${skipCount} 项,失败 ${errorCount} 项`;
|
||||||
|
|
||||||
|
res.json(ResponseUtil.success({
|
||||||
|
message,
|
||||||
|
stats: { successCount, skipCount, errorCount, deletedCount },
|
||||||
|
items: updatedItems
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('导入待看名单失败:', error);
|
||||||
|
res.status(500).json(ResponseUtil.error(error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待看名单存储配置
|
||||||
|
* GET /api/watchlist/config
|
||||||
|
*/
|
||||||
|
router.get('/config', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const mode = await getStorageMode();
|
||||||
|
res.json(ResponseUtil.success({ storageMode: mode }));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('获取待看名单配置失败:', error);
|
||||||
|
res.status(500).json(ResponseUtil.error(error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -63,6 +63,7 @@ const ModuleColors = {
|
|||||||
'AbortControllerManager': '\x1b[94m', // 亮蓝色
|
'AbortControllerManager': '\x1b[94m', // 亮蓝色
|
||||||
'DatabaseManager': '\x1b[95m', // 亮紫色
|
'DatabaseManager': '\x1b[95m', // 亮紫色
|
||||||
'RegistrySchema': '\x1b[94m', // 亮蓝色
|
'RegistrySchema': '\x1b[94m', // 亮蓝色
|
||||||
|
'WatchlistDatabase': '\x1b[94m', // 亮蓝色
|
||||||
'RegistryDatabase': '\x1b[94m', // 亮蓝色
|
'RegistryDatabase': '\x1b[94m', // 亮蓝色
|
||||||
'Default': '\x1b[39m' // 默认颜色
|
'Default': '\x1b[39m' // 默认颜色
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"server": {
|
"server": {
|
||||||
"port": 3001,
|
"port": 3000,
|
||||||
"autoOpenBrowser": false
|
"autoOpenBrowser": false
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL=http://localhost:3000
|
||||||
@@ -147,10 +147,10 @@ const filteredAndSortedItems = computed(() => {
|
|||||||
otherItems = filteredItems;
|
otherItems = filteredItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对其他项目进行排序
|
// 对其他项目进行排序(按更新时间排序)
|
||||||
otherItems.sort((a, b) => {
|
otherItems.sort((a, b) => {
|
||||||
const dateA = new Date(a.createdAt).getTime();
|
const dateA = new Date(a.updatedAt || a.createdAt).getTime();
|
||||||
const dateB = new Date(b.createdAt).getTime();
|
const dateB = new Date(b.updatedAt || b.createdAt).getTime();
|
||||||
return sortOrder.value === 'desc' ? dateB - dateA : dateA - dateB;
|
return sortOrder.value === 'desc' ? dateB - dateA : dateA - dateB;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>URL或路由路径</label>
|
<label>URL或路由路径</label>
|
||||||
<input :value="url" @input="$emit('update:url', ($event.target as HTMLInputElement).value)" type="text"
|
<input :value="url" @input="$emit('update:url', ($event.target as HTMLInputElement).value)" type="text"
|
||||||
class="form-input" placeholder="例如: /artist/12345?page=2 或 http://localhost:3001/artwork/98765"
|
class="form-input" placeholder="例如: /artist/12345?page=2 或 http://localhost:3000/artwork/98765"
|
||||||
@keyup.enter="handleSave">
|
@keyup.enter="handleSave">
|
||||||
<small class="form-help">
|
<small class="form-help">
|
||||||
支持完整URL或相对路径,如:/artist/12345、/search?keyword=插画 等
|
支持完整URL或相对路径,如:/artist/12345、/search?keyword=插画 等
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
<textarea :value="batchUrls"
|
<textarea :value="batchUrls"
|
||||||
@input="$emit('update:batchUrls', ($event.target as HTMLTextAreaElement).value)" class="form-textarea"
|
@input="$emit('update:batchUrls', ($event.target as HTMLTextAreaElement).value)" class="form-textarea"
|
||||||
rows="8" placeholder="请输入多个URL,每行一个,例如:
|
rows="8" placeholder="请输入多个URL,每行一个,例如:
|
||||||
http://localhost:3001/artist/72143697
|
http://localhost:3000/artist/72143697
|
||||||
http://localhost:3001/artist/103047332
|
http://localhost:3000/artist/103047332
|
||||||
/artist/113088709
|
/artist/113088709
|
||||||
/artwork/98765?page=2
|
/artwork/98765?page=2
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,27 @@
|
|||||||
@update:search-query="$emit('update:searchQuery', $event)" @clear-search="$emit('clearSearch')"
|
@update:search-query="$emit('update:searchQuery', $event)" @clear-search="$emit('clearSearch')"
|
||||||
@toggle-sort="$emit('toggleSort')" />
|
@toggle-sort="$emit('toggleSort')" />
|
||||||
|
|
||||||
|
<!-- 存储模式设置 -->
|
||||||
|
<div class="storage-config">
|
||||||
|
<div class="storage-row">
|
||||||
|
<span class="storage-label">存储模式:</span>
|
||||||
|
<select v-model="selectedStorageMode" class="storage-select">
|
||||||
|
<option value="json">JSON文件存储</option>
|
||||||
|
<option value="database">MySQL数据库存储</option>
|
||||||
|
</select>
|
||||||
|
<span class="config-indicator" :class="{ unsaved: hasStorageModeChanges, saved: !hasStorageModeChanges }">
|
||||||
|
<SvgIcon :name="hasStorageModeChanges ? 'warning' : 'check'" class="indicator-icon" />
|
||||||
|
{{ hasStorageModeChanges ? '配置已修改' : `当前模式: ${storageModeText}` }}
|
||||||
|
</span>
|
||||||
|
<button class="save-config-btn" :disabled="configLoading || !hasStorageModeChanges" @click="saveStorageMode">
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
<button class="reset-config-btn" :disabled="configLoading || !hasStorageModeChanges" @click="resetStorageMode">
|
||||||
|
重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<WatchlistContent :loading="loading" :error="error" :items="items" :filtered-items="filteredItems"
|
<WatchlistContent :loading="loading" :error="error" :items="items" :filtered-items="filteredItems"
|
||||||
:search-query="searchQuery" :is-current-url="isCurrentUrl" :is-duplicate-author="isDuplicateAuthor"
|
:search-query="searchQuery" :is-current-url="isCurrentUrl" :is-duplicate-author="isDuplicateAuthor"
|
||||||
@@ -58,7 +79,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useWatchlistStore } from '@/stores/watchlist';
|
import { useWatchlistStore } from '@/stores/watchlist';
|
||||||
import WatchlistControls from './WatchlistControls.vue';
|
import WatchlistControls from './WatchlistControls.vue';
|
||||||
import WatchlistContent from './WatchlistContent.vue';
|
import WatchlistContent from './WatchlistContent.vue';
|
||||||
@@ -143,6 +164,31 @@ const handleFileImport = async (event: Event) => {
|
|||||||
target.value = '';
|
target.value = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 存储模式相关逻辑
|
||||||
|
const selectedStorageMode = ref<'json' | 'database'>('json');
|
||||||
|
const configLoading = computed(() => watchlistStore.configLoading);
|
||||||
|
const hasStorageModeChanges = computed(() => selectedStorageMode.value !== watchlistStore.storageMode);
|
||||||
|
const storageModeText = computed(() => watchlistStore.storageMode === 'database' ? 'MySQL数据库存储' : 'JSON文件存储');
|
||||||
|
|
||||||
|
const saveStorageMode = async () => {
|
||||||
|
if (configLoading.value || !hasStorageModeChanges.value) return;
|
||||||
|
const ok = await watchlistStore.saveStorageModeConfig(selectedStorageMode.value);
|
||||||
|
if (!ok) return;
|
||||||
|
// 成功后同步选择值并刷新列表与配置
|
||||||
|
selectedStorageMode.value = watchlistStore.storageMode;
|
||||||
|
await watchlistStore.fetchConfig();
|
||||||
|
await watchlistStore.fetchItems();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetStorageMode = () => {
|
||||||
|
selectedStorageMode.value = watchlistStore.storageMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await watchlistStore.fetchConfig();
|
||||||
|
selectedStorageMode.value = watchlistStore.storageMode;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -150,7 +196,7 @@ const handleFileImport = async (event: Event) => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 400px;
|
width: 570px;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
background: var(--color-bg-primary);
|
background: var(--color-bg-primary);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
@@ -198,6 +244,65 @@ const handleFileImport = async (event: Event) => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.storage-config {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
gap: 8px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.config-indicator.saved {
|
||||||
|
color: var(--color-success, #16a34a);
|
||||||
|
}
|
||||||
|
.config-indicator.unsaved {
|
||||||
|
color: var(--color-warning, #ca8a04);
|
||||||
|
}
|
||||||
|
.indicator-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-config-btn,
|
||||||
|
.reset-config-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.save-config-btn:disabled,
|
||||||
|
.reset-config-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.item-count-text {
|
.item-count-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export interface UpdateWatchlistItemParams {
|
|||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 存储配置接口
|
||||||
|
export interface WatchlistConfig {
|
||||||
|
storageMode: 'json' | 'database';
|
||||||
|
}
|
||||||
|
|
||||||
class WatchlistService {
|
class WatchlistService {
|
||||||
/**
|
/**
|
||||||
* 获取所有待看项目
|
* 获取所有待看项目
|
||||||
@@ -50,6 +55,44 @@ class WatchlistService {
|
|||||||
async deleteItem(id: string): Promise<ApiResponse<WatchlistItem[]>> {
|
async deleteItem(id: string): Promise<ApiResponse<WatchlistItem[]>> {
|
||||||
return apiService.delete<WatchlistItem[]>(`/api/watchlist/${id}`);
|
return apiService.delete<WatchlistItem[]>(`/api/watchlist/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出待看名单(后端)
|
||||||
|
*/
|
||||||
|
async export(): Promise<ApiResponse<{ version: string; exportTime: string; items: WatchlistItem[] }>> {
|
||||||
|
return apiService.get<{ version: string; exportTime: string; items: WatchlistItem[] }>(
|
||||||
|
'/api/watchlist/export'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入待看名单(后端)
|
||||||
|
*/
|
||||||
|
async import(data: any, importMode: 'merge' | 'overwrite' = 'merge'): Promise<
|
||||||
|
ApiResponse<{ message: string; stats: { successCount: number; skipCount: number; errorCount: number; deletedCount: number }; items: WatchlistItem[] }>
|
||||||
|
> {
|
||||||
|
return apiService.post(
|
||||||
|
'/api/watchlist/import',
|
||||||
|
{
|
||||||
|
watchlistData: data,
|
||||||
|
importMode
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待看名单存储配置
|
||||||
|
*/
|
||||||
|
async getConfig(): Promise<ApiResponse<WatchlistConfig>> {
|
||||||
|
return apiService.get<WatchlistConfig>('/api/watchlist/config');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新待看名单存储配置
|
||||||
|
*/
|
||||||
|
async updateConfig(storageMode: 'json' | 'database'): Promise<ApiResponse<WatchlistConfig>> {
|
||||||
|
return apiService.put<WatchlistConfig>('/api/watchlist/config', { storageMode });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const watchlistService = new WatchlistService();
|
export const watchlistService = new WatchlistService();
|
||||||
|
|||||||
+84
-89
@@ -1,12 +1,14 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { watchlistService, type WatchlistItem, type AddWatchlistItemParams, type UpdateWatchlistItemParams } from '@/services/watchlist';
|
import { watchlistService, type WatchlistItem, type AddWatchlistItemParams, type UpdateWatchlistItemParams, type WatchlistConfig } from '@/services/watchlist';
|
||||||
|
|
||||||
export const useWatchlistStore = defineStore('watchlist', () => {
|
export const useWatchlistStore = defineStore('watchlist', () => {
|
||||||
// 状态
|
// 状态
|
||||||
const items = ref<WatchlistItem[]>([]);
|
const items = ref<WatchlistItem[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
const configLoading = ref(false);
|
||||||
|
const storageMode = ref<'json' | 'database'>('json');
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const itemCount = computed(() => items.value.length);
|
const itemCount = computed(() => items.value.length);
|
||||||
@@ -37,6 +39,44 @@ export const useWatchlistStore = defineStore('watchlist', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取存储配置
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
try {
|
||||||
|
configLoading.value = true;
|
||||||
|
const response = await watchlistService.getConfig();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
storageMode.value = (response.data.storageMode ?? 'json') as 'json' | 'database';
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '获取存储配置失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, '获取存储配置失败');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
configLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存存储模式
|
||||||
|
const saveStorageModeConfig = async (mode: 'json' | 'database') => {
|
||||||
|
try {
|
||||||
|
configLoading.value = true;
|
||||||
|
const response = await watchlistService.updateConfig(mode);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
storageMode.value = response.data.storageMode;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '更新存储配置失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, '更新存储配置失败');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
configLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 添加待看项目
|
// 添加待看项目
|
||||||
const addItem = async (params: AddWatchlistItemParams) => {
|
const addItem = async (params: AddWatchlistItemParams) => {
|
||||||
try {
|
try {
|
||||||
@@ -184,106 +224,57 @@ export const useWatchlistStore = defineStore('watchlist', () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出待看名单数据
|
// 导出待看名单数据(通过后端)
|
||||||
const exportWatchlist = () => {
|
const exportWatchlist = async () => {
|
||||||
const exportData = {
|
try {
|
||||||
version: '1.0',
|
loading.value = true;
|
||||||
exportTime: new Date().toISOString(),
|
error.value = null;
|
||||||
items: items.value.map(item => ({
|
const response = await watchlistService.export();
|
||||||
id: item.id,
|
if (response.success && response.data) {
|
||||||
title: item.title,
|
const dataStr = JSON.stringify(response.data, null, 2);
|
||||||
url: item.url,
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||||
createdAt: item.createdAt,
|
const link = document.createElement('a');
|
||||||
updatedAt: item.updatedAt
|
link.href = URL.createObjectURL(dataBlob);
|
||||||
}))
|
link.download = `watchlist-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
};
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
const dataStr = JSON.stringify(exportData, null, 2);
|
document.body.removeChild(link);
|
||||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
URL.revokeObjectURL(link.href);
|
||||||
|
return { success: true };
|
||||||
const link = document.createElement('a');
|
} else {
|
||||||
link.href = URL.createObjectURL(dataBlob);
|
throw new Error(response.error || '导出待看名单失败');
|
||||||
link.download = `watchlist-${new Date().toISOString().split('T')[0]}.json`;
|
}
|
||||||
document.body.appendChild(link);
|
} catch (err) {
|
||||||
link.click();
|
handleError(err, '导出待看名单失败');
|
||||||
document.body.removeChild(link);
|
return { success: false };
|
||||||
URL.revokeObjectURL(link.href);
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导入待看名单数据
|
// 导入待看名单数据(通过后端)
|
||||||
const importWatchlist = async (file: File, importMode: 'merge' | 'overwrite' = 'merge') => {
|
const importWatchlist = async (file: File, importMode: 'merge' | 'overwrite' = 'merge') => {
|
||||||
try {
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const importData = JSON.parse(text);
|
const importData = JSON.parse(text);
|
||||||
|
|
||||||
// 验证数据格式
|
|
||||||
if (!importData.items || !Array.isArray(importData.items)) {
|
if (!importData.items || !Array.isArray(importData.items)) {
|
||||||
throw new Error('无效的导入文件格式');
|
throw new Error('无效的导入文件格式');
|
||||||
}
|
}
|
||||||
|
const response = await watchlistService.import(importData, importMode);
|
||||||
// 统计导入结果
|
if (response.success && response.data) {
|
||||||
let successCount = 0;
|
// 刷新本地列表
|
||||||
let skipCount = 0;
|
items.value = response.data.items || items.value;
|
||||||
let errorCount = 0;
|
return { success: true, message: response.data.message, stats: response.data.stats };
|
||||||
let deletedCount = 0;
|
|
||||||
|
|
||||||
// 如果是覆盖模式,先删除所有现有项目
|
|
||||||
if (importMode === 'overwrite') {
|
|
||||||
const allItems = items.value;
|
|
||||||
for (const item of allItems) {
|
|
||||||
try {
|
|
||||||
await deleteItem(item.id);
|
|
||||||
deletedCount++;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('删除项目失败:', item, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of importData.items) {
|
|
||||||
try {
|
|
||||||
// 在重合模式下检查是否已存在
|
|
||||||
if (importMode === 'merge' && hasUrl(item.url)) {
|
|
||||||
skipCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加项目
|
|
||||||
const success = await addItem({
|
|
||||||
url: item.url,
|
|
||||||
title: item.title
|
|
||||||
});
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
successCount++;
|
|
||||||
} else {
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('导入项目失败:', item, err);
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = '';
|
|
||||||
if (importMode === 'overwrite') {
|
|
||||||
message = `覆盖导入完成:删除 ${deletedCount} 项,成功添加 ${successCount} 项,失败 ${errorCount} 项`;
|
|
||||||
} else {
|
} else {
|
||||||
message = `重合导入完成:成功 ${successCount} 项,跳过 ${skipCount} 项,失败 ${errorCount} 项`;
|
throw new Error(response.error || '导入待看名单失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message,
|
|
||||||
stats: { successCount, skipCount, errorCount, deletedCount }
|
|
||||||
};
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('导入失败:', err);
|
handleError(err, '导入待看名单失败');
|
||||||
return {
|
return { success: false, message: error.value || '导入待看名单失败', stats: { successCount: 0, skipCount: 0, errorCount: 0, deletedCount: 0 } };
|
||||||
success: false,
|
} finally {
|
||||||
message: err instanceof Error ? err.message : '导入失败',
|
loading.value = false;
|
||||||
stats: { successCount: 0, skipCount: 0, errorCount: 0, deletedCount: 0 }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -292,11 +283,15 @@ export const useWatchlistStore = defineStore('watchlist', () => {
|
|||||||
items,
|
items,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
configLoading,
|
||||||
|
storageMode,
|
||||||
// 计算属性
|
// 计算属性
|
||||||
itemCount,
|
itemCount,
|
||||||
hasItems,
|
hasItems,
|
||||||
// 方法
|
// 方法
|
||||||
fetchItems,
|
fetchItems,
|
||||||
|
fetchConfig,
|
||||||
|
saveStorageModeConfig,
|
||||||
addItem,
|
addItem,
|
||||||
updateItem,
|
updateItem,
|
||||||
deleteItem,
|
deleteItem,
|
||||||
|
|||||||
Reference in New Issue
Block a user