From 0e24e75b62d632b0185ed80df53e2b77c6d05bec Mon Sep 17 00:00:00 2001 From: hzj <1304805162@qq.com> Date: Thu, 4 Dec 2025 17:18:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E7=89=87=E3=80=81=E7=89=88?= =?UTF-8?q?=E6=9C=AC):=20=E6=9B=B4=E6=96=B0=E5=9B=BE=E7=89=87=E7=9A=84?= =?UTF-8?q?=E9=A2=84=E5=8A=A0=E8=BD=BD=E3=80=81=E7=BC=93=E5=AD=98=E3=80=81?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=8C=87=E4=BB=A4=E3=80=81=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E7=9A=84=E7=94=9F=E6=88=90=E5=92=8C=E6=A3=80?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 9 ++- scripts/generate-version.js | 11 +++- src/directives/smartImage.js | 60 +++++------------- src/main.js | 8 +-- src/utils/imageCacheManager.js | 96 +++++++++++++++++++++++++---- src/utils/imagePreloader.js | 25 +++----- src/utils/versionChecker.js | 109 +++++++++++++++++++++++++++++---- 7 files changed, 228 insertions(+), 90 deletions(-) diff --git a/.env b/.env index c6551cc..13e48a1 100644 --- a/.env +++ b/.env @@ -1,8 +1,11 @@ # 应用版本号 -VITE_APP_VERSION=1.6.8 +VITE_APP_VERSION=1.6.9 -# 构建时间戳(会在构建时自动更新) -VITE_BUILD_TIME="" +# 版本类型 +# major:重大更新(清空用户缓存) +# minor:一般更新(可选择不清空缓存) +# patch:日常小更新(不清空用户缓存) +VITE_VERSION_TYPE=major # 是否启用版本检查 VITE_ENABLE_VERSION_CHECK=true diff --git a/scripts/generate-version.js b/scripts/generate-version.js index debd352..c7e6a82 100644 --- a/scripts/generate-version.js +++ b/scripts/generate-version.js @@ -10,14 +10,20 @@ const __dirname = dirname(__filename) // 读取 .env 文件获取版本号 const envPath = resolve(__dirname, '../.env') let version = '1.0.0' +let versionType = 'patch' try { if (existsSync(envPath)) { const envContent = readFileSync(envPath, 'utf-8') const versionMatch = envContent.match(/VITE_APP_VERSION=([^\r\n]+)/) + const typeMatch = envContent.match(/VITE_VERSION_TYPE=([^\r\n]+)/) + if (versionMatch && versionMatch[1]) { version = versionMatch[1].replace(/['"]/g, '') } + if (typeMatch && typeMatch[1]) { + versionType = typeMatch[1].replace(/['"]/g, '') + } } } catch (error) { console.warn('无法读取 .env 文件,使用默认版本号:', error.message) @@ -26,6 +32,7 @@ try { // 生成版本信息 const versionInfo = { version: version, + type: versionType, // 新增版本类型 buildTime: new Date().toISOString(), timestamp: Date.now(), } @@ -40,4 +47,6 @@ if (!existsSync(distDir)) { const outputPath = resolve(distDir, 'version.json') writeFileSync(outputPath, JSON.stringify(versionInfo, null, 2)) -console.log(`✅ 版本文件已生成: ${versionInfo.version} (${versionInfo.buildTime})`) +console.log( + `✅ 版本文件已生成: ${versionInfo.version}-${versionInfo.type} (${versionInfo.buildTime})` +) diff --git a/src/directives/smartImage.js b/src/directives/smartImage.js index de736c1..117e898 100644 --- a/src/directives/smartImage.js +++ b/src/directives/smartImage.js @@ -3,37 +3,25 @@ import { imageCacheManager } from '@/utils/imageCacheManager' export const smartImage = { async mounted(el, binding) { + // 只有当绑定的值存在时才处理 if (binding.value) { // 设置加载状态样式 el.style.opacity = '0' el.style.transition = 'opacity 0.3s ease-in-out' - try { - // 对于OSS图片使用特殊处理 - if (binding.value.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) { - const img = new Image() - img.crossOrigin = 'anonymous' - img.onload = () => { - el.src = binding.value - el.style.opacity = '1' - } - img.onerror = (error) => { - console.warn(`图片加载失败: ${binding.value}`, error) - el.src = binding.value - el.style.opacity = '1' - } - img.src = binding.value - } else { - // 对于外部链接,直接使用普通加载方式 - if (!imageCacheManager.isBuildAsset(binding.value)) { - el.src = binding.value - el.style.opacity = '1' - return - } + // 等待DOM更新确保样式生效 + await nextTick() + try { + // 统一使用缓存管理器处理所有可缓存资源 + if (imageCacheManager.isBuildAsset(binding.value)) { await imageCacheManager.loadImage(binding.value) el.src = binding.value el.style.opacity = '1' + } else { + // 外部链接直接加载 + el.src = binding.value + el.style.opacity = '1' } } catch (error) { console.warn(`图片加载失败: ${binding.value}`, error) @@ -47,31 +35,17 @@ export const smartImage = { if (binding.value !== binding.oldValue && binding.value) { el.style.opacity = '0' - try { - // 对于OSS图片使用特殊处理 - if (binding.value.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) { - const img = new Image() - img.crossOrigin = 'anonymous' - img.onload = () => { - el.src = binding.value - el.style.opacity = '1' - } - img.onerror = (error) => { - console.warn(`图片更新失败: ${binding.value}`, error) - el.src = binding.value - el.style.opacity = '1' - } - img.src = binding.value - } else { - // 对于外部链接,直接使用普通加载方式 - if (!imageCacheManager.isBuildAsset(binding.value)) { - el.src = binding.value - return - } + // 等待DOM更新确保样式生效 + await nextTick() + try { + if (imageCacheManager.isBuildAsset(binding.value)) { await imageCacheManager.loadImage(binding.value) el.src = binding.value el.style.opacity = '1' + } else { + el.src = binding.value + el.style.opacity = '1' } } catch (error) { console.warn(`图片更新失败: ${binding.value}`, error) diff --git a/src/main.js b/src/main.js index c4cc2cf..0086a96 100644 --- a/src/main.js +++ b/src/main.js @@ -97,13 +97,13 @@ app.use(i18n) app.mount('#app') -// 定期检查版本更新(每5分钟) +// 定时检查逻辑 if (versionChecker.enableCheck) { setInterval(async () => { - const hasUpdate = await versionChecker.checkServerVersion() - if (hasUpdate) { + const versionResult = await versionChecker.checkServerVersion() + if (versionResult.shouldUpdate) { // 自动处理更新,不需要用户交互 - versionChecker.handleVersionUpdate() + versionChecker.handleVersionUpdate(versionResult.versionType) } }, 5 * 60 * 1000) } diff --git a/src/utils/imageCacheManager.js b/src/utils/imageCacheManager.js index 2f7cfaa..05624fa 100644 --- a/src/utils/imageCacheManager.js +++ b/src/utils/imageCacheManager.js @@ -3,12 +3,60 @@ 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.imageCacheName = `likei-images-v${this.currentAppVersion}` // OSS图片 + this.assetCacheName = `likei-assets-v${this.currentAppVersion}` // 本地assets + this.maxCacheItems = 100 // 最大缓存数量(100张) this.maxCacheSize = 100 * 1024 * 1024 // 最大缓存大小(100MB) + + this.pendingRequests = new Map() // 请求去重的关键 + this.init() } + // 根据资源类型选择缓存 + async getCacheForUrl(url) { + if (url.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) { + return await caches.open(this.imageCacheName) + } else { + return await caches.open(this.assetCacheName) + } + } + + // 选择性清除缓存 + async clearCacheSelective(clearType = 'images') { + if (!('caches' in window)) return + + try { + const cacheNames = await caches.keys() + let cachesToClear = [] + + switch (clearType) { + case 'images': + cachesToClear = cacheNames.filter((name) => name.startsWith('likei-images-v')) + break + case 'assets': + cachesToClear = cacheNames.filter((name) => name.startsWith('likei-assets-v')) + break + case 'all': + // 清除所有相关缓存(除了动态内容) + cachesToClear = cacheNames.filter( + (name) => name.startsWith('likei-images-v') || name.startsWith('likei-assets-v') + ) + break + default: + // 默认只清除图片缓存 + cachesToClear = cacheNames.filter((name) => name.startsWith('likei-images-v')) + } + + await Promise.all(cachesToClear.map((name) => caches.delete(name))) + console.log(`已清理${clearType}类型缓存:`, cachesToClear) + } catch (error) { + console.warn('清理缓存失败:', error) + } + } + async init() { await this.checkVersionAndUpdate() } @@ -126,7 +174,7 @@ class ImageCacheManager { try { const cacheNames = await caches.keys() const oldCaches = cacheNames.filter( - (name) => name.startsWith('likei-images-v') && name !== this.cacheName + (name) => name.startsWith('likei-images-v') && name !== this.imageCacheName ) await Promise.all(oldCaches.map((name) => caches.delete(name))) @@ -173,14 +221,13 @@ class ImageCacheManager { } } - // 缓存图片 + // 分离缓存图片 async cacheImage(url) { if (!('caches' in window) || !url) return try { - // 只缓存本地构建后的资源,避免缓存外部网络图片 if (this.isBuildAsset(url)) { - const cache = await caches.open(this.cacheName) + const cache = await this.getCacheForUrl(url) // 使用分离的缓存 const cachedResponse = await cache.match(url) if (!cachedResponse) { @@ -209,7 +256,7 @@ class ImageCacheManager { try { // 只从缓存获取本地资源 if (this.isBuildAsset(url)) { - const cache = await caches.open(this.cacheName) + const cache = await caches.open(this.imageCacheName) return await cache.match(url) } return null @@ -221,32 +268,59 @@ class ImageCacheManager { // 智能加载图片 async loadImage(src) { - return new Promise(async (resolve, reject) => { + // 1. 请求去重:检查是否有相同请求正在进行 + if (this.pendingRequests.has(src)) { + // 返回现有的Promise,避免重复请求 + return this.pendingRequests.get(src) + } + + // 2. 创建新的加载Promise + const loadPromise = new Promise(async (resolve, reject) => { try { - // 首先尝试从缓存获取(仅限本地资源) + // 3. 尝试从缓存获取 const cachedResponse = await this.getCachedImage(src) if (cachedResponse) { + // 4. 缓存命中,处理图片 const blob = await cachedResponse.blob() const imageUrl = URL.createObjectURL(blob) + + // 5. 创建Image对象并设置src const img = new Image() img.onload = () => { + // 6. 加载成功,清理临时URL并resolve URL.revokeObjectURL(imageUrl) resolve(img) } + + // 7. 关键:加载失败时的处理 img.onerror = () => { + // 8. 清理临时URL URL.revokeObjectURL(imageUrl) + // 9. 回退到网络加载 this.loadImageFromNetwork(src, resolve, reject) } - img.src = imageUrl + + img.src = imageUrl // 10. 开始加载 return } - // 缓存中没有或不是本地资源,从网络加载 + // 11. 缓存中没有或不是本地资源,从网络加载 this.loadImageFromNetwork(src, resolve, reject) } catch (error) { this.loadImageFromNetwork(src, resolve, reject) } }) + + // 12. 记录正在进行的请求 + this.pendingRequests.set(src, loadPromise) + + try { + const result = await loadPromise + return result + } finally { + // 13. 清理记录 + this.pendingRequests.delete(src) + } } // 从网络加载图片 @@ -307,7 +381,7 @@ class ImageCacheManager { // 检查浏览器是否支持缓存API以及资源是否适合缓存 if (!('caches' in window) || !this.isBuildAsset(src)) return - const cache = await caches.open(this.cacheName) + const cache = await caches.open(this.imageCacheName) const cachedResponse = await cache.match(src) // 只有当图片尚未缓存时才进行缓存操作 diff --git a/src/utils/imagePreloader.js b/src/utils/imagePreloader.js index e08e0d5..cc53301 100644 --- a/src/utils/imagePreloader.js +++ b/src/utils/imagePreloader.js @@ -1,8 +1,7 @@ // src/utils/imagePreloader.js import { imageCacheManager } from '@/utils/imageCacheManager' -export const preloadImages = (sources, options = {}) => { - const { useSmartCache = false } = options +export const preloadImages = (sources, options) => { let urls = [] // 支持数组和对象形式的图片源 @@ -12,23 +11,15 @@ export const preloadImages = (sources, options = {}) => { urls = Object.values(sources) } - if (useSmartCache) { - // 使用 SmartImage 缓存系统 - urls.forEach((url) => { - if (imageCacheManager.isBuildAsset(url)) { - imageCacheManager.loadImage(url) // 触发缓存 - } else { - const img = new Image() - img.src = url - } - }) - } else { - // 使用浏览器默认缓存 - urls.forEach((url) => { + // 使用 SmartImage 缓存系统 + urls.forEach((url) => { + if (imageCacheManager.isBuildAsset(url)) { + imageCacheManager.loadImage(url) // 触发缓存加载 + } else { const img = new Image() img.src = url - }) - } + } + }) console.log(`开始预加载 ${urls.length} 张图片`) } diff --git a/src/utils/versionChecker.js b/src/utils/versionChecker.js index ca806bc..665b1f4 100644 --- a/src/utils/versionChecker.js +++ b/src/utils/versionChecker.js @@ -1,4 +1,13 @@ // 版本检查工具 + +const PERSISTENT_KEYS = [ + 'preferred-language', // 语言设置 + 'userProfile', // 用户资料 + 'teamInfo', // 团队信息 + 'lastImgUrl', // 最后图片URL + 'applyInfo', // 申请资料 +] + export class VersionChecker { constructor() { this.currentVersion = import.meta.env.VITE_APP_VERSION || Date.now().toString() @@ -30,6 +39,7 @@ export class VersionChecker { const versionInfo = await response.json() const serverVersion = versionInfo.version + const versionType = versionInfo.type || 'patch' // 获取版本类型 const storedServerVersion = localStorage.getItem(this.serverVersionKey) // 更新服务器版本信息 @@ -37,23 +47,28 @@ export class VersionChecker { // 比较版本 if (storedServerVersion && storedServerVersion !== serverVersion) { - console.log(`检测到服务器版本更新: ${storedServerVersion} -> ${serverVersion}`) - return true + console.log(`检测到${versionType}版本更新: ${storedServerVersion} -> ${serverVersion}`) + + // 返回版本类型信息 + return { + shouldUpdate: true, + versionType: versionType, + } } - return false + return { shouldUpdate: false } } catch (error) { console.warn('版本检查失败:', error) - return false + return { shouldUpdate: false } } } // 检查版本更新(综合方法) async checkVersion() { // 首先检查服务器版本 - const hasServerUpdate = await this.checkServerVersion() + const versionResult = await this.checkServerVersion() - if (hasServerUpdate) { + if (versionResult.shouldUpdate) { this.handleVersionUpdate() return true } @@ -71,9 +86,28 @@ export class VersionChecker { } // 处理版本更新 - handleVersionUpdate() { - console.log('检测到新版本,正在清理缓存...') - this.clearCache() + handleVersionUpdate(updateType = 'major') { + console.log(`检测到${updateType}版本更新,正在清理缓存...`) + + // 根据更新类型决定清除策略 + switch (updateType) { + case 'major': + // 重大更新:清除所有缓存 + this.clearCache() + break + case 'minor': + // 一般更新:只清除图片和assets缓存 + this.clearCacheSelective('all') + break + case 'patch': + // 小更新:只清除图片缓存 + this.clearCacheSelective('images') + break + default: + // 默认:清除所有缓存 + this.clearCache() + } + localStorage.setItem(this.storageKey, this.currentVersion) localStorage.setItem(this.serverVersionKey, this.currentVersion) @@ -81,13 +115,66 @@ export class VersionChecker { this.forceReload() } + // 添加选择性清除缓存方法 + clearCacheSelective(clearType = 'images') { + // 保存需要保留的数据 + const preservedData = {} + + PERSISTENT_KEYS.forEach((key) => { + const value = localStorage.getItem(key) + if (value !== null) { + preservedData[key] = value + } + }) + + // 清除sessionStorage + sessionStorage.clear() + + // 清除指定类型的缓存 + if ('caches' in window) { + caches + .keys() + .then((names) => { + let cachesToClear = [] + switch (clearType) { + case 'images': + cachesToClear = names.filter((name) => name.startsWith('likei-images-v')) + break + case 'assets': + cachesToClear = names.filter((name) => name.startsWith('likei-assets-v')) + break + case 'all': + cachesToClear = names.filter( + (name) => name.startsWith('likei-images-v') || name.startsWith('likei-assets-v') + ) + break + default: + cachesToClear = names.filter((name) => name.startsWith('likei-images-v')) + } + + cachesToClear.forEach((name) => { + caches.delete(name) + }) + + console.log(`已选择性清除${clearType}缓存:`, cachesToClear) + }) + .catch((err) => { + console.warn('清除缓存失败:', err) + }) + } + + // 恢复保留的数据 + Object.keys(preservedData).forEach((key) => { + localStorage.setItem(key, preservedData[key]) + }) + } + // 清除应用缓存 clearCache() { // 保存需要保留的数据 const preservedData = {} - const keysToPreserve = ['user_token', 'user_info', 'lang'] - keysToPreserve.forEach((key) => { + PERSISTENT_KEYS.forEach((key) => { const value = localStorage.getItem(key) if (value !== null) { preservedData[key] = value