test(图片预加载、缓存): 功能测试

This commit is contained in:
hzj 2025-12-02 16:16:23 +08:00
parent a59c75c57b
commit d53b1daddb
6 changed files with 201 additions and 65 deletions

View File

@ -10,6 +10,7 @@
alt="" alt=""
width="100%" width="100%"
style="display: block" style="display: block"
:crossorigin="isOSSImage ? 'anonymous' : undefined"
/> />
<div <div
v-else v-else
@ -78,6 +79,11 @@ const shouldShowImage = computed(() => {
return isVisible.value return isVisible.value
}) })
// OSS
const isOSSImage = computed(() => {
return props.imgUrl.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')
})
// //
const createObserver = () => { const createObserver = () => {
if (!props.lazy || props.immediate) return if (!props.lazy || props.immediate) return

View File

@ -1,6 +1,8 @@
// src/config/imagePaths.js // src/config/imagePaths.js
export const OSS_BASE_URL = 'https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5/' // 阿里云oss export const OSS_BASE_URL =
// export const OMAN_NATIONAL_DAY_PATH = 'Activities/Oman/NationalDay/'// 文件路径 process.env.NODE_ENV === 'development'
? '/oss/h5/' // 开发环境使用代理
: 'https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5/' // 生产环境直接访问
// 组合使用示例 // 组合使用示例
export const getPngUrl = (imagePath, filename) => { export const getPngUrl = (imagePath, filename) => {

View File

@ -4,21 +4,37 @@ import { imageCacheManager } from '@/utils/imageCacheManager'
export const smartImage = { export const smartImage = {
async mounted(el, binding) { async mounted(el, binding) {
if (binding.value) { if (binding.value) {
// 对于外部链接,直接使用普通加载方式
if (!imageCacheManager.isBuildAsset(binding.value)) {
el.src = binding.value
el.style.opacity = '1'
return
}
// 设置加载状态样式 // 设置加载状态样式
el.style.opacity = '0' el.style.opacity = '0'
el.style.transition = 'opacity 0.3s ease-in-out' el.style.transition = 'opacity 0.3s ease-in-out'
try { try {
await imageCacheManager.loadImage(binding.value) // 对于OSS图片使用特殊处理
el.src = binding.value if (binding.value.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) {
el.style.opacity = '1' 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
}
await imageCacheManager.loadImage(binding.value)
el.src = binding.value
el.style.opacity = '1'
}
} catch (error) { } catch (error) {
console.warn(`图片加载失败: ${binding.value}`, error) console.warn(`图片加载失败: ${binding.value}`, error)
el.src = binding.value el.src = binding.value
@ -29,17 +45,34 @@ export const smartImage = {
async updated(el, binding) { async updated(el, binding) {
if (binding.value !== binding.oldValue && binding.value) { if (binding.value !== binding.oldValue && binding.value) {
// 对于外部链接,直接使用普通加载方式
if (!imageCacheManager.isBuildAsset(binding.value)) {
el.src = binding.value
return
}
el.style.opacity = '0' el.style.opacity = '0'
try { try {
await imageCacheManager.loadImage(binding.value) // 对于OSS图片使用特殊处理
el.src = binding.value if (binding.value.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) {
el.style.opacity = '1' 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
}
await imageCacheManager.loadImage(binding.value)
el.src = binding.value
el.style.opacity = '1'
}
} catch (error) { } catch (error) {
console.warn(`图片更新失败: ${binding.value}`, error) console.warn(`图片更新失败: ${binding.value}`, error)
el.src = binding.value el.src = binding.value

View File

@ -4,8 +4,8 @@ class ImageCacheManager {
this.cacheVersionKey = 'likei-image-cache-version' this.cacheVersionKey = 'likei-image-cache-version'
this.currentAppVersion = import.meta.env.VITE_APP_VERSION || '1.0.0' this.currentAppVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
this.cacheName = `likei-images-v${this.currentAppVersion}` this.cacheName = `likei-images-v${this.currentAppVersion}`
this.maxCacheItems = 50 // 限制最多缓存50张图片可根据需要调整 this.maxCacheItems = 100 // 限制最多缓存50张图片可根据需要调整
this.maxCacheSize = 50 * 1024 * 1024 // 限制总大小50MB可根据需要调整 this.maxCacheSize = 100 * 1024 * 1024 // 限制总大小50MB可根据需要调整
this.init() this.init()
} }
@ -119,7 +119,10 @@ class ImageCacheManager {
// 先检查并清理超出限制的缓存 // 先检查并清理超出限制的缓存
// await this.enforceCacheLimits(cache) // await this.enforceCacheLimits(cache)
const response = await fetch(url) const response = await fetch(url, {
mode: 'cors',
credentials: 'omit',
})
if (response.ok) { if (response.ok) {
await cache.put(url, response.clone()) await cache.put(url, response.clone())
console.log(`图片已缓存: ${url}`) console.log(`图片已缓存: ${url}`)
@ -180,15 +183,42 @@ class ImageCacheManager {
async loadImageFromNetwork(src, resolve, reject) { async loadImageFromNetwork(src, resolve, reject) {
try { try {
const response = await fetch(src) // 对于OSS资源先尝试直接创建Image对象绕过fetch的CORS问题
if (src.startsWith('https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5')) {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
resolve(img)
// 对于OSS资源仍然尝试缓存如果支持
if (this.isBuildAsset(src)) {
this.cacheImageFromImage(src, img)
}
}
img.onerror = (error) => {
console.warn('图片加载失败:', src, error)
reject(error)
}
img.src = src
return
}
// 对于其他资源使用fetch
const response = await fetch(src, {
mode: 'cors',
credentials: 'omit',
})
if (response.ok) { if (response.ok) {
const img = new Image() const img = new Image()
img.onload = async () => { img.onload = async () => {
resolve(img) resolve(img)
// 缓存带有 ETag 的响应 // 缓存带有 ETag 的响应
if (this.isBuildAsset(src)) { if (this.isBuildAsset(src)) {
const cache = await caches.open(this.cacheName) try {
await cache.put(src, response.clone()) await this.cacheImage(src) // 使用原始的缓存方法
} catch (cacheError) {
console.warn('缓存图片时出错:', cacheError)
}
} }
} }
img.onerror = reject img.onerror = reject
@ -197,9 +227,43 @@ class ImageCacheManager {
reject(new Error(`HTTP ${response.status}`)) reject(new Error(`HTTP ${response.status}`))
} }
} catch (error) { } catch (error) {
console.warn('网络加载图片失败:', src, error)
reject(error) reject(error)
} }
} }
// 为OSS图片创建缓存的特殊处理
async cacheImageFromImage(src, img) {
try {
if (!('caches' in window) || !this.isBuildAsset(src)) return
const cache = await caches.open(this.cacheName)
const cachedResponse = await cache.match(src)
if (!cachedResponse) {
// 创建一个模拟的响应来缓存
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
canvas.toBlob(async (blob) => {
if (blob) {
const response = new Response(blob, {
headers: {
'Content-Type': 'image/png',
},
})
await cache.put(src, response.clone())
console.log(`OSS图片已缓存: ${src}`)
}
})
}
} catch (error) {
console.warn('缓存OSS图片失败:', src, error)
}
}
} }
// 导出单例 // 导出单例

View File

@ -1,7 +1,7 @@
<!-- src/views/Activities/heroesDay/index.vue --> <!-- src/views/Activities/Game/index.vue -->
<template> <template>
<div class="fullPage"> <div class="fullPage">
<div class="bg"> <div class="bg" :style="{ '--bg-url': `url(${imageUrl('bg')})` }">
<!-- 状态栏占位区域仅在APP中显示 --> <!-- 状态栏占位区域仅在APP中显示 -->
<div v-if="isInAppEnvironment" style="height: 30px"></div> <div v-if="isInAppEnvironment" style="height: 30px"></div>
@ -20,25 +20,25 @@
<!-- 倒计时 --> <!-- 倒计时 -->
<div style="display: flex; justify-content: center; margin-top: 15vw"> <div style="display: flex; justify-content: center; margin-top: 15vw">
<div style="width: 20%"> <div style="width: 20%">
<itemCenter :imgUrl="images.timeBg"> <itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Days }}D</div> <div class="timeText" style="">{{ Days }}D</div>
</itemCenter> </itemCenter>
</div> </div>
<div class="timeText timeGap"></div> <div class="timeText timeGap"></div>
<div style="width: 20%"> <div style="width: 20%">
<itemCenter :imgUrl="images.timeBg"> <itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Hours }}</div> <div class="timeText" style="">{{ Hours }}</div>
</itemCenter> </itemCenter>
</div> </div>
<div class="timeText timeGap">:</div> <div class="timeText timeGap">:</div>
<div style="width: 20%"> <div style="width: 20%">
<itemCenter :imgUrl="images.timeBg"> <itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Minutes }}</div> <div class="timeText" style="">{{ Minutes }}</div>
</itemCenter> </itemCenter>
</div> </div>
<div class="timeText timeGap">:</div> <div class="timeText timeGap">:</div>
<div style="width: 20%"> <div style="width: 20%">
<itemCenter :imgUrl="images.timeBg"> <itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Second }}</div> <div class="timeText" style="">{{ Second }}</div>
</itemCenter> </itemCenter>
</div> </div>
@ -52,14 +52,14 @@
> >
<img <img
v-smart-img v-smart-img
:src="rankingShow ? images.rankingBtActive : images.rankingBt" :src="rankingShow ? imageUrl('rankingBtActive') : imageUrl('rankingBt')"
alt="" alt=""
style="display: block; width: 40%" style="display: block; width: 40%"
@click="rankingShow = true" @click="rankingShow = true"
/> />
<img <img
v-smart-img v-smart-img
:src="!rankingShow ? images.rewardBtActive : images.rewardsBt" :src="!rankingShow ? imageUrl('rewardBtActive') : imageUrl('rewardsBt')"
alt="" alt=""
style="display: block; width: 40%" style="display: block; width: 40%"
@click="rankingShow = false" @click="rankingShow = false"
@ -71,14 +71,14 @@
> >
<img <img
v-smart-img v-smart-img
:src="rankingShow ? images.rankingBtActiveAr : images.rankingBtAr" :src="rankingShow ? imageUrl('rankingBtActiveAr') : imageUrl('rankingBtAr')"
alt="" alt=""
style="display: block; width: 40%" style="display: block; width: 40%"
@click="rankingShow = true" @click="rankingShow = true"
/> />
<img <img
v-smart-img v-smart-img
:src="!rankingShow ? images.rewardBtActiveAr : images.rewardsBtAr" :src="!rankingShow ? imageUrl('rewardBtActiveAr') : imageUrl('rewardsBtAr')"
alt="" alt=""
style="display: block; width: 40%" style="display: block; width: 40%"
@click="rankingShow = false" @click="rankingShow = false"
@ -90,20 +90,17 @@
<div> <div>
<!-- Ranking --> <!-- Ranking -->
<div v-show="rankingShow" style="position: relative; width: 100%"> <div v-show="rankingShow" style="position: relative; width: 100%">
<!-- 背景图 -->
<!-- <div class="ranking-bg"></div> -->
<div style="position: relative; z-index: 2"> <div style="position: relative; z-index: 2">
<!-- 前三名 --> <!-- 前三名 -->
<itemCenter <itemCenter
:imgUrl="images.RankingMain" :imgUrl="imageUrl('RankingMain')"
:contentStyle="`padding: 25% 5% 0;flex-direction: column;`" :contentStyle="`padding: 25% 5% 0;flex-direction: column;`"
> >
<div> <div>
<!-- 第一 --> <!-- 第一 -->
<div style="display: flex; justify-content: center"> <div style="display: flex; justify-content: center">
<TopUser <TopUser
:BorderImgUrl="images.topOne" :BorderImgUrl="imageUrl('topOne')"
:avatarUrl="RankingHasTop3[0].avatar" :avatarUrl="RankingHasTop3[0].avatar"
:name="RankingHasTop3[0].nickname" :name="RankingHasTop3[0].nickname"
:distributionValue="RankingHasTop3[0].quantity" :distributionValue="RankingHasTop3[0].quantity"
@ -116,7 +113,7 @@
<TopUser <TopUser
:isTopOne="false" :isTopOne="false"
ranking="2" ranking="2"
:BorderImgUrl="images.topTwo" :BorderImgUrl="imageUrl('topTwo')"
:avatarUrl="RankingHasTop3[1].avatar" :avatarUrl="RankingHasTop3[1].avatar"
:name="RankingHasTop3[1].nickname" :name="RankingHasTop3[1].nickname"
:distributionValue="RankingHasTop3[1].quantity" :distributionValue="RankingHasTop3[1].quantity"
@ -126,7 +123,7 @@
<TopUser <TopUser
:isTopOne="false" :isTopOne="false"
ranking="3" ranking="3"
:BorderImgUrl="images.topThree" :BorderImgUrl="imageUrl('topThree')"
:avatarUrl="RankingHasTop3[2].avatar" :avatarUrl="RankingHasTop3[2].avatar"
:name="RankingHasTop3[2].nickname" :name="RankingHasTop3[2].nickname"
:distributionValue="RankingHasTop3[2].quantity" :distributionValue="RankingHasTop3[2].quantity"
@ -143,9 +140,12 @@
style="margin-bottom: 6vw; position: relative; z-index: 2" style="margin-bottom: 6vw; position: relative; z-index: 2"
> >
<itemCenter <itemCenter
v-for="listItem in showRanking" v-for="(listItem, index) in showRanking"
:imgUrl="images.RankingItem" :key="listItem.userId"
:imgUrl="imageUrl('RankingItem')"
:contentStyle="`padding: 2% 13% 0`" :contentStyle="`padding: 2% 13% 0`"
:lazy="true"
:immediate="index < 2"
@click="viewUserInfo(listItem.userId)" @click="viewUserInfo(listItem.userId)"
> >
<!-- 基本信息 --> <!-- 基本信息 -->
@ -228,7 +228,7 @@
<!-- 底框 --> <!-- 底框 -->
<img <img
v-smart-img v-smart-img
:src="images.RankingBottomBorder" :src="imageUrl('RankingBottomBorder')"
alt="" alt=""
width="100%" width="100%"
style="display: block; margin-top: -40vw; position: relative; z-index: 1" style="display: block; margin-top: -40vw; position: relative; z-index: 1"
@ -255,7 +255,7 @@
:key="rewardIndex" :key="rewardIndex"
style="width: 100%" style="width: 100%"
> >
<itemCenter :imgUrl="images.giftItemBg"> <itemCenter :imgUrl="imageUrl('giftItemBg')">
<img <img
v-smart-img v-smart-img
:src="reward.cover || ''" :src="reward.cover || ''"
@ -298,7 +298,7 @@
<!-- 我的排名 --> <!-- 我的排名 -->
<itemCenter <itemCenter
style="position: fixed; bottom: 0; z-index: 2" style="position: fixed; bottom: 0; z-index: 2"
:imgUrl="images.myRankingBg" :imgUrl="imageUrl('myRankingBg')"
:contentStyle="`padding: 2% 8% 0`" :contentStyle="`padding: 2% 8% 0`"
> >
<!-- 基本信息 --> <!-- 基本信息 -->
@ -380,14 +380,14 @@
<img <img
v-if="currentLangType == 'en'" v-if="currentLangType == 'en'"
v-smart-img v-smart-img
:src="images.helpInfo" :src="imageUrl('helpInfo')"
alt="" alt=""
style="display: block; width: 100%" style="display: block; width: 100%"
/> />
<img <img
v-if="currentLangType == 'ar'" v-if="currentLangType == 'ar'"
v-smart-img v-smart-img
:src="images.helpInfoAr" :src="imageUrl('helpInfoAr')"
alt="" alt=""
style="display: block; width: 100%" style="display: block; width: 100%"
/> />
@ -402,6 +402,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { isInApp, viewUserInfo } from '@/utils/appBridge.js' import { isInApp, viewUserInfo } from '@/utils/appBridge.js'
import { connectToApp } from '@/utils/appConnector.js' import { connectToApp } from '@/utils/appConnector.js'
import { useLangStore } from '@/stores/lang' import { useLangStore } from '@/stores/lang'
import { getPngUrl } from '@/config/imagePaths.js'
import { preloadImages } from '@/utils/imagePreloader.js'
import { getThisWeekRewardsAndGifts, getRankingListAndMyRanking } from '@/api/activity.js' import { getThisWeekRewardsAndGifts, getRankingListAndMyRanking } from '@/api/activity.js'
@ -409,17 +411,8 @@ import itemCenter from '@/components/itemCenter.vue'
import TopUser from './components/topUser.vue' import TopUser from './components/topUser.vue'
import maskLayer from '@/components/MaskLayer.vue' import maskLayer from '@/components/MaskLayer.vue'
// vite // OSSURL
const imageModules = import.meta.glob('@/assets/images/Activities/Game/*.{png,jpg,svg}', { const imageUrl = (filename) => getPngUrl('Activities/Game/', filename)
eager: true,
})
//
const images = Object.fromEntries(
Object.entries(imageModules).map(([path, module]) => {
const fileName = path.split('/').pop().split('.')[0]
return [fileName, module.default]
})
)
const isInAppEnvironment = ref(false) // APP const isInAppEnvironment = ref(false) // APP
const maskLayerShow = ref(false) // const maskLayerShow = ref(false) //
@ -444,7 +437,7 @@ const Ranking = ref([]) // 排行榜
const myRanking = ref({}) // const myRanking = ref({}) //
const rewardsList = ref([]) // const rewardsList = ref([]) //
const topImg = ref([images.top1RewardBg, images.top2RewardBg, images.top3RewardBg]) // top const topImg = ref([imageUrl('top1RewardBg'), imageUrl('top2RewardBg'), imageUrl('top3RewardBg')]) // top
// 3 // 3
const RankingHasTop3 = computed(() => { const RankingHasTop3 = computed(() => {
@ -463,7 +456,6 @@ const RankingHasTop3 = computed(() => {
// //
const showRanking = computed(() => { const showRanking = computed(() => {
return Ranking.value.filter((_, index) => index >= 3) return Ranking.value.filter((_, index) => index >= 3)
// return []
}) })
// //
@ -586,11 +578,43 @@ const initData = () => {
getRewardsAndGifts() // getRewardsAndGifts() //
} }
//
const preloadCriticalImages = () => {
const criticalImages = [
imageUrl('bg'),
imageUrl('timeBg'),
imageUrl('rankingBt'),
imageUrl('rankingBtActive'),
imageUrl('rankingBtAr'),
imageUrl('rankingBtActiveAr'),
imageUrl('rewardBtActive'),
imageUrl('rewardBtActiveAr'),
imageUrl('rewardsBt'),
imageUrl('rewardsBtAr'),
imageUrl('RankingMain'),
imageUrl('topOne'),
imageUrl('topTwo'),
imageUrl('topThree'),
imageUrl('RankingItem'),
imageUrl('RankingBottomBorder'),
imageUrl('myRankingBg'),
imageUrl('helpInfo'),
imageUrl('helpInfoAr'),
imageUrl('top1RewardBg'),
imageUrl('top2RewardBg'),
imageUrl('top3RewardBg'),
imageUrl('giftItemBg'),
]
preloadImages(criticalImages, { useSmartCache: true })
}
// 使APP // 使APP
const connectToAppHandler = async () => { const connectToAppHandler = async () => {
await connectToApp(() => { await connectToApp(() => {
// //
initData() // initData() //
preloadCriticalImages() //
}) })
} }
@ -622,7 +646,7 @@ onUnmounted(() => {
.bg { .bg {
width: 100vw; width: 100vw;
min-height: 100vh; min-height: 100vh;
background-image: url(../../../assets/images/Activities/Game/bg.png); background-image: var(--bg-url);
background-size: 100% auto; background-size: 100% auto;
background-repeat: no-repeat; background-repeat: no-repeat;
position: relative; position: relative;

View File

@ -45,6 +45,13 @@ export default defineConfig({
cors: true, // 启用CORS cors: true, // 启用CORS
// 代理配置(如果需要) // 代理配置(如果需要)
proxy: { proxy: {
// OSS 代理配置
'/oss': {
target: 'https://tkm-likei.oss-ap-southeast-1.aliyuncs.com',
changeOrigin: true,
secure: true,
rewrite: (path) => path.replace(/^\/oss/, ''),
},
// '/api': { // '/api': {
// target: 'http://localhost:3000', // target: 'http://localhost:3000',
// changeOrigin: true, // changeOrigin: true,