From d9cf63af3d1eea5359a88f1816e51e7d97763fd4 Mon Sep 17 00:00:00 2001 From: hzj <1304805162@qq.com> Date: Tue, 18 Nov 2025 15:30:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E7=89=87=E7=BC=93=E5=AD=98):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=B9=E5=8A=A0=E8=BD=BD=E8=BF=87=E7=9A=84?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=BF=9B=E8=A1=8C=E7=BC=93=E5=AD=98=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8F=AA=E6=9C=89=E5=9C=A8=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=96=B0=E7=89=88=E6=9C=AC=E6=97=B6=E6=89=8D=E4=BC=9A?= =?UTF-8?q?=E6=B8=85=E7=A9=BA=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/directives/smartImage.js | 50 +++++++++ src/main.js | 4 + src/utils/imageCacheManager.js | 189 +++++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 src/directives/smartImage.js create mode 100644 src/utils/imageCacheManager.js diff --git a/src/directives/smartImage.js b/src/directives/smartImage.js new file mode 100644 index 0000000..38ba326 --- /dev/null +++ b/src/directives/smartImage.js @@ -0,0 +1,50 @@ +// src/directives/smartImage.js +import { imageCacheManager } from '@/utils/imageCacheManager' + +export const smartImage = { + async mounted(el, binding) { + if (binding.value) { + // 对于外部链接,直接使用普通加载方式 + if (!imageCacheManager.isBuildAsset(binding.value)) { + el.src = binding.value + el.style.opacity = '1' + return + } + + // 设置加载状态样式 + el.style.opacity = '0' + el.style.transition = 'opacity 0.3s ease-in-out' + + try { + await imageCacheManager.loadImage(binding.value) + el.src = binding.value + el.style.opacity = '1' + } catch (error) { + console.warn(`图片加载失败: ${binding.value}`, error) + el.src = binding.value + el.style.opacity = '1' + } + } + }, + + async updated(el, binding) { + if (binding.value !== binding.oldValue && binding.value) { + // 对于外部链接,直接使用普通加载方式 + if (!imageCacheManager.isBuildAsset(binding.value)) { + el.src = binding.value + return + } + + el.style.opacity = '0' + try { + await imageCacheManager.loadImage(binding.value) + el.src = binding.value + el.style.opacity = '1' + } catch (error) { + console.warn(`图片更新失败: ${binding.value}`, error) + el.src = binding.value + el.style.opacity = '1' + } + } + }, +} diff --git a/src/main.js b/src/main.js index 47ee3cc..cfe228d 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ import './assets/main.css' import { logEnvInfo } from './utils/env.js' import { versionChecker } from './utils/versionChecker.js' +import { smartImage } from './directives/smartImage.js' import { createPinia } from 'pinia' import { createApp } from 'vue' import i18n, { initI18n } from './locales/i18n' @@ -20,6 +21,9 @@ console.debug('🔐 Permission system initialized') const pinia = createPinia() const app = createApp(App) +// 注册智能图片指令 +app.directive('smart-img', smartImage) + app.use(router) app.use(pinia) diff --git a/src/utils/imageCacheManager.js b/src/utils/imageCacheManager.js new file mode 100644 index 0000000..bc18703 --- /dev/null +++ b/src/utils/imageCacheManager.js @@ -0,0 +1,189 @@ +// src/utils/imageCacheManager.js +class ImageCacheManager { + constructor() { + this.cacheVersionKey = 'likei-image-cache-version' + this.currentAppVersion = import.meta.env.VITE_APP_VERSION || '1.0.0' + this.cacheName = `likei-images-v${this.currentAppVersion}` + this.maxCacheItems = 50 // 限制最多缓存50张图片(可根据需要调整) + this.maxCacheSize = 50 * 1024 * 1024 // 限制总大小50MB(可根据需要调整) + this.init() + } + + async init() { + await this.checkVersionAndUpdate() + } + + async checkVersionAndUpdate() { + const cachedVersion = localStorage.getItem(this.cacheVersionKey) + + if (!cachedVersion) { + localStorage.setItem(this.cacheVersionKey, this.currentAppVersion) + console.log('初始化图片缓存版本:', this.currentAppVersion) + return + } + + if (cachedVersion !== this.currentAppVersion) { + console.log(`图片缓存版本更新: ${cachedVersion} -> ${this.currentAppVersion}`) + await this.clearOldCaches() + localStorage.setItem(this.cacheVersionKey, this.currentAppVersion) + } + } + + // 检查并清理超出限制的缓存 + async enforceCacheLimits(cache) { + try { + const keys = await cache.keys() + + // 检查数量限制 + if (keys.length > this.maxCacheItems) { + // 删除最旧的缓存项 + const itemsToRemove = keys.length - this.maxCacheItems + for (let i = 0; i < itemsToRemove; i++) { + await cache.delete(keys[i]) + console.log(`已删除超出限制的缓存项: ${keys[i].url}`) + } + } + + // 可选:检查大小限制(需要估算) + // 这里简化处理,实际项目中可以更精确计算 + } catch (error) { + console.warn('检查缓存限制时出错:', error) + } + } + + async clearOldCaches() { + if (!('caches' in window)) return + + try { + const cacheNames = await caches.keys() + const oldCaches = cacheNames.filter( + (name) => name.startsWith('likei-images-v') && name !== this.cacheName + ) + + await Promise.all(oldCaches.map((name) => caches.delete(name))) + console.log('已清理旧版本图片缓存:', oldCaches) + } catch (error) { + console.warn('清理旧图片缓存失败:', error) + } + } + + // 更严格的判断是否为本地构建资源 + isBuildAsset(url) { + try { + // 如果是相对路径,肯定是本地资源 + if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) { + return true + } + + // 如果是绝对URL,检查是否为本地域名 + const currentOrigin = window.location.origin + if (url.startsWith(currentOrigin)) { + return true + } + + // 检查是否为构建后的资源(包含哈希) + if (/\.[a-f0-9]{8,}\./.test(url)) { + return true + } + + // 检查是否为本地assets路径 + if (url.includes('/src/assets/') || url.includes('/assets/')) { + return true + } + + return false + } catch (error) { + console.warn('判断资源类型时出错:', error) + return false + } + } + + // 缓存图片 + async cacheImage(url) { + if (!('caches' in window) || !url) return + + try { + // 只缓存本地构建后的资源,避免缓存外部网络图片 + if (this.isBuildAsset(url)) { + const cache = await caches.open(this.cacheName) + const cachedResponse = await cache.match(url) + + if (!cachedResponse) { + // 先检查并清理超出限制的缓存 + // await this.enforceCacheLimits(cache) + + const response = await fetch(url) + if (response.ok) { + await cache.put(url, response.clone()) + console.log(`图片已缓存: ${url}`) + } + } + } + } catch (error) { + console.warn(`缓存图片失败 ${url}:`, error) + } + } + + // 从缓存获取图片 + async getCachedImage(url) { + if (!('caches' in window)) return null + + try { + // 只从缓存获取本地资源 + if (this.isBuildAsset(url)) { + const cache = await caches.open(this.cacheName) + return await cache.match(url) + } + return null + } catch (error) { + console.warn('获取缓存图片失败:', error) + return null + } + } + + // 智能加载图片 + async loadImage(src) { + return new Promise(async (resolve, reject) => { + try { + // 首先尝试从缓存获取(仅限本地资源) + const cachedResponse = await this.getCachedImage(src) + if (cachedResponse) { + const blob = await cachedResponse.blob() + const imageUrl = URL.createObjectURL(blob) + const img = new Image() + img.onload = () => { + URL.revokeObjectURL(imageUrl) + resolve(img) + } + img.onerror = () => { + URL.revokeObjectURL(imageUrl) + this.loadImageFromNetwork(src, resolve, reject) + } + img.src = imageUrl + return + } + + // 缓存中没有或不是本地资源,从网络加载 + this.loadImageFromNetwork(src, resolve, reject) + } catch (error) { + this.loadImageFromNetwork(src, resolve, reject) + } + }) + } + + async loadImageFromNetwork(src, resolve, reject) { + const img = new Image() + img.onload = async () => { + resolve(img) + // 只有本地资源才缓存 + if (this.isBuildAsset(src)) { + await this.cacheImage(src) + } + } + img.onerror = reject + img.src = src + } +} + +// 导出单例 +export const imageCacheManager = new ImageCacheManager()