作品缩略图懒加载避免卡死,修复缓存文件清理bug
This commit is contained in:
@@ -263,6 +263,7 @@ 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);
|
||||||
|
try {
|
||||||
const stats = await fs.stat(filePath);
|
const stats = await fs.stat(filePath);
|
||||||
totalSize += stats.size;
|
totalSize += stats.size;
|
||||||
fileStats.push({
|
fileStats.push({
|
||||||
@@ -270,6 +271,14 @@ class ImageCacheService {
|
|||||||
size: stats.size,
|
size: stats.size,
|
||||||
mtime: stats.mtime
|
mtime: stats.mtime
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 如果文件不存在,记录日志但继续处理其他文件
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
logger.warn(`缓存文件不存在,跳过: ${filePath}`);
|
||||||
|
} else {
|
||||||
|
logger.error(`检查缓存文件失败: ${filePath}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果超过最大大小,删除最旧的文件
|
// 如果超过最大大小,删除最旧的文件
|
||||||
@@ -280,12 +289,21 @@ 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) {
|
||||||
|
try {
|
||||||
await fs.unlink(file.path);
|
await fs.unlink(file.path);
|
||||||
totalSize -= file.size;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`缓存清理完成,当前大小: ${totalSize}`);
|
logger.info(`缓存清理完成,当前大小: ${totalSize}`);
|
||||||
@@ -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);
|
||||||
|
try {
|
||||||
const stats = await fs.stat(filePath);
|
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) {
|
||||||
|
try {
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
cleanedCount++;
|
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);
|
||||||
|
try {
|
||||||
await fs.unlink(filePath);
|
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);
|
||||||
|
try {
|
||||||
const stats = await fs.stat(filePath);
|
const stats = await fs.stat(filePath);
|
||||||
totalSize += stats.size;
|
totalSize += stats.size;
|
||||||
fileCount++;
|
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user