修复暂停下载后不能恢复和更改时间无效的问题

This commit is contained in:
2025-09-02 06:36:25 +08:00
parent a5f38a4eed
commit 24e9dc08bc
6 changed files with 521 additions and 69 deletions
+115 -10
View File
@@ -33,6 +33,10 @@ class DownloadExecutor {
// 检查是否应该暂停
if (this.shouldPause(task.id)) {
logger.info('任务已暂停,停止下载:', task.id);
// 确保任务状态为暂停
task.status = 'paused';
await this.taskManager.saveTasks();
this.progressManager.notifyProgressUpdate(task.id, task);
break;
}
@@ -75,10 +79,13 @@ class DownloadExecutor {
// 验证文件完整性
const integrity = await this.fileManager.checkFileIntegrity(filePath);
if (integrity.valid) {
task.completed_files++;
task.progress = Math.round((task.completed_files / task.total_files) * 100);
await this.taskManager.saveTasks();
this.progressManager.notifyProgressUpdate(task.id, task);
// 只有在非恢复模式下才增加计数,避免重复计算
if (!task.isResuming) {
task.completed_files++;
task.progress = Math.round((task.completed_files / task.total_files) * 100);
await this.taskManager.saveTasks();
this.progressManager.notifyProgressUpdate(task.id, task);
}
results.push({ success: true, file: fileName, skipped: true });
continue;
} else {
@@ -115,6 +122,12 @@ class DownloadExecutor {
}
}
// 检查任务是否被暂停,如果是则不要更新最终状态
if (task.status === 'paused') {
logger.info('任务已暂停,跳过最终状态更新:', task.id);
return;
}
// 保存作品信息
const infoPath = path.join(artworkDir, 'artwork_info.json');
await fs.writeJson(infoPath, artwork, { spaces: 2 });
@@ -527,32 +540,123 @@ class DownloadExecutor {
async resumeTask(taskId) {
const task = this.taskManager.getTask(taskId);
if (!task) {
logger.error('恢复任务失败:任务不存在', { taskId });
throw new Error('任务不存在');
}
// logger.info('下载执行器检查任务状态', {
// taskId,
// currentStatus: task.status,
// type: task.type
// });
if (task.status !== 'paused') {
logger.error('恢复任务失败:任务状态不是暂停状态', {
taskId,
currentStatus: task.status
});
throw new Error('任务状态不是暂停状态');
}
// 根据任务类型重新开始下载
if (task.type === 'artwork') {
// logger.info('开始恢复单个作品下载任务', { taskId, artwork_id: task.artwork_id });
// 重新获取作品信息和图片URL
const artworkResult = await this.downloadService.artworkService.getArtwork(task.artwork_id);
const artworkResult = await this.downloadService.artworkService.getArtworkDetail(task.artwork_id);
if (!artworkResult.success) {
logger.error('获取作品信息失败', { taskId, error: artworkResult.error });
throw new Error(`获取作品信息失败: ${artworkResult.error}`);
}
const imagesResult = await this.downloadService.artworkService.getArtworkImages(task.artwork_id, 'original');
let imagesResult = await this.downloadService.artworkService.getArtworkImages(task.artwork_id, 'original');
if (!imagesResult.success) {
logger.error('获取图片URL失败', { taskId, error: imagesResult.error });
throw new Error(`获取图片URL失败: ${imagesResult.error}`);
}
const artwork = artworkResult.data;
const images = imagesResult.data.images;
const artworkDir = await this.fileManager.getArtworkDirectory(artwork);
let images = imagesResult.data.images;
// 重新开始下载
this.executeArtworkDownload(task, images, 'original', artworkDir, artwork);
// 创建作品目录(使用与DownloadService相同的逻辑)
const artistName = this.fileManager.createSafeDirectoryName(artwork.user.name || 'Unknown Artist');
const artworkTitle = this.fileManager.createSafeDirectoryName(artwork.title || 'Untitled');
const downloadPath = await this.fileManager.getDownloadPath();
const artistDir = path.join(downloadPath, artistName);
const artworkDirName = `${task.artwork_id}_${artworkTitle}`;
const artworkDir = path.join(artistDir, artworkDirName);
// logger.info('准备恢复下载,重置任务状态', {
// taskId,
// originalTotalFiles: task.total_files,
// newImageCount: images.length,
// artworkDir
// });
// 如果新获取的图片数量与原始数量不同,记录警告但使用原始数量
if (images.length !== task.total_files) {
logger.warn('恢复时图片数量发生变化', {
taskId,
originalTotalFiles: task.total_files,
newImageCount: images.length
});
// 使用原始数量,避免任务状态混乱
images = images.slice(0, task.total_files);
}
// 检查哪些文件已经完成下载
const completedFiles = [];
const incompleteFiles = [];
for (let index = 0; index < images.length; index++) {
const imageObj = images[index];
let imageUrl = imageObj.original || imageObj.large || imageObj.medium;
const fileName = `image_${index + 1}.${this.getFileExtension(imageUrl)}`;
const filePath = path.join(artworkDir, fileName);
// 检查文件是否存在且完整
if (await this.fileManager.fileExists(filePath)) {
const integrity = await this.fileManager.checkFileIntegrity(filePath);
if (integrity.valid) {
completedFiles.push({ index, fileName, filePath });
} else {
incompleteFiles.push({ index, fileName, filePath });
}
} else {
incompleteFiles.push({ index, fileName, filePath });
}
}
// logger.info('文件检查完成', {
// taskId,
// completedCount: completedFiles.length,
// incompleteCount: incompleteFiles.length
// });
// 只删除未完成的文件
for (const fileInfo of incompleteFiles) {
try {
await this.fileManager.safeDeleteFile(fileInfo.filePath);
logger.debug(`删除未完成文件: ${fileInfo.fileName}`);
} catch (error) {
// 忽略删除错误,文件可能不存在
logger.debug(`删除文件失败(可能不存在): ${fileInfo.filePath}`);
}
}
// 重置任务状态,但保留已完成的文件计数
task.completed_files = completedFiles.length;
task.failed_files = 0;
task.progress = Math.round((task.completed_files / task.total_files) * 100);
task.status = 'downloading';
// 添加恢复标志,避免重复计算已完成的文件
task.isResuming = true;
await this.taskManager.saveTasks();
// logger.info('开始执行作品下载', { taskId });
// 重新开始下载 - 等待异步执行开始
await this.executeArtworkDownload(task, images, 'original', artworkDir, artwork);
} else if (task.type === 'batch' || task.type === 'artist') {
// 批量下载和作者下载的恢复逻辑
// 这里需要根据具体实现来恢复
@@ -560,6 +664,7 @@ class DownloadExecutor {
// TODO: 实现批量下载的恢复逻辑
}
logger.info('任务恢复执行完成', { taskId });
return { success: true };
}
+47 -12
View File
@@ -224,32 +224,67 @@ class DownloadService {
return { success: false, error: '任务不存在' };
}
// 只允许暂停正在下载的任务
if (task.status !== 'downloading') {
return { success: false, error: '只能暂停正在下载的任务' };
}
await this.taskManager.updateTask(taskId, { status: 'paused' });
this.progressManager.notifyProgressUpdate(taskId, task);
return { success: true };
// 获取更新后的任务
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
return { success: true, data: updatedTask };
}
async resumeTask(taskId) {
const task = this.taskManager.getTask(taskId);
if (!task) {
logger.error('恢复任务失败:任务不存在', { taskId });
return { success: false, error: '任务不存在' };
}
// logger.info('尝试恢复任务', {
// taskId,
// currentStatus: task.status,
// type: task.type,
// artwork_id: task.artwork_id
// });
// 只允许恢复暂停的任务
if (task.status !== 'paused') {
return { success: false, error: '任务状态不是暂停状态' };
logger.warn('恢复任务失败:任务状态不是暂停状态', {
taskId,
currentStatus: task.status
});
return { success: false, error: '只能恢复暂停的任务' };
}
// 更新任务状态为下载中
await this.taskManager.updateTask(taskId, { status: 'downloading' });
// 通知进度更新
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
// 重新开始下载执行
this.downloadExecutor.resumeTask(taskId);
try {
logger.info('开始恢复任务执行', { taskId });
await this.downloadExecutor.resumeTask(taskId);
return { success: true, data: updatedTask };
// 获取更新后的任务状态
const updatedTask = this.taskManager.getTask(taskId);
this.progressManager.notifyProgressUpdate(taskId, updatedTask);
logger.info('任务恢复成功', {
taskId,
newStatus: updatedTask.status
});
} catch (error) {
logger.error('恢复任务执行失败', {
taskId,
error: error.message,
stack: error.stack
});
// 如果恢复失败,保持暂停状态
return { success: false, error: `恢复任务失败: ${error.message}` };
}
return { success: true, data: this.taskManager.getTask(taskId) };
}
// 代理方法 - 历史记录管理
+192 -21
View File
@@ -20,13 +20,16 @@ class ImageCacheService {
if (isPkg) {
// 在打包环境中,使用可执行文件所在目录
this.cacheDir = path.join(process.cwd(), 'data', 'image-cache');
this.indexPath = path.join(process.cwd(), 'data', 'image-cache-index.json');
} else {
// 在开发环境中,使用项目根目录的data文件夹
this.cacheDir = path.join(__dirname, '..', '..', 'data', 'image-cache');
this.indexPath = path.join(__dirname, '..', '..', 'data', 'image-cache-index.json');
}
// 确保路径是绝对路径
this.cacheDir = path.resolve(this.cacheDir);
this.indexPath = path.resolve(this.indexPath);
// 创建配置管理器
this.configManager = new CacheConfigManager();
@@ -46,6 +49,9 @@ class ImageCacheService {
allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'],
};
// 缓存索引
this.cacheIndex = new Map();
// 初始化配置
this.initializeConfig();
}
@@ -62,6 +68,12 @@ class ImageCacheService {
// 确保缓存目录存在
await this.ensureCacheDir();
// 加载缓存索引
await this.loadCacheIndex();
// 验证并同步缓存索引
await this.validateAndSyncIndex();
// 启动定期清理任务
this.startCleanupTask();
@@ -170,9 +182,21 @@ class ImageCacheService {
*/
async saveToCache(url, data) {
try {
const cacheKey = this.generateCacheKey(url);
const cachePath = this.getCacheFilePath(url);
const filename = path.basename(cachePath);
await fs.writeFile(cachePath, data);
// 获取文件信息并添加到索引
const stats = await fs.stat(cachePath);
this.addToIndex(cacheKey, filename, stats.size, stats.mtime.getTime());
// 异步保存索引
this.saveCacheIndex().catch(error => {
logger.error('异步保存缓存索引失败:', error);
});
// 检查缓存大小,如果超过限制则清理
await this.checkCacheSize();
} catch (error) {
@@ -256,25 +280,27 @@ class ImageCacheService {
*/
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);
// 使用索引计算总大小和收集文件信息
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
const filePath = path.join(this.cacheDir, fileInfo.filename);
try {
// 验证文件是否实际存在
const stats = await fs.stat(filePath);
totalSize += stats.size;
fileStats.push({
path: filePath,
size: stats.size,
mtime: stats.mtime
mtime: new Date(fileInfo.mtime),
cacheKey: cacheKey
});
} catch (error) {
// 如果文件不存在,记录日志但继续处理其他文件
// 如果文件不存在,从索引中移除
if (error.code === 'ENOENT') {
logger.warn(`缓存文件不存在,跳过: ${filePath}`);
logger.warn(`缓存文件不存在,从索引中移除: ${filePath}`);
this.removeFromIndex(cacheKey);
} else {
logger.error(`检查缓存文件失败: ${filePath}`, error);
}
@@ -293,6 +319,9 @@ class ImageCacheService {
await fs.unlink(file.path);
totalSize -= file.size;
// 从索引中移除
this.removeFromIndex(file.cacheKey);
if (totalSize <= this.config.maxSize * 0.8) { // 清理到80%
break;
}
@@ -300,12 +329,17 @@ class ImageCacheService {
// 如果删除文件失败,记录日志但继续处理其他文件
if (error.code === 'ENOENT') {
logger.warn(`删除缓存文件时文件不存在: ${file.path}`);
// 从索引中移除
this.removeFromIndex(file.cacheKey);
} else {
logger.error(`删除缓存文件失败: ${file.path}`, error);
}
}
}
// 保存更新后的索引
await this.saveCacheIndex();
logger.info(`缓存清理完成,当前大小: ${totalSize}`);
}
} catch (error) {
@@ -319,31 +353,35 @@ class ImageCacheService {
*/
async cleanupExpiredCache() {
try {
const files = await fs.readdir(this.cacheDir);
let cleanedCount = 0;
const now = Date.now();
for (const file of files) {
const filePath = path.join(this.cacheDir, file);
// 使用索引检查过期文件
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
const filePath = path.join(this.cacheDir, fileInfo.filename);
try {
const stats = await fs.stat(filePath);
const age = Date.now() - stats.mtime.getTime();
const age = now - stats.mtime.getTime();
if (age > this.config.maxAge) {
try {
await fs.unlink(filePath);
this.removeFromIndex(cacheKey);
cleanedCount++;
} catch (deleteError) {
if (deleteError.code === 'ENOENT') {
logger.warn(`删除过期缓存文件时文件不存在: ${filePath}`);
this.removeFromIndex(cacheKey);
} else {
logger.error(`删除过期缓存文件失败: ${filePath}`, deleteError);
}
}
}
} catch (error) {
// 如果文件不存在,记录日志但继续处理其他文件
// 如果文件不存在,从索引中移除
if (error.code === 'ENOENT') {
logger.warn(`过期缓存文件不存在,跳过: ${filePath}`);
logger.warn(`过期缓存文件不存在,从索引中移除: ${filePath}`);
this.removeFromIndex(cacheKey);
} else {
logger.error(`检查过期缓存文件失败: ${filePath}`, error);
}
@@ -351,6 +389,8 @@ class ImageCacheService {
}
if (cleanedCount > 0) {
// 保存更新后的索引
await this.saveCacheIndex();
logger.info(`清理了 ${cleanedCount} 个过期缓存文件`);
}
} catch (error) {
@@ -375,12 +415,12 @@ class ImageCacheService {
*/
async clearAllCache() {
try {
const files = await fs.readdir(this.cacheDir);
let deletedCount = 0;
let errorCount = 0;
for (const file of files) {
const filePath = path.join(this.cacheDir, file);
// 使用索引清理所有文件
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
const filePath = path.join(this.cacheDir, fileInfo.filename);
try {
await fs.unlink(filePath);
deletedCount++;
@@ -394,6 +434,10 @@ class ImageCacheService {
}
}
// 清空索引
this.cacheIndex.clear();
await this.saveCacheIndex();
if (errorCount === 0) {
logger.info(`所有缓存已清理,共删除 ${deletedCount} 个文件`);
} else {
@@ -411,13 +455,14 @@ class ImageCacheService {
*/
async getCacheStats() {
try {
const files = await fs.readdir(this.cacheDir);
let totalSize = 0;
let fileCount = 0;
let errorCount = 0;
let indexSize = this.cacheIndex.size;
for (const file of files) {
const filePath = path.join(this.cacheDir, file);
// 使用索引获取统计信息
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
const filePath = path.join(this.cacheDir, fileInfo.filename);
try {
const stats = await fs.stat(filePath);
totalSize += stats.size;
@@ -425,6 +470,8 @@ class ImageCacheService {
} catch (error) {
if (error.code === 'ENOENT') {
logger.warn(`统计缓存时文件不存在: ${filePath}`);
// 从索引中移除不存在的文件
this.removeFromIndex(cacheKey);
} else {
logger.error(`获取缓存文件统计失败: ${filePath}`, error);
}
@@ -432,6 +479,11 @@ class ImageCacheService {
}
}
// 如果有文件被移除,保存索引
if (errorCount > 0) {
await this.saveCacheIndex();
}
return {
fileCount,
totalSize,
@@ -439,7 +491,8 @@ class ImageCacheService {
maxAge: this.config.maxAge,
enabled: this.config.enabled,
config: this.config,
errorCount
errorCount,
indexSize
};
} catch (error) {
logger.error('获取缓存统计失败:', error);
@@ -450,7 +503,8 @@ class ImageCacheService {
maxAge: this.config.maxAge,
enabled: this.config.enabled,
config: this.config,
errorCount: 0
errorCount: 0,
indexSize: this.cacheIndex.size
};
}
}
@@ -483,6 +537,123 @@ class ImageCacheService {
this.config = { ...this.config, ...defaultConfig };
return defaultConfig;
}
/**
* 加载缓存索引
*/
async loadCacheIndex() {
try {
if (await fs.access(this.indexPath).then(() => true).catch(() => false)) {
const indexData = await fs.readFile(this.indexPath, 'utf8');
const index = JSON.parse(indexData);
this.cacheIndex = new Map(Object.entries(index));
logger.info(`已加载缓存索引,包含 ${this.cacheIndex.size} 个文件记录`);
} else {
logger.info('缓存索引文件不存在,将创建新的索引');
this.cacheIndex = new Map();
}
} catch (error) {
logger.warn('加载缓存索引失败,将创建新的索引:', error.message);
this.cacheIndex = new Map();
}
}
/**
* 保存缓存索引
*/
async saveCacheIndex() {
try {
const indexData = Object.fromEntries(this.cacheIndex);
await fs.writeFile(this.indexPath, JSON.stringify(indexData, null, 2));
} catch (error) {
logger.error('保存缓存索引失败:', error);
}
}
/**
* 验证并同步缓存索引
*/
async validateAndSyncIndex() {
try {
const files = await fs.readdir(this.cacheDir);
const fileSet = new Set(files);
let removedCount = 0;
let addedCount = 0;
// 检查索引中的文件是否实际存在
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
if (!fileSet.has(fileInfo.filename)) {
this.cacheIndex.delete(cacheKey);
removedCount++;
}
}
// 检查实际文件是否在索引中
for (const filename of files) {
const filePath = path.join(this.cacheDir, filename);
try {
const stats = await fs.stat(filePath);
const cacheKey = this.findCacheKeyByFilename(filename);
if (!cacheKey) {
// 文件存在但不在索引中,添加到索引
this.cacheIndex.set(filename, {
filename: filename,
size: stats.size,
mtime: stats.mtime.getTime(),
added: Date.now()
});
addedCount++;
}
} catch (error) {
// 文件不存在,从索引中移除
const cacheKey = this.findCacheKeyByFilename(filename);
if (cacheKey) {
this.cacheIndex.delete(cacheKey);
removedCount++;
}
}
}
if (removedCount > 0 || addedCount > 0) {
logger.info(`缓存索引同步完成: 移除 ${removedCount} 个无效记录,添加 ${addedCount} 个新记录`);
await this.saveCacheIndex();
}
} catch (error) {
logger.error('验证缓存索引失败:', error);
}
}
/**
* 根据文件名查找缓存键
*/
findCacheKeyByFilename(filename) {
for (const [cacheKey, fileInfo] of this.cacheIndex.entries()) {
if (fileInfo.filename === filename) {
return cacheKey;
}
}
return null;
}
/**
* 添加文件到缓存索引
*/
addToIndex(cacheKey, filename, size, mtime) {
this.cacheIndex.set(cacheKey, {
filename: filename,
size: size,
mtime: mtime,
added: Date.now()
});
}
/**
* 从缓存索引中移除文件
*/
removeFromIndex(cacheKey) {
this.cacheIndex.delete(cacheKey);
}
}
module.exports = ImageCacheService;
+11 -2
View File
@@ -86,12 +86,21 @@ process.on('SIGTERM', async () => {
// 处理未捕获的异常
process.on('uncaughtException', error => {
logger.error('❌ 未捕获的异常', error);
logger.error('❌ 异常堆栈:', error.stack);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('❌ 未处理的 Promise 拒绝', reason);
process.exit(1);
logger.error('❌ 未处理的 Promise 拒绝');
logger.error('❌ 拒绝原因:', reason);
if (reason instanceof Error) {
logger.error('❌ 错误堆栈:', reason.stack);
}
logger.error('❌ Promise:', promise);
// 不要立即退出进程,而是记录错误并继续运行
// 这样可以避免因为自动恢复任务的小错误而停止整个服务
logger.warn('⚠️ 继续运行服务器,但建议检查上述错误');
});
// 启动服务器
+55 -10
View File
@@ -112,12 +112,16 @@
<div class="description-content" v-html="artwork.description"></div>
</div>
<!-- 创建时间 -->
<!-- 时间信息 -->
<div class="artwork-meta">
<p>创建时间: {{ formatDate(artwork.create_date) }}</p>
<p v-if="artwork.update_date !== artwork.create_date">
更新时间: {{ formatDate(artwork.update_date) }}
</p>
<div class="meta-item">
<span class="meta-label">创建时间:</span>
<span class="meta-value">{{ formatDate(artwork.create_date) }}</span>
</div>
<div v-if="isValidUpdateDate && artwork.update_date !== artwork.create_date" class="meta-item">
<span class="meta-label">更新时间:</span>
<span class="meta-value">{{ formatDate(artwork.update_date) }}</span>
</div>
</div>
</div>
</template>
@@ -159,9 +163,33 @@ const emit = defineEmits<{
// 使用统一的图片代理函数
const getImageUrl = getImageProxyUrl;
// 检查更新时间是否有效
const isValidUpdateDate = computed(() => {
if (!props.artwork.update_date) return false;
try {
const date = new Date(props.artwork.update_date);
return !isNaN(date.getTime());
} catch (error) {
return false;
}
});
// 格式化日期
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN');
if (!dateString) return '未知时间';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
console.warn('无效的日期格式:', dateString);
return '时间格式错误';
}
return date.toLocaleDateString('zh-CN');
} catch (error) {
console.error('日期格式化错误:', error);
return '时间解析失败';
}
};
// 处理标签点击
@@ -374,12 +402,29 @@ const handleTagClick = (event: MouseEvent, tagName: string) => {
.artwork-meta {
padding-top: 1.5rem;
border-top: 1px solid #e5e7eb;
}
.artwork-meta p {
color: #6b7280;
font-size: 0.875rem;
margin: 0.25rem 0;
}
.meta-item {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.meta-item:last-child {
margin-bottom: 0;
}
.meta-label {
font-weight: 500;
color: #374151;
min-width: 80px;
margin-right: 0.5rem;
}
.meta-value {
color: #6b7280;
}
.artwork-navigation {
+101 -14
View File
@@ -38,18 +38,37 @@
</button>
<div class="download-section">
<div class="download-input-group">
<label for="downloadLimit">下载数量:</label>
<select v-model="downloadLimit" id="downloadLimit" class="download-select">
<option value="10">10</option>
<option value="30">30</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="500">500</option>
<option value="9999">全部</option>
<label for="downloadType">下载方式:</label>
<select v-model="downloadType" id="downloadType" class="download-select"
@change="handleDownloadTypeChange">
<option value="custom">自定义数量</option>
<option value="pages">按页数选择</option>
<option value="all">全部下载</option>
</select>
</div>
<button @click="handleDownloadAll" class="btn btn-secondary" :disabled="downloading">
<!-- 自定义数量输入 -->
<div v-if="downloadType === 'custom'" class="download-input-group">
<label for="customLimit">数量:</label>
<input v-model="customLimit" type="number" id="customLimit" class="download-input" :min="1" :max="9999"
placeholder="输入数量" />
</div>
<!-- 按页数选择 -->
<div v-if="downloadType === 'pages'" class="download-input-group">
<label for="pageLimit">页数:</label>
<select v-model="pageLimit" id="pageLimit" class="download-select">
<option value="1">1 (30)</option>
<option value="2">2 (60)</option>
<option value="3">3 (90)</option>
<option value="5">5 (150)</option>
<option value="10">10 (300)</option>
<option value="20">20 (600)</option>
<option value="50">50 (1500)</option>
</select>
</div>
<button @click="handleDownloadAll" class="btn btn-secondary" :disabled="downloading || !isDownloadValid">
{{ downloading ? '下载中...' : '下载作品' }}
</button>
</div>
@@ -178,7 +197,10 @@ const totalCount = ref(0);
const totalPages = ref(0);
// 下载设置
const downloadLimit = ref('50');
const downloadType = ref<'custom' | 'pages' | 'all'>('custom');
const customLimit = ref('50');
const pageLimit = ref('1');
const downloadLimit = ref('50'); // 保留用于兼容性
// 缓存相关
const cache = ref<Map<string, any>>(new Map());
@@ -203,6 +225,29 @@ const visiblePages = computed(() => {
return pages;
});
// 下载验证
const isDownloadValid = computed(() => {
if (downloadType.value === 'custom') {
const limit = parseInt(customLimit.value);
return !isNaN(limit) && limit > 0 && limit <= 9999;
}
return true;
});
// 获取实际下载数量
const getActualDownloadLimit = () => {
switch (downloadType.value) {
case 'custom':
return parseInt(customLimit.value) || 50;
case 'pages':
return parseInt(pageLimit.value) * 30;
case 'all':
return 9999;
default:
return 50;
}
};
// 缓存键生成
const getCacheKey = (type: string, page: number) => {
return `${route.params.id}_${type}_${page}`;
@@ -369,6 +414,16 @@ const handleTypeChange = () => {
fetchArtworks(1);
};
// 处理下载类型切换
const handleDownloadTypeChange = () => {
// 重置相关值
if (downloadType.value === 'custom') {
customLimit.value = '50';
} else if (downloadType.value === 'pages') {
pageLimit.value = '1';
}
};
// 跳转到指定页面
const goToPage = (page: number) => {
if (page < 1 || page === currentPage.value) return;
@@ -412,15 +467,30 @@ const handleDownloadAll = async () => {
try {
downloading.value = true;
const actualLimit = getActualDownloadLimit();
const response = await downloadService.downloadArtistArtworks(artist.value.id, {
type: artworkType.value,
limit: parseInt(downloadLimit.value)
limit: actualLimit
});
if (response.success) {
console.log('下载任务已创建:', response.data);
const limitText = downloadLimit.value === '9999' ? '全部' : downloadLimit.value;
downloadSuccess.value = `下载任务已创建,将下载 ${limitText} 个作品`;
let limitText = '';
switch (downloadType.value) {
case 'custom':
limitText = `${actualLimit}`;
break;
case 'pages':
limitText = `${pageLimit.value} 页 (${actualLimit} 个)`;
break;
case 'all':
limitText = '全部';
break;
}
downloadSuccess.value = `下载任务已创建,将下载 ${limitText} 作品`;
// 3秒后清除成功提示
setTimeout(() => {
@@ -676,6 +746,23 @@ onMounted(async () => {
min-width: 100px;
}
.download-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background: white;
font-size: 0.875rem;
color: #374151;
min-width: 100px;
transition: border-color 0.2s;
}
.download-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.btn {
display: inline-flex;
align-items: center;