作品缩略图懒加载避免卡死,修复缓存文件清理bug

This commit is contained in:
2025-09-01 12:51:18 +08:00
parent ff05567e6b
commit a5f38a4eed
2 changed files with 190 additions and 29 deletions
+86 -23
View File
@@ -263,13 +263,22 @@ class ImageCacheService {
// 计算总大小和收集文件信息 // 计算总大小和收集文件信息
for (const file of files) { for (const file of files) {
const filePath = path.join(this.cacheDir, file); const filePath = path.join(this.cacheDir, file);
const stats = await fs.stat(filePath); try {
totalSize += stats.size; const stats = await fs.stat(filePath);
fileStats.push({ totalSize += stats.size;
path: filePath, fileStats.push({
size: stats.size, path: filePath,
mtime: stats.mtime size: stats.size,
}); mtime: stats.mtime
});
} catch (error) {
// 如果文件不存在,记录日志但继续处理其他文件
if (error.code === 'ENOENT') {
logger.warn(`缓存文件不存在,跳过: ${filePath}`);
} else {
logger.error(`检查缓存文件失败: ${filePath}`, error);
}
}
} }
// 如果超过最大大小,删除最旧的文件 // 如果超过最大大小,删除最旧的文件
@@ -280,11 +289,20 @@ class ImageCacheService {
fileStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime()); fileStats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
for (const file of fileStats) { for (const file of fileStats) {
await fs.unlink(file.path); try {
totalSize -= file.size; await fs.unlink(file.path);
totalSize -= file.size;
if (totalSize <= this.config.maxSize * 0.8) { // 清理到80% if (totalSize <= this.config.maxSize * 0.8) { // 清理到80%
break; break;
}
} catch (error) {
// 如果删除文件失败,记录日志但继续处理其他文件
if (error.code === 'ENOENT') {
logger.warn(`删除缓存文件时文件不存在: ${file.path}`);
} else {
logger.error(`删除缓存文件失败: ${file.path}`, error);
}
} }
} }
@@ -306,12 +324,29 @@ class ImageCacheService {
for (const file of files) { for (const file of files) {
const filePath = path.join(this.cacheDir, file); const filePath = path.join(this.cacheDir, file);
const stats = await fs.stat(filePath); try {
const stats = await fs.stat(filePath);
const age = Date.now() - stats.mtime.getTime(); const age = Date.now() - stats.mtime.getTime();
if (age > this.config.maxAge) { if (age > this.config.maxAge) {
await fs.unlink(filePath); try {
cleanedCount++; await fs.unlink(filePath);
cleanedCount++;
} catch (deleteError) {
if (deleteError.code === 'ENOENT') {
logger.warn(`删除过期缓存文件时文件不存在: ${filePath}`);
} else {
logger.error(`删除过期缓存文件失败: ${filePath}`, deleteError);
}
}
}
} catch (error) {
// 如果文件不存在,记录日志但继续处理其他文件
if (error.code === 'ENOENT') {
logger.warn(`过期缓存文件不存在,跳过: ${filePath}`);
} else {
logger.error(`检查过期缓存文件失败: ${filePath}`, error);
}
} }
} }
@@ -341,13 +376,29 @@ class ImageCacheService {
async clearAllCache() { async clearAllCache() {
try { try {
const files = await fs.readdir(this.cacheDir); const files = await fs.readdir(this.cacheDir);
let deletedCount = 0;
let errorCount = 0;
for (const file of files) { for (const file of files) {
const filePath = path.join(this.cacheDir, file); const filePath = path.join(this.cacheDir, file);
await fs.unlink(filePath); try {
await fs.unlink(filePath);
deletedCount++;
} catch (error) {
if (error.code === 'ENOENT') {
logger.warn(`清理缓存时文件不存在: ${filePath}`);
} else {
logger.error(`删除缓存文件失败: ${filePath}`, error);
errorCount++;
}
}
} }
logger.info('所有缓存已清理'); if (errorCount === 0) {
logger.info(`所有缓存已清理,共删除 ${deletedCount} 个文件`);
} else {
logger.warn(`缓存清理完成,成功删除 ${deletedCount} 个文件,失败 ${errorCount} 个文件`);
}
} catch (error) { } catch (error) {
logger.error('清理所有缓存失败:', error); logger.error('清理所有缓存失败:', error);
throw error; throw error;
@@ -363,12 +414,22 @@ class ImageCacheService {
const files = await fs.readdir(this.cacheDir); const files = await fs.readdir(this.cacheDir);
let totalSize = 0; let totalSize = 0;
let fileCount = 0; let fileCount = 0;
let errorCount = 0;
for (const file of files) { for (const file of files) {
const filePath = path.join(this.cacheDir, file); const filePath = path.join(this.cacheDir, file);
const stats = await fs.stat(filePath); try {
totalSize += stats.size; const stats = await fs.stat(filePath);
fileCount++; totalSize += stats.size;
fileCount++;
} catch (error) {
if (error.code === 'ENOENT') {
logger.warn(`统计缓存时文件不存在: ${filePath}`);
} else {
logger.error(`获取缓存文件统计失败: ${filePath}`, error);
}
errorCount++;
}
} }
return { return {
@@ -377,7 +438,8 @@ class ImageCacheService {
maxSize: this.config.maxSize, maxSize: this.config.maxSize,
maxAge: this.config.maxAge, maxAge: this.config.maxAge,
enabled: this.config.enabled, enabled: this.config.enabled,
config: this.config config: this.config,
errorCount
}; };
} catch (error) { } catch (error) {
logger.error('获取缓存统计失败:', error); logger.error('获取缓存统计失败:', error);
@@ -387,7 +449,8 @@ class ImageCacheService {
maxSize: this.config.maxSize, maxSize: this.config.maxSize,
maxAge: this.config.maxAge, maxAge: this.config.maxAge,
enabled: this.config.enabled, enabled: this.config.enabled,
config: this.config config: this.config,
errorCount: 0
}; };
} }
} }
+102 -4
View File
@@ -18,15 +18,25 @@
<!-- 多页作品缩略图 --> <!-- 多页作品缩略图 -->
<div v-if="artwork.page_count > 1" class="thumbnails"> <div v-if="artwork.page_count > 1" class="thumbnails">
<button v-for="(page, index) in artwork.meta_pages" :key="index" @click="$emit('pageChange', index)" <button v-for="(page, index) in artwork.meta_pages" :key="index" @click="$emit('pageChange', index)"
class="thumbnail" :class="{ active: currentPage === index }"> class="thumbnail" :class="{ active: currentPage === index }" ref="thumbnailRefs">
<img :src="getImageUrl(page.image_urls.square_medium)" :alt="`第 ${index + 1} 页`" crossorigin="anonymous" /> <!-- 懒加载的缩略图 -->
<img v-if="visibleThumbnails.has(index)"
:src="getImageUrl(page.image_urls.square_medium)"
:alt="`第 ${index + 1} 页`"
crossorigin="anonymous" />
<!-- 占位符 -->
<div v-else class="thumbnail-placeholder">
<div class="placeholder-content">
<span>{{ index + 1 }}</span>
</div>
</div>
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue'; import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getImageProxyUrl } from '@/services/api'; import { getImageProxyUrl } from '@/services/api';
import type { Artwork } from '@/types'; import type { Artwork } from '@/types';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
@@ -49,6 +59,11 @@ const emit = defineEmits<{
const imageLoaded = ref(false); const imageLoaded = ref(false);
const imageError = ref(false); const imageError = ref(false);
// 懒加载相关
const thumbnailRefs = ref<HTMLElement[]>([]);
const visibleThumbnails = ref<Set<number>>(new Set());
let intersectionObserver: IntersectionObserver | null = null;
// 计算当前图片URL // 计算当前图片URL
const currentImageUrl = computed(() => { const currentImageUrl = computed(() => {
if (!props.artwork) return ''; if (!props.artwork) return '';
@@ -65,16 +80,84 @@ const currentImageUrl = computed(() => {
// 使用统一的图片代理函数 // 使用统一的图片代理函数
const getImageUrl = getImageProxyUrl; const getImageUrl = getImageProxyUrl;
// 初始化懒加载
const initLazyLoading = () => {
// 清理之前的观察器
if (intersectionObserver) {
intersectionObserver.disconnect();
}
// 创建新的 Intersection Observer
intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const index = parseInt(entry.target.getAttribute('data-index') || '0');
visibleThumbnails.value.add(index);
}
});
}, {
root: null,
rootMargin: '50px', // 提前50px开始加载
threshold: 0.1
});
// 观察所有缩略图
thumbnailRefs.value.forEach((el, index) => {
if (el) {
el.setAttribute('data-index', index.toString());
intersectionObserver?.observe(el);
}
});
};
// 监听页面变化,重置图片加载状态 // 监听页面变化,重置图片加载状态
watch(() => props.currentPage, () => { watch(() => props.currentPage, () => {
imageLoaded.value = false; imageLoaded.value = false;
imageError.value = false; imageError.value = false;
}); });
// 监听作品变化,重置图片加载状态 // 监听作品变化,重置图片加载状态和懒加载
watch(() => props.artwork.id, () => { watch(() => props.artwork.id, () => {
imageLoaded.value = false; imageLoaded.value = false;
imageError.value = false; imageError.value = false;
// 重置懒加载状态
visibleThumbnails.value.clear();
// 重新初始化懒加载
setTimeout(() => {
initLazyLoading();
}, 100);
});
// 监听当前页面变化,确保当前页面的缩略图可见
watch(() => props.currentPage, (newPage) => {
// 确保当前页面的缩略图可见
visibleThumbnails.value.add(newPage);
});
// 监听缩略图引用变化
watch(thumbnailRefs, () => {
if (thumbnailRefs.value.length > 0) {
initLazyLoading();
}
}, { deep: true });
onMounted(() => {
// 确保当前页面的缩略图可见
visibleThumbnails.value.add(props.currentPage);
// 初始化懒加载
setTimeout(() => {
initLazyLoading();
}, 100);
});
onUnmounted(() => {
// 清理观察器
if (intersectionObserver) {
intersectionObserver.disconnect();
intersectionObserver = null;
}
}); });
</script> </script>
@@ -171,6 +254,21 @@ watch(() => props.artwork.id, () => {
object-fit: cover; object-fit: cover;
} }
.thumbnail-placeholder {
width: 100%;
height: 100%;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
}
.placeholder-content {
color: #9ca3af;
font-size: 0.75rem;
font-weight: 500;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.thumbnails { .thumbnails {
padding: 0.5rem; padding: 0.5rem;