作品缩略图懒加载避免卡死,修复缓存文件清理bug
This commit is contained in:
@@ -263,13 +263,22 @@ class ImageCacheService {
|
||||
// 计算总大小和收集文件信息
|
||||
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
|
||||
});
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
totalSize += stats.size;
|
||||
fileStats.push({
|
||||
path: filePath,
|
||||
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());
|
||||
|
||||
for (const file of fileStats) {
|
||||
await fs.unlink(file.path);
|
||||
totalSize -= file.size;
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
totalSize -= file.size;
|
||||
|
||||
if (totalSize <= this.config.maxSize * 0.8) { // 清理到80%
|
||||
break;
|
||||
if (totalSize <= this.config.maxSize * 0.8) { // 清理到80%
|
||||
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) {
|
||||
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();
|
||||
if (age > this.config.maxAge) {
|
||||
await fs.unlink(filePath);
|
||||
cleanedCount++;
|
||||
const age = Date.now() - stats.mtime.getTime();
|
||||
if (age > this.config.maxAge) {
|
||||
try {
|
||||
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() {
|
||||
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);
|
||||
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) {
|
||||
logger.error('清理所有缓存失败:', error);
|
||||
throw error;
|
||||
@@ -363,12 +414,22 @@ class ImageCacheService {
|
||||
const files = await fs.readdir(this.cacheDir);
|
||||
let totalSize = 0;
|
||||
let fileCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(this.cacheDir, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
totalSize += stats.size;
|
||||
fileCount++;
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
totalSize += stats.size;
|
||||
fileCount++;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
logger.warn(`统计缓存时文件不存在: ${filePath}`);
|
||||
} else {
|
||||
logger.error(`获取缓存文件统计失败: ${filePath}`, error);
|
||||
}
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -377,7 +438,8 @@ class ImageCacheService {
|
||||
maxSize: this.config.maxSize,
|
||||
maxAge: this.config.maxAge,
|
||||
enabled: this.config.enabled,
|
||||
config: this.config
|
||||
config: this.config,
|
||||
errorCount
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('获取缓存统计失败:', error);
|
||||
@@ -387,7 +449,8 @@ class ImageCacheService {
|
||||
maxSize: this.config.maxSize,
|
||||
maxAge: this.config.maxAge,
|
||||
enabled: this.config.enabled,
|
||||
config: this.config
|
||||
config: this.config,
|
||||
errorCount: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,25 @@
|
||||
<!-- 多页作品缩略图 -->
|
||||
<div v-if="artwork.page_count > 1" class="thumbnails">
|
||||
<button v-for="(page, index) in artwork.meta_pages" :key="index" @click="$emit('pageChange', index)"
|
||||
class="thumbnail" :class="{ active: currentPage === index }">
|
||||
<img :src="getImageUrl(page.image_urls.square_medium)" :alt="`第 ${index + 1} 页`" crossorigin="anonymous" />
|
||||
class="thumbnail" :class="{ active: currentPage === index }" ref="thumbnailRefs">
|
||||
<!-- 懒加载的缩略图 -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { getImageProxyUrl } from '@/services/api';
|
||||
import type { Artwork } from '@/types';
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
|
||||
@@ -49,6 +59,11 @@ const emit = defineEmits<{
|
||||
const imageLoaded = ref(false);
|
||||
const imageError = ref(false);
|
||||
|
||||
// 懒加载相关
|
||||
const thumbnailRefs = ref<HTMLElement[]>([]);
|
||||
const visibleThumbnails = ref<Set<number>>(new Set());
|
||||
let intersectionObserver: IntersectionObserver | null = null;
|
||||
|
||||
// 计算当前图片URL
|
||||
const currentImageUrl = computed(() => {
|
||||
if (!props.artwork) return '';
|
||||
@@ -65,16 +80,84 @@ const currentImageUrl = computed(() => {
|
||||
// 使用统一的图片代理函数
|
||||
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, () => {
|
||||
imageLoaded.value = false;
|
||||
imageError.value = false;
|
||||
});
|
||||
|
||||
// 监听作品变化,重置图片加载状态
|
||||
// 监听作品变化,重置图片加载状态和懒加载
|
||||
watch(() => props.artwork.id, () => {
|
||||
imageLoaded.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>
|
||||
|
||||
@@ -171,6 +254,21 @@ watch(() => props.artwork.id, () => {
|
||||
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) {
|
||||
.thumbnails {
|
||||
padding: 0.5rem;
|
||||
|
||||
Reference in New Issue
Block a user