增加图片缓存,页数跳转,搜索页面返回query
This commit is contained in:
@@ -89,6 +89,12 @@ backend/
|
|||||||
|
|
||||||
- `GET /api/proxy/image` - 图片代理服务
|
- `GET /api/proxy/image` - 图片代理服务
|
||||||
- 参数: `url` (图片URL)
|
- 参数: `url` (图片URL)
|
||||||
|
- `GET /api/proxy/cache/stats` - 获取图片缓存统计信息
|
||||||
|
- `DELETE /api/proxy/cache` - 清理所有图片缓存
|
||||||
|
- `DELETE /api/proxy/cache/expired` - 清理过期图片缓存
|
||||||
|
- `GET /api/proxy/cache/config` - 获取缓存配置
|
||||||
|
- `PUT /api/proxy/cache/config` - 更新缓存配置
|
||||||
|
- `POST /api/proxy/cache/config/reset` - 重置缓存配置为默认值
|
||||||
|
|
||||||
### 仓库管理相关
|
### 仓库管理相关
|
||||||
|
|
||||||
@@ -158,6 +164,7 @@ backend/
|
|||||||
- **artist.js**: 作者服务,处理作者API调用
|
- **artist.js**: 作者服务,处理作者API调用
|
||||||
- **download.js**: 下载服务,处理文件下载
|
- **download.js**: 下载服务,处理文件下载
|
||||||
- **repository.js**: 仓库管理服务,处理文件管理和配置
|
- **repository.js**: 仓库管理服务,处理文件管理和配置
|
||||||
|
- **image-cache.js**: 图片缓存服务,管理图片代理缓存
|
||||||
|
|
||||||
### 工具类
|
### 工具类
|
||||||
|
|
||||||
@@ -204,6 +211,13 @@ backend/
|
|||||||
- 磁盘使用情况监控
|
- 磁盘使用情况监控
|
||||||
- 作品删除管理
|
- 作品删除管理
|
||||||
|
|
||||||
|
### 7. 图片缓存管理
|
||||||
|
- 图片代理缓存功能
|
||||||
|
- 自动缓存过期清理
|
||||||
|
- 缓存大小限制管理
|
||||||
|
- 缓存统计信息查看
|
||||||
|
- 手动缓存清理功能
|
||||||
|
|
||||||
## 🔒 安全特性
|
## 🔒 安全特性
|
||||||
|
|
||||||
- 统一的错误处理
|
- 统一的错误处理
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存配置管理器
|
||||||
|
* 负责管理图片缓存的配置选项
|
||||||
|
*/
|
||||||
|
class CacheConfigManager {
|
||||||
|
constructor() {
|
||||||
|
// 检测是否在pkg打包环境中运行
|
||||||
|
const isPkg = process.pkg !== undefined;
|
||||||
|
|
||||||
|
if (isPkg) {
|
||||||
|
// 在打包环境中,使用可执行文件所在目录
|
||||||
|
this.configPath = path.join(process.cwd(), 'data', 'cache-config.json');
|
||||||
|
} else {
|
||||||
|
// 在开发环境中,使用相对路径
|
||||||
|
this.configPath = path.join(__dirname, 'cache-config.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保路径是绝对路径
|
||||||
|
this.configPath = path.resolve(this.configPath);
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
this.defaultConfig = {
|
||||||
|
// 缓存配置
|
||||||
|
maxAge: 24 * 60 * 60 * 1000, // 24小时缓存
|
||||||
|
maxSize: 100 * 1024 * 1024, // 100MB最大缓存大小
|
||||||
|
cleanupInterval: 60 * 60 * 1000, // 1小时清理一次
|
||||||
|
|
||||||
|
// 缓存启用状态
|
||||||
|
enabled: true,
|
||||||
|
|
||||||
|
// 代理配置
|
||||||
|
proxy: {
|
||||||
|
enabled: true,
|
||||||
|
timeout: 30000, // 30秒超时
|
||||||
|
retryCount: 3, // 重试次数
|
||||||
|
retryDelay: 1000, // 重试延迟(毫秒)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 文件类型过滤
|
||||||
|
allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'],
|
||||||
|
|
||||||
|
// 最后更新时间
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确保配置目录存在
|
||||||
|
this.ensureConfigDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保配置目录存在
|
||||||
|
*/
|
||||||
|
ensureConfigDir() {
|
||||||
|
try {
|
||||||
|
const configDir = path.dirname(this.configPath);
|
||||||
|
if (!require('fs').existsSync(configDir)) {
|
||||||
|
require('fs').mkdirSync(configDir, { recursive: true });
|
||||||
|
console.log('缓存配置目录创建成功:', configDir);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建缓存配置目录失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置文件
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
try {
|
||||||
|
// 检查配置文件是否存在
|
||||||
|
await fs.access(this.configPath);
|
||||||
|
console.log('缓存配置文件已存在');
|
||||||
|
} catch (error) {
|
||||||
|
// 配置文件不存在,创建默认配置
|
||||||
|
console.log('创建默认缓存配置文件...');
|
||||||
|
await this.createDefaultConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认配置文件
|
||||||
|
*/
|
||||||
|
async createDefaultConfig() {
|
||||||
|
try {
|
||||||
|
const configContent = JSON.stringify(this.defaultConfig, null, 2);
|
||||||
|
await fs.writeFile(this.configPath, configContent, 'utf8');
|
||||||
|
console.log('默认缓存配置文件创建成功:', this.configPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建默认缓存配置文件失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载配置
|
||||||
|
*/
|
||||||
|
async loadConfig() {
|
||||||
|
try {
|
||||||
|
const configContent = await fs.readFile(this.configPath, 'utf8');
|
||||||
|
const config = JSON.parse(configContent);
|
||||||
|
|
||||||
|
// 合并默认配置,确保所有字段都存在
|
||||||
|
return { ...this.defaultConfig, ...config };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载缓存配置失败:', error);
|
||||||
|
return this.defaultConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存配置
|
||||||
|
*/
|
||||||
|
async saveConfig(config) {
|
||||||
|
try {
|
||||||
|
// 更新最后修改时间
|
||||||
|
config.lastUpdated = new Date().toISOString();
|
||||||
|
|
||||||
|
const configContent = JSON.stringify(config, null, 2);
|
||||||
|
await fs.writeFile(this.configPath, configContent, 'utf8');
|
||||||
|
console.log('缓存配置保存成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存缓存配置失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新配置
|
||||||
|
*/
|
||||||
|
async updateConfig(updates) {
|
||||||
|
try {
|
||||||
|
const currentConfig = await this.loadConfig();
|
||||||
|
const newConfig = { ...currentConfig, ...updates };
|
||||||
|
await this.saveConfig(newConfig);
|
||||||
|
return newConfig;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新缓存配置失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置为默认配置
|
||||||
|
*/
|
||||||
|
async resetToDefault() {
|
||||||
|
try {
|
||||||
|
await this.saveConfig(this.defaultConfig);
|
||||||
|
console.log('缓存配置已重置为默认值');
|
||||||
|
return this.defaultConfig;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置缓存配置失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证配置
|
||||||
|
*/
|
||||||
|
validateConfig(config) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (config.maxAge < 0) {
|
||||||
|
errors.push('maxAge 必须大于等于 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.maxSize < 0) {
|
||||||
|
errors.push('maxSize 必须大于等于 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.cleanupInterval < 0) {
|
||||||
|
errors.push('cleanupInterval 必须大于等于 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.proxy.timeout < 0) {
|
||||||
|
errors.push('proxy.timeout 必须大于等于 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.proxy.retryCount < 0) {
|
||||||
|
errors.push('proxy.retryCount 必须大于等于 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.proxy.retryDelay < 0) {
|
||||||
|
errors.push('proxy.retryDelay 必须大于等于 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置路径
|
||||||
|
*/
|
||||||
|
getConfigPath() {
|
||||||
|
return this.configPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CacheConfigManager;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"maxAge": 86400000,
|
||||||
|
"maxSize": 104857600,
|
||||||
|
"cleanupInterval": 3600000,
|
||||||
|
"enabled": true,
|
||||||
|
"proxy": {
|
||||||
|
"enabled": true,
|
||||||
|
"timeout": 30000,
|
||||||
|
"retryCount": 3,
|
||||||
|
"retryDelay": 1000
|
||||||
|
},
|
||||||
|
"allowedExtensions": [
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".png",
|
||||||
|
".gif",
|
||||||
|
".webp",
|
||||||
|
".bmp"
|
||||||
|
],
|
||||||
|
"lastUpdated": "2025-08-25T06:14:10.559Z"
|
||||||
|
}
|
||||||
+149
-15
@@ -1,6 +1,9 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const axios = require('axios');
|
const ImageCacheService = require('../services/image-cache');
|
||||||
|
|
||||||
|
// 创建缓存服务实例
|
||||||
|
const imageCache = new ImageCacheService();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片代理
|
* 图片代理
|
||||||
@@ -17,28 +20,22 @@ router.get('/image', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios({
|
const decodedUrl = decodeURIComponent(url);
|
||||||
method: 'GET',
|
|
||||||
url: decodeURIComponent(url),
|
// 使用缓存服务获取图片
|
||||||
responseType: 'stream',
|
const imageData = await imageCache.getImage(decodedUrl);
|
||||||
headers: {
|
|
||||||
'Referer': 'https://www.pixiv.net/',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
||||||
},
|
|
||||||
timeout: 30000
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置响应头
|
// 设置响应头
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': response.headers['content-type'],
|
'Content-Type': getContentType(decodedUrl),
|
||||||
'Cache-Control': 'public, max-age=3600', // 缓存1小时
|
'Cache-Control': 'public, max-age=3600', // 浏览器缓存1小时
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'GET',
|
'Access-Control-Allow-Methods': 'GET',
|
||||||
'Access-Control-Allow-Headers': 'Content-Type'
|
'Access-Control-Allow-Headers': 'Content-Type'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 流式传输图片数据
|
// 发送图片数据
|
||||||
response.data.pipe(res);
|
res.send(imageData);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Image proxy error:', error.message);
|
console.error('Image proxy error:', error.message);
|
||||||
@@ -49,4 +46,141 @@ router.get('/image', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理 - 获取缓存统计信息
|
||||||
|
* GET /api/proxy/cache/stats
|
||||||
|
*/
|
||||||
|
router.get('/cache/stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stats = await imageCache.getCacheStats();
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: stats
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理 - 清理所有缓存
|
||||||
|
* DELETE /api/proxy/cache
|
||||||
|
*/
|
||||||
|
router.delete('/cache', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await imageCache.clearAllCache();
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '所有缓存已清理'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理 - 清理过期缓存
|
||||||
|
* DELETE /api/proxy/cache/expired
|
||||||
|
*/
|
||||||
|
router.delete('/cache/expired', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await imageCache.cleanupExpiredCache();
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '过期缓存已清理'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理 - 获取缓存配置
|
||||||
|
* GET /api/proxy/cache/config
|
||||||
|
*/
|
||||||
|
router.get('/cache/config', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const config = await imageCache.getConfig();
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: config
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理 - 更新缓存配置
|
||||||
|
* PUT /api/proxy/cache/config
|
||||||
|
*/
|
||||||
|
router.put('/cache/config', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const updates = req.body;
|
||||||
|
const config = await imageCache.updateConfig(updates);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: config,
|
||||||
|
message: '缓存配置已更新'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理 - 重置缓存配置
|
||||||
|
* POST /api/proxy/cache/config/reset
|
||||||
|
*/
|
||||||
|
router.post('/cache/config/reset', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const config = await imageCache.resetConfig();
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: config,
|
||||||
|
message: '缓存配置已重置为默认值'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件内容类型
|
||||||
|
* @param {string} url 图片URL
|
||||||
|
* @returns {string} 内容类型
|
||||||
|
*/
|
||||||
|
function getContentType(url) {
|
||||||
|
const ext = url.split('.').pop()?.toLowerCase();
|
||||||
|
const contentTypeMap = {
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'png': 'image/png',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'webp': 'image/webp',
|
||||||
|
'bmp': 'image/bmp',
|
||||||
|
'svg': 'image/svg+xml'
|
||||||
|
};
|
||||||
|
|
||||||
|
return contentTypeMap[ext] || 'image/jpeg';
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const axios = require('axios');
|
||||||
|
const CacheConfigManager = require('../config/cache-config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片缓存服务
|
||||||
|
* 负责管理图片代理的缓存功能
|
||||||
|
*/
|
||||||
|
class ImageCacheService {
|
||||||
|
constructor() {
|
||||||
|
// 检测是否在pkg打包环境中运行
|
||||||
|
const isPkg = process.pkg !== undefined;
|
||||||
|
|
||||||
|
if (isPkg) {
|
||||||
|
// 在打包环境中,使用可执行文件所在目录
|
||||||
|
this.cacheDir = path.join(process.cwd(), 'data', 'image-cache');
|
||||||
|
} else {
|
||||||
|
// 在开发环境中,使用相对路径
|
||||||
|
this.cacheDir = path.join(__dirname, '..', 'data', 'image-cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保路径是绝对路径
|
||||||
|
this.cacheDir = path.resolve(this.cacheDir);
|
||||||
|
|
||||||
|
// 创建配置管理器
|
||||||
|
this.configManager = new CacheConfigManager();
|
||||||
|
|
||||||
|
// 默认缓存配置
|
||||||
|
this.config = {
|
||||||
|
maxAge: 24 * 60 * 60 * 1000, // 24小时缓存
|
||||||
|
maxSize: 100 * 1024 * 1024, // 100MB最大缓存大小
|
||||||
|
cleanupInterval: 60 * 60 * 1000, // 1小时清理一次
|
||||||
|
enabled: true,
|
||||||
|
proxy: {
|
||||||
|
enabled: true,
|
||||||
|
timeout: 30000,
|
||||||
|
retryCount: 3,
|
||||||
|
retryDelay: 1000,
|
||||||
|
},
|
||||||
|
allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化配置
|
||||||
|
this.initializeConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置
|
||||||
|
*/
|
||||||
|
async initializeConfig() {
|
||||||
|
try {
|
||||||
|
await this.configManager.initialize();
|
||||||
|
const config = await this.configManager.loadConfig();
|
||||||
|
this.config = { ...this.config, ...config };
|
||||||
|
|
||||||
|
// 确保缓存目录存在
|
||||||
|
await this.ensureCacheDir();
|
||||||
|
|
||||||
|
// 启动定期清理任务
|
||||||
|
this.startCleanupTask();
|
||||||
|
|
||||||
|
console.log('图片缓存服务初始化完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('图片缓存服务初始化失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保缓存目录存在
|
||||||
|
*/
|
||||||
|
async ensureCacheDir() {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(this.cacheDir, { recursive: true });
|
||||||
|
console.log('图片缓存目录创建成功:', this.cacheDir);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建图片缓存目录失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成缓存文件名
|
||||||
|
* @param {string} url 原始图片URL
|
||||||
|
* @returns {string} 缓存文件名
|
||||||
|
*/
|
||||||
|
generateCacheKey(url) {
|
||||||
|
const hash = crypto.createHash('md5').update(url).digest('hex');
|
||||||
|
const ext = this.getFileExtension(url);
|
||||||
|
return `${hash}${ext}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名
|
||||||
|
* @param {string} url 图片URL
|
||||||
|
* @returns {string} 文件扩展名
|
||||||
|
*/
|
||||||
|
getFileExtension(url) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const pathname = urlObj.pathname;
|
||||||
|
const ext = path.extname(pathname);
|
||||||
|
return ext || '.jpg'; // 默认使用.jpg
|
||||||
|
} catch (error) {
|
||||||
|
return '.jpg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存文件路径
|
||||||
|
* @param {string} url 原始图片URL
|
||||||
|
* @returns {string} 缓存文件路径
|
||||||
|
*/
|
||||||
|
getCacheFilePath(url) {
|
||||||
|
const cacheKey = this.generateCacheKey(url);
|
||||||
|
return path.join(this.cacheDir, cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存是否存在且有效
|
||||||
|
* @param {string} url 原始图片URL
|
||||||
|
* @returns {Promise<boolean>} 缓存是否有效
|
||||||
|
*/
|
||||||
|
async isCacheValid(url) {
|
||||||
|
try {
|
||||||
|
const cachePath = this.getCacheFilePath(url);
|
||||||
|
const stats = await fs.stat(cachePath);
|
||||||
|
|
||||||
|
// 检查文件是否过期
|
||||||
|
const age = Date.now() - stats.mtime.getTime();
|
||||||
|
return age < this.config.maxAge;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从缓存获取图片
|
||||||
|
* @param {string} url 原始图片URL
|
||||||
|
* @returns {Promise<Buffer|null>} 图片数据,如果缓存不存在则返回null
|
||||||
|
*/
|
||||||
|
async getFromCache(url) {
|
||||||
|
try {
|
||||||
|
if (!(await this.isCacheValid(url))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachePath = this.getCacheFilePath(url);
|
||||||
|
const data = await fs.readFile(cachePath);
|
||||||
|
|
||||||
|
// 更新文件访问时间
|
||||||
|
await fs.utimes(cachePath, new Date(), new Date());
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取缓存失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片保存到缓存
|
||||||
|
* @param {string} url 原始图片URL
|
||||||
|
* @param {Buffer} data 图片数据
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async saveToCache(url, data) {
|
||||||
|
try {
|
||||||
|
const cachePath = this.getCacheFilePath(url);
|
||||||
|
await fs.writeFile(cachePath, data);
|
||||||
|
|
||||||
|
// 检查缓存大小,如果超过限制则清理
|
||||||
|
await this.checkCacheSize();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存缓存失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从网络下载图片并缓存
|
||||||
|
* @param {string} url 图片URL
|
||||||
|
* @returns {Promise<Buffer>} 图片数据
|
||||||
|
*/
|
||||||
|
async downloadAndCache(url) {
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= this.config.proxy.retryCount; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
headers: {
|
||||||
|
'Referer': 'https://www.pixiv.net/',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
|
},
|
||||||
|
timeout: this.config.proxy.timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = Buffer.from(response.data);
|
||||||
|
|
||||||
|
// 异步保存到缓存(不等待完成)
|
||||||
|
if (this.config.enabled) {
|
||||||
|
this.saveToCache(url, data).catch(error => {
|
||||||
|
console.error('异步保存缓存失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
if (attempt < this.config.proxy.retryCount) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, this.config.proxy.retryDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图片(优先从缓存,缓存不存在则下载)
|
||||||
|
* @param {string} url 图片URL
|
||||||
|
* @returns {Promise<Buffer>} 图片数据
|
||||||
|
*/
|
||||||
|
async getImage(url) {
|
||||||
|
// 检查缓存是否启用
|
||||||
|
if (!this.config.enabled) {
|
||||||
|
return await this.downloadAndCache(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件类型是否允许缓存
|
||||||
|
const ext = this.getFileExtension(url);
|
||||||
|
if (!this.config.allowedExtensions.includes(ext)) {
|
||||||
|
return await this.downloadAndCache(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首先尝试从缓存获取
|
||||||
|
const cachedData = await this.getFromCache(url);
|
||||||
|
if (cachedData) {
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存不存在,从网络下载
|
||||||
|
return await this.downloadAndCache(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存大小并清理
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async checkCacheSize() {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(this.cacheDir);
|
||||||
|
let totalSize = 0;
|
||||||
|
const fileStats = [];
|
||||||
|
|
||||||
|
// 计算总大小和收集文件信息
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(this.cacheDir, file);
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
totalSize += stats.size;
|
||||||
|
fileStats.push({
|
||||||
|
path: filePath,
|
||||||
|
size: stats.size,
|
||||||
|
mtime: stats.mtime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果超过最大大小,删除最旧的文件
|
||||||
|
if (totalSize > this.config.maxSize) {
|
||||||
|
console.log(`缓存大小 ${totalSize} 超过限制 ${this.config.maxSize},开始清理...`);
|
||||||
|
|
||||||
|
// 按修改时间排序,删除最旧的文件
|
||||||
|
fileStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
|
||||||
|
|
||||||
|
for (const file of fileStats) {
|
||||||
|
await fs.unlink(file.path);
|
||||||
|
totalSize -= file.size;
|
||||||
|
|
||||||
|
if (totalSize <= this.config.maxSize * 0.8) { // 清理到80%
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`缓存清理完成,当前大小: ${totalSize}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查缓存大小失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期缓存
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async cleanupExpiredCache() {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(this.cacheDir);
|
||||||
|
let cleanedCount = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(this.cacheDir, file);
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
|
||||||
|
const age = Date.now() - stats.mtime.getTime();
|
||||||
|
if (age > this.config.maxAge) {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
cleanedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanedCount > 0) {
|
||||||
|
console.log(`清理了 ${cleanedCount} 个过期缓存文件`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清理过期缓存失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动定期清理任务
|
||||||
|
*/
|
||||||
|
startCleanupTask() {
|
||||||
|
setInterval(() => {
|
||||||
|
this.cleanupExpiredCache().catch(error => {
|
||||||
|
console.error('定期清理任务失败:', error);
|
||||||
|
});
|
||||||
|
}, this.config.cleanupInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动清理所有缓存
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async clearAllCache() {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(this.cacheDir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(this.cacheDir, file);
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('所有缓存已清理');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清理所有缓存失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存统计信息
|
||||||
|
* @returns {Promise<Object>} 缓存统计信息
|
||||||
|
*/
|
||||||
|
async getCacheStats() {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(this.cacheDir);
|
||||||
|
let totalSize = 0;
|
||||||
|
let fileCount = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(this.cacheDir, file);
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
totalSize += stats.size;
|
||||||
|
fileCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileCount,
|
||||||
|
totalSize,
|
||||||
|
maxSize: this.config.maxSize,
|
||||||
|
maxAge: this.config.maxAge,
|
||||||
|
enabled: this.config.enabled,
|
||||||
|
config: this.config
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取缓存统计失败:', error);
|
||||||
|
return {
|
||||||
|
fileCount: 0,
|
||||||
|
totalSize: 0,
|
||||||
|
maxSize: this.config.maxSize,
|
||||||
|
maxAge: this.config.maxAge,
|
||||||
|
enabled: this.config.enabled,
|
||||||
|
config: this.config
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存配置
|
||||||
|
* @returns {Promise<Object>} 缓存配置
|
||||||
|
*/
|
||||||
|
async getConfig() {
|
||||||
|
return await this.configManager.loadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新缓存配置
|
||||||
|
* @param {Object} updates 配置更新
|
||||||
|
* @returns {Promise<Object>} 更新后的配置
|
||||||
|
*/
|
||||||
|
async updateConfig(updates) {
|
||||||
|
const newConfig = await this.configManager.updateConfig(updates);
|
||||||
|
this.config = { ...this.config, ...newConfig };
|
||||||
|
return newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置缓存配置
|
||||||
|
* @returns {Promise<Object>} 重置后的配置
|
||||||
|
*/
|
||||||
|
async resetConfig() {
|
||||||
|
const defaultConfig = await this.configManager.resetToDefault();
|
||||||
|
this.config = { ...this.config, ...defaultConfig };
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ImageCacheService;
|
||||||
Binary file not shown.
+348
@@ -0,0 +1,348 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cache-manager">
|
||||||
|
<div class="cache-header">
|
||||||
|
<h3>图片缓存管理</h3>
|
||||||
|
<div class="cache-actions">
|
||||||
|
<button @click="refreshStats" class="btn btn-secondary" :disabled="loading">
|
||||||
|
{{ loading ? '刷新中...' : '刷新' }}
|
||||||
|
</button>
|
||||||
|
<button @click="clearExpiredCache" class="btn btn-warning" :disabled="loading">
|
||||||
|
清理过期缓存
|
||||||
|
</button>
|
||||||
|
<button @click="clearAllCache" class="btn btn-danger" :disabled="loading">
|
||||||
|
清理所有缓存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="stats" class="cache-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">缓存文件数</div>
|
||||||
|
<div class="stat-value">{{ stats.fileCount }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">缓存大小</div>
|
||||||
|
<div class="stat-value">{{ formatFileSize(stats.totalSize) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">最大缓存大小</div>
|
||||||
|
<div class="stat-value">{{ formatFileSize(stats.maxSize) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">缓存有效期</div>
|
||||||
|
<div class="stat-value">{{ formatDuration(stats.maxAge) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">使用率</div>
|
||||||
|
<div class="stat-value">
|
||||||
|
<div class="usage-bar">
|
||||||
|
<div
|
||||||
|
class="usage-fill"
|
||||||
|
:style="{ width: `${usagePercentage}%` }"
|
||||||
|
:class="{ 'usage-high': usagePercentage > 80 }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="usage-text">{{ usagePercentage.toFixed(1) }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading && !stats" class="loading">
|
||||||
|
加载缓存信息中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import apiService from '@/services/api';
|
||||||
|
|
||||||
|
interface CacheStats {
|
||||||
|
fileCount: number;
|
||||||
|
totalSize: number;
|
||||||
|
maxSize: number;
|
||||||
|
maxAge: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = ref<CacheStats | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
// 计算使用率百分比
|
||||||
|
const usagePercentage = computed(() => {
|
||||||
|
if (!stats.value) return 0;
|
||||||
|
return (stats.value.totalSize / stats.value.maxSize) * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化持续时间
|
||||||
|
const formatDuration = (milliseconds: number): string => {
|
||||||
|
const hours = Math.floor(milliseconds / (1000 * 60 * 60));
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days} 天`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours} 小时`;
|
||||||
|
} else {
|
||||||
|
const minutes = Math.floor(milliseconds / (1000 * 60));
|
||||||
|
return `${minutes} 分钟`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取缓存统计信息
|
||||||
|
const fetchCacheStats = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const response = await apiService.get<CacheStats>('/api/proxy/cache/stats');
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
stats.value = response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '获取缓存统计失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : '获取缓存统计失败';
|
||||||
|
console.error('获取缓存统计失败:', err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新统计信息
|
||||||
|
const refreshStats = () => {
|
||||||
|
fetchCacheStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理过期缓存
|
||||||
|
const clearExpiredCache = async () => {
|
||||||
|
if (!confirm('确定要清理过期的缓存文件吗?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const response = await apiService.delete('/api/proxy/cache/expired');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
alert('过期缓存已清理');
|
||||||
|
await fetchCacheStats(); // 刷新统计信息
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '清理过期缓存失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : '清理过期缓存失败';
|
||||||
|
console.error('清理过期缓存失败:', err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理所有缓存
|
||||||
|
const clearAllCache = async () => {
|
||||||
|
if (!confirm('确定要清理所有缓存文件吗?这将删除所有缓存的图片。')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const response = await apiService.delete('/api/proxy/cache');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
alert('所有缓存已清理');
|
||||||
|
await fetchCacheStats(); // 刷新统计信息
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '清理所有缓存失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : '清理所有缓存失败';
|
||||||
|
console.error('清理所有缓存失败:', err);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时获取统计信息
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCacheStats();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cache-manager {
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover:not(:disabled) {
|
||||||
|
background: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 0.5rem;
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #10b981;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-fill.usage-high {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6b7280;
|
||||||
|
min-width: 3rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cache-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+170
-10
@@ -56,8 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 作品列表 -->
|
<!-- 作品列表 -->
|
||||||
<div class="artworks-section">
|
<div class="artworks-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -102,11 +100,23 @@
|
|||||||
<button @click="goToPage(currentPage + 1)" class="page-btn" :disabled="currentPage >= totalPages">
|
<button @click="goToPage(currentPage + 1)" class="page-btn" :disabled="currentPage >= totalPages">
|
||||||
下一页
|
下一页
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" class="page-icon">
|
<svg viewBox="0 0 24 24" fill="currentColor" class="page-icon">
|
||||||
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
|
<path d="8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 跳转到指定页面 -->
|
||||||
|
<div v-if="totalPages > 1 && artworks.length > 0" class="jump-to-page">
|
||||||
|
<div class="jump-input-group">
|
||||||
|
<label for="jumpPage">跳转到:</label>
|
||||||
|
<input v-model="jumpPageInput" type="number" id="jumpPage" class="jump-input" :min="1" :max="totalPages"
|
||||||
|
placeholder="页码" @keyup.enter="handleJumpToPage" />
|
||||||
|
<button @click="handleJumpToPage" class="jump-btn" :disabled="!jumpPageInput || jumping">
|
||||||
|
{{ jumping ? '跳转中...' : '跳转' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 页面信息 -->
|
<!-- 页面信息 -->
|
||||||
<div v-if="totalPages > 1 && artworks.length > 0" class="page-info">
|
<div v-if="totalPages > 1 && artworks.length > 0" class="page-info">
|
||||||
<span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
|
<span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
|
||||||
@@ -247,7 +257,7 @@ const fetchArtistInfo = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取作者作品
|
// 获取作者作品
|
||||||
const fetchArtworks = async (page = 1) => {
|
const fetchArtworks = async (page = 1, isJumpToPage = false) => {
|
||||||
if (!artist.value) return;
|
if (!artist.value) return;
|
||||||
|
|
||||||
const cacheKey = getCacheKey(artworkType.value, page);
|
const cacheKey = getCacheKey(artworkType.value, page);
|
||||||
@@ -313,8 +323,14 @@ const fetchArtworks = async (page = 1) => {
|
|||||||
throw new Error(response.error || '获取作品列表失败');
|
throw new Error(response.error || '获取作品列表失败');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : '获取作品列表失败';
|
|
||||||
console.error('获取作品列表失败:', err);
|
console.error('获取作品列表失败:', err);
|
||||||
|
|
||||||
|
// 只有在跳转到指定页面失败时才显示错误
|
||||||
|
if (isJumpToPage) {
|
||||||
|
error.value = `跳转失败:无法跳转到第 ${page} 页`;
|
||||||
|
} else {
|
||||||
|
error.value = err instanceof Error ? err.message : '获取作品列表失败';
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
artworksLoading.value = false;
|
artworksLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -323,12 +339,30 @@ const fetchArtworks = async (page = 1) => {
|
|||||||
// 处理类型切换
|
// 处理类型切换
|
||||||
const handleTypeChange = () => {
|
const handleTypeChange = () => {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
|
|
||||||
|
// 清除URL中的页码参数
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
page: undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
fetchArtworks(1);
|
fetchArtworks(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 跳转到指定页面
|
// 跳转到指定页面
|
||||||
const goToPage = (page: number) => {
|
const goToPage = (page: number) => {
|
||||||
if (page < 1 || page > totalPages.value || page === currentPage.value) return;
|
if (page < 1 || page === currentPage.value) return;
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
page: page.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
fetchArtworks(page);
|
fetchArtworks(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -407,22 +441,63 @@ const clearError = () => {
|
|||||||
error.value = null;
|
error.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 跳转到指定页面输入框
|
||||||
|
const jumpPageInput = ref<string | number>('');
|
||||||
|
const jumping = ref(false);
|
||||||
|
|
||||||
|
// 处理跳转到指定页面
|
||||||
|
const handleJumpToPage = async () => {
|
||||||
|
const page = parseInt(jumpPageInput.value as string);
|
||||||
|
if (isNaN(page) || page < 1) {
|
||||||
|
error.value = '请输入有效的页码';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jumping.value = true;
|
||||||
|
jumpPageInput.value = ''; // 清空输入框
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
page: page.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchArtworks(page, true);
|
||||||
|
} finally {
|
||||||
|
jumping.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 监听路由变化
|
// 监听路由变化
|
||||||
watch(() => route.params.id, () => {
|
watch(() => route.params.id, () => {
|
||||||
// 清除缓存并重新加载
|
// 清除缓存并重新加载
|
||||||
clearCache();
|
clearCache();
|
||||||
fetchArtistInfo();
|
fetchArtistInfo();
|
||||||
|
|
||||||
// 检查是否有返回的页面信息
|
// 检查是否有返回的页面信息或指定的页码
|
||||||
const returnPage = parseInt(route.query.page as string);
|
const returnPage = parseInt(route.query.page as string);
|
||||||
if (returnPage && returnPage > 0) {
|
if (returnPage && returnPage > 0) {
|
||||||
currentPage.value = returnPage;
|
currentPage.value = returnPage;
|
||||||
fetchArtworks(returnPage);
|
fetchArtworks(returnPage, true);
|
||||||
} else {
|
} else {
|
||||||
fetchArtworks(1);
|
fetchArtworks(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听URL查询参数变化
|
||||||
|
watch(() => route.query.page, (newPage) => {
|
||||||
|
if (newPage && artist.value) {
|
||||||
|
const page = parseInt(newPage as string);
|
||||||
|
if (page > 0 && page !== currentPage.value) {
|
||||||
|
currentPage.value = page;
|
||||||
|
fetchArtworks(page, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 组件卸载时清理缓存
|
// 组件卸载时清理缓存
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearCache();
|
clearCache();
|
||||||
@@ -431,11 +506,11 @@ onUnmounted(() => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchArtistInfo();
|
await fetchArtistInfo();
|
||||||
|
|
||||||
// 检查是否有返回的页面信息
|
// 检查是否有返回的页面信息或指定的页码
|
||||||
const returnPage = parseInt(route.query.page as string);
|
const returnPage = parseInt(route.query.page as string);
|
||||||
if (returnPage && returnPage > 0) {
|
if (returnPage && returnPage > 0) {
|
||||||
currentPage.value = returnPage;
|
currentPage.value = returnPage;
|
||||||
await fetchArtworks(returnPage);
|
await fetchArtworks(returnPage, true);
|
||||||
} else {
|
} else {
|
||||||
await fetchArtworks(1);
|
await fetchArtworks(1);
|
||||||
}
|
}
|
||||||
@@ -749,6 +824,73 @@ onMounted(async () => {
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jump-to-page {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-input-group label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-input {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: white;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #374151;
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-btn:hover:not(:disabled) {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
@@ -806,5 +948,23 @@ onMounted(async () => {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jump-to-page {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-input-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+148
-10
@@ -7,16 +7,19 @@
|
|||||||
<div class="search-form">
|
<div class="search-form">
|
||||||
<!-- 搜索类型选择 -->
|
<!-- 搜索类型选择 -->
|
||||||
<div class="search-type-tabs">
|
<div class="search-type-tabs">
|
||||||
<button @click="searchMode = 'keyword'" class="tab-btn" :class="{ active: searchMode === 'keyword' }">
|
<button @click="handleSearchModeChange('keyword')" class="tab-btn"
|
||||||
|
:class="{ active: searchMode === 'keyword' }">
|
||||||
关键词搜索
|
关键词搜索
|
||||||
</button>
|
</button>
|
||||||
<button @click="searchMode = 'tags'" class="tab-btn" :class="{ active: searchMode === 'tags' }">
|
<button @click="handleSearchModeChange('tags')" class="tab-btn" :class="{ active: searchMode === 'tags' }">
|
||||||
标签搜索
|
标签搜索
|
||||||
</button>
|
</button>
|
||||||
<button @click="searchMode = 'artwork'" class="tab-btn" :class="{ active: searchMode === 'artwork' }">
|
<button @click="handleSearchModeChange('artwork')" class="tab-btn"
|
||||||
|
:class="{ active: searchMode === 'artwork' }">
|
||||||
作品ID
|
作品ID
|
||||||
</button>
|
</button>
|
||||||
<button @click="searchMode = 'artist'" class="tab-btn" :class="{ active: searchMode === 'artist' }">
|
<button @click="handleSearchModeChange('artist')" class="tab-btn"
|
||||||
|
:class="{ active: searchMode === 'artist' }">
|
||||||
作者ID
|
作者ID
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,20 +84,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-filters">
|
<div class="search-filters">
|
||||||
<select v-model="searchType" class="filter-select">
|
<select v-model="searchType" @change="updateFiltersInUrl" class="filter-select">
|
||||||
<option value="all">全部类型</option>
|
<option value="all">全部类型</option>
|
||||||
<option value="art">插画</option>
|
<option value="art">插画</option>
|
||||||
<option value="manga">漫画</option>
|
<option value="manga">漫画</option>
|
||||||
<option value="novel">小说</option>
|
<option value="novel">小说</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select v-model="searchSort" class="filter-select">
|
<select v-model="searchSort" @change="updateFiltersInUrl" class="filter-select">
|
||||||
<option value="date_desc">最新</option>
|
<option value="date_desc">最新</option>
|
||||||
<option value="date_asc">最旧</option>
|
<option value="date_asc">最旧</option>
|
||||||
<option value="popular_desc">最受欢迎</option>
|
<option value="popular_desc">最受欢迎</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select v-model="searchDuration" class="filter-select">
|
<select v-model="searchDuration" @change="updateFiltersInUrl" class="filter-select">
|
||||||
<option value="all">全部时间</option>
|
<option value="all">全部时间</option>
|
||||||
<option value="within_last_day">最近一天</option>
|
<option value="within_last_day">最近一天</option>
|
||||||
<option value="within_last_week">最近一周</option>
|
<option value="within_last_week">最近一周</option>
|
||||||
@@ -203,6 +206,14 @@ const handleSearch = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
const query: any = { ...route.query };
|
||||||
|
query.keyword = searchKeyword.value.trim();
|
||||||
|
query.mode = 'keyword';
|
||||||
|
// 清除标签相关参数
|
||||||
|
delete query.tags;
|
||||||
|
router.push({ query });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
@@ -299,6 +310,15 @@ const handleArtworkSearch = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
const query: any = { ...route.query };
|
||||||
|
query.artworkId = idStr;
|
||||||
|
query.mode = 'artwork';
|
||||||
|
// 清除其他搜索参数
|
||||||
|
delete query.keyword;
|
||||||
|
delete query.tags;
|
||||||
|
router.push({ query });
|
||||||
|
|
||||||
router.push(`/artwork/${id}`);
|
router.push(`/artwork/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,6 +336,15 @@ const handleArtistSearch = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
const query: any = { ...route.query };
|
||||||
|
query.artistId = idStr;
|
||||||
|
query.mode = 'artist';
|
||||||
|
// 清除其他搜索参数
|
||||||
|
delete query.keyword;
|
||||||
|
delete query.tags;
|
||||||
|
router.push({ query });
|
||||||
|
|
||||||
// 切换到作者搜索模式并跳转
|
// 切换到作者搜索模式并跳转
|
||||||
searchMode.value = 'artist';
|
searchMode.value = 'artist';
|
||||||
router.push(`/artist/${id}`);
|
router.push(`/artist/${id}`);
|
||||||
@@ -327,11 +356,80 @@ const addTag = () => {
|
|||||||
if (tag && !searchTags.value.includes(tag)) {
|
if (tag && !searchTags.value.includes(tag)) {
|
||||||
searchTags.value.push(tag);
|
searchTags.value.push(tag);
|
||||||
tagInput.value = '';
|
tagInput.value = '';
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
updateSearchTagsInUrl();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTag = (index: number) => {
|
const removeTag = (index: number) => {
|
||||||
searchTags.value.splice(index, 1);
|
searchTags.value.splice(index, 1);
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
updateSearchTagsInUrl();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新URL中的搜索标签参数
|
||||||
|
const updateSearchTagsInUrl = () => {
|
||||||
|
const query: any = { ...route.query };
|
||||||
|
|
||||||
|
if (searchTags.value.length > 0) {
|
||||||
|
query.tags = searchTags.value;
|
||||||
|
query.mode = 'tags';
|
||||||
|
} else {
|
||||||
|
// 如果没有标签,清除相关参数
|
||||||
|
delete query.tags;
|
||||||
|
delete query.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({ query });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 更新搜索过滤器到URL
|
||||||
|
const updateFiltersInUrl = () => {
|
||||||
|
const query: any = { ...route.query };
|
||||||
|
|
||||||
|
if (searchType.value !== 'all') query.type = searchType.value;
|
||||||
|
else delete query.type;
|
||||||
|
|
||||||
|
if (searchSort.value !== 'date_desc') query.sort = searchSort.value;
|
||||||
|
else delete query.sort;
|
||||||
|
|
||||||
|
if (searchDuration.value !== 'all') query.duration = searchDuration.value;
|
||||||
|
else delete query.duration;
|
||||||
|
|
||||||
|
router.push({ query });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理搜索模式切换
|
||||||
|
const handleSearchModeChange = (mode: 'keyword' | 'tags' | 'artwork' | 'artist') => {
|
||||||
|
searchMode.value = mode;
|
||||||
|
|
||||||
|
// 清除其他模式的输入
|
||||||
|
if (mode !== 'keyword') searchKeyword.value = '';
|
||||||
|
if (mode !== 'tags') {
|
||||||
|
searchTags.value = [];
|
||||||
|
tagInput.value = '';
|
||||||
|
}
|
||||||
|
if (mode !== 'artwork') artworkId.value = '';
|
||||||
|
if (mode !== 'artist') artistId.value = '';
|
||||||
|
|
||||||
|
// 更新URL参数,清除不相关的参数
|
||||||
|
const query: any = { ...route.query };
|
||||||
|
query.mode = mode;
|
||||||
|
|
||||||
|
// 根据模式清除不相关的参数
|
||||||
|
if (mode !== 'keyword') delete query.keyword;
|
||||||
|
if (mode !== 'tags') {
|
||||||
|
delete query.tags;
|
||||||
|
delete query.tag;
|
||||||
|
}
|
||||||
|
if (mode !== 'artwork') delete query.artworkId;
|
||||||
|
if (mode !== 'artist') delete query.artistId;
|
||||||
|
|
||||||
|
router.push({ query });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagSearch = async () => {
|
const handleTagSearch = async () => {
|
||||||
@@ -339,6 +437,14 @@ const handleTagSearch = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
const query: any = { ...route.query };
|
||||||
|
query.tags = searchTags.value;
|
||||||
|
query.mode = 'tags';
|
||||||
|
// 清除关键词相关参数
|
||||||
|
delete query.keyword;
|
||||||
|
router.push({ query });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
@@ -384,13 +490,37 @@ const handleArtistDownload = (artist: any) => {
|
|||||||
// 监听路由变化,处理URL参数
|
// 监听路由变化,处理URL参数
|
||||||
watch(() => route.query, () => {
|
watch(() => route.query, () => {
|
||||||
const urlMode = route.query.mode as string;
|
const urlMode = route.query.mode as string;
|
||||||
|
const urlKeyword = route.query.keyword as string;
|
||||||
const urlTag = route.query.tag as string;
|
const urlTag = route.query.tag as string;
|
||||||
const urlTags = route.query.tags;
|
const urlTags = route.query.tags;
|
||||||
|
const urlType = route.query.type as string;
|
||||||
|
const urlSort = route.query.sort as string;
|
||||||
|
const urlDuration = route.query.duration as string;
|
||||||
|
const urlArtworkId = route.query.artworkId as string;
|
||||||
|
const urlArtistId = route.query.artistId as string;
|
||||||
|
|
||||||
|
// 恢复搜索模式
|
||||||
|
if (urlMode) {
|
||||||
|
searchMode.value = urlMode as 'keyword' | 'tags' | 'artwork' | 'artist';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复关键词
|
||||||
|
if (urlKeyword) {
|
||||||
|
searchKeyword.value = urlKeyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复作品ID
|
||||||
|
if (urlArtworkId) {
|
||||||
|
artworkId.value = urlArtworkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复作者ID
|
||||||
|
if (urlArtistId) {
|
||||||
|
artistId.value = urlArtistId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复标签
|
||||||
if (urlMode === 'tags') {
|
if (urlMode === 'tags') {
|
||||||
// 自动设置标签搜索模式
|
|
||||||
searchMode.value = 'tags';
|
|
||||||
|
|
||||||
if (urlTags) {
|
if (urlTags) {
|
||||||
// 处理多个标签
|
// 处理多个标签
|
||||||
if (Array.isArray(urlTags)) {
|
if (Array.isArray(urlTags)) {
|
||||||
@@ -411,7 +541,15 @@ watch(() => route.query, () => {
|
|||||||
if (searchTags.value.length > 0) {
|
if (searchTags.value.length > 0) {
|
||||||
handleTagSearch();
|
handleTagSearch();
|
||||||
}
|
}
|
||||||
|
} else if (urlMode === 'keyword' && urlKeyword) {
|
||||||
|
// 如果是关键词搜索模式且有关键词,自动执行搜索
|
||||||
|
handleSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 恢复过滤器
|
||||||
|
if (urlType) searchType.value = urlType as 'all' | 'art' | 'manga' | 'novel';
|
||||||
|
if (urlSort) searchSort.value = urlSort as 'date_desc' | 'date_asc' | 'popular_desc';
|
||||||
|
if (urlDuration) searchDuration.value = urlDuration as 'all' | 'within_last_day' | 'within_last_week' | 'within_last_month';
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user