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

View File

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

View File

@ -4,21 +4,37 @@ 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'
// 对于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
}
await imageCacheManager.loadImage(binding.value)
el.src = binding.value
el.style.opacity = '1'
}
} catch (error) {
console.warn(`图片加载失败: ${binding.value}`, error)
el.src = binding.value
@ -29,17 +45,34 @@ export const smartImage = {
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'
// 对于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
}
await imageCacheManager.loadImage(binding.value)
el.src = binding.value
el.style.opacity = '1'
}
} catch (error) {
console.warn(`图片更新失败: ${binding.value}`, error)
el.src = binding.value

View File

@ -4,8 +4,8 @@ class ImageCacheManager {
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.maxCacheItems = 100 // 限制最多缓存50张图片可根据需要调整
this.maxCacheSize = 100 * 1024 * 1024 // 限制总大小50MB可根据需要调整
this.init()
}
@ -119,7 +119,10 @@ class ImageCacheManager {
// 先检查并清理超出限制的缓存
// await this.enforceCacheLimits(cache)
const response = await fetch(url)
const response = await fetch(url, {
mode: 'cors',
credentials: 'omit',
})
if (response.ok) {
await cache.put(url, response.clone())
console.log(`图片已缓存: ${url}`)
@ -180,15 +183,42 @@ class ImageCacheManager {
async loadImageFromNetwork(src, resolve, reject) {
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) {
const img = new Image()
img.onload = async () => {
resolve(img)
// 缓存带有 ETag 的响应
if (this.isBuildAsset(src)) {
const cache = await caches.open(this.cacheName)
await cache.put(src, response.clone())
try {
await this.cacheImage(src) // 使用原始的缓存方法
} catch (cacheError) {
console.warn('缓存图片时出错:', cacheError)
}
}
}
img.onerror = reject
@ -197,9 +227,43 @@ class ImageCacheManager {
reject(new Error(`HTTP ${response.status}`))
}
} catch (error) {
console.warn('网络加载图片失败:', src, 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>
<div class="fullPage">
<div class="bg">
<div class="bg" :style="{ '--bg-url': `url(${imageUrl('bg')})` }">
<!-- 状态栏占位区域仅在APP中显示 -->
<div v-if="isInAppEnvironment" style="height: 30px"></div>
@ -20,25 +20,25 @@
<!-- 倒计时 -->
<div style="display: flex; justify-content: center; margin-top: 15vw">
<div style="width: 20%">
<itemCenter :imgUrl="images.timeBg">
<itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Days }}D</div>
</itemCenter>
</div>
<div class="timeText timeGap"></div>
<div style="width: 20%">
<itemCenter :imgUrl="images.timeBg">
<itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Hours }}</div>
</itemCenter>
</div>
<div class="timeText timeGap">:</div>
<div style="width: 20%">
<itemCenter :imgUrl="images.timeBg">
<itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Minutes }}</div>
</itemCenter>
</div>
<div class="timeText timeGap">:</div>
<div style="width: 20%">
<itemCenter :imgUrl="images.timeBg">
<itemCenter :imgUrl="imageUrl('timeBg')">
<div class="timeText" style="">{{ Second }}</div>
</itemCenter>
</div>
@ -52,14 +52,14 @@
>
<img
v-smart-img
:src="rankingShow ? images.rankingBtActive : images.rankingBt"
:src="rankingShow ? imageUrl('rankingBtActive') : imageUrl('rankingBt')"
alt=""
style="display: block; width: 40%"
@click="rankingShow = true"
/>
<img
v-smart-img
:src="!rankingShow ? images.rewardBtActive : images.rewardsBt"
:src="!rankingShow ? imageUrl('rewardBtActive') : imageUrl('rewardsBt')"
alt=""
style="display: block; width: 40%"
@click="rankingShow = false"
@ -71,14 +71,14 @@
>
<img
v-smart-img
:src="rankingShow ? images.rankingBtActiveAr : images.rankingBtAr"
:src="rankingShow ? imageUrl('rankingBtActiveAr') : imageUrl('rankingBtAr')"
alt=""
style="display: block; width: 40%"
@click="rankingShow = true"
/>
<img
v-smart-img
:src="!rankingShow ? images.rewardBtActiveAr : images.rewardsBtAr"
:src="!rankingShow ? imageUrl('rewardBtActiveAr') : imageUrl('rewardsBtAr')"
alt=""
style="display: block; width: 40%"
@click="rankingShow = false"
@ -90,20 +90,17 @@
<div>
<!-- Ranking -->
<div v-show="rankingShow" style="position: relative; width: 100%">
<!-- 背景图 -->
<!-- <div class="ranking-bg"></div> -->
<div style="position: relative; z-index: 2">
<!-- 前三名 -->
<itemCenter
:imgUrl="images.RankingMain"
:imgUrl="imageUrl('RankingMain')"
:contentStyle="`padding: 25% 5% 0;flex-direction: column;`"
>
<div>
<!-- 第一 -->
<div style="display: flex; justify-content: center">
<TopUser
:BorderImgUrl="images.topOne"
:BorderImgUrl="imageUrl('topOne')"
:avatarUrl="RankingHasTop3[0].avatar"
:name="RankingHasTop3[0].nickname"
:distributionValue="RankingHasTop3[0].quantity"
@ -116,7 +113,7 @@
<TopUser
:isTopOne="false"
ranking="2"
:BorderImgUrl="images.topTwo"
:BorderImgUrl="imageUrl('topTwo')"
:avatarUrl="RankingHasTop3[1].avatar"
:name="RankingHasTop3[1].nickname"
:distributionValue="RankingHasTop3[1].quantity"
@ -126,7 +123,7 @@
<TopUser
:isTopOne="false"
ranking="3"
:BorderImgUrl="images.topThree"
:BorderImgUrl="imageUrl('topThree')"
:avatarUrl="RankingHasTop3[2].avatar"
:name="RankingHasTop3[2].nickname"
:distributionValue="RankingHasTop3[2].quantity"
@ -143,9 +140,12 @@
style="margin-bottom: 6vw; position: relative; z-index: 2"
>
<itemCenter
v-for="listItem in showRanking"
:imgUrl="images.RankingItem"
v-for="(listItem, index) in showRanking"
:key="listItem.userId"
:imgUrl="imageUrl('RankingItem')"
:contentStyle="`padding: 2% 13% 0`"
:lazy="true"
:immediate="index < 2"
@click="viewUserInfo(listItem.userId)"
>
<!-- 基本信息 -->
@ -228,7 +228,7 @@
<!-- 底框 -->
<img
v-smart-img
:src="images.RankingBottomBorder"
:src="imageUrl('RankingBottomBorder')"
alt=""
width="100%"
style="display: block; margin-top: -40vw; position: relative; z-index: 1"
@ -255,7 +255,7 @@
:key="rewardIndex"
style="width: 100%"
>
<itemCenter :imgUrl="images.giftItemBg">
<itemCenter :imgUrl="imageUrl('giftItemBg')">
<img
v-smart-img
:src="reward.cover || ''"
@ -298,7 +298,7 @@
<!-- 我的排名 -->
<itemCenter
style="position: fixed; bottom: 0; z-index: 2"
:imgUrl="images.myRankingBg"
:imgUrl="imageUrl('myRankingBg')"
:contentStyle="`padding: 2% 8% 0`"
>
<!-- 基本信息 -->
@ -380,14 +380,14 @@
<img
v-if="currentLangType == 'en'"
v-smart-img
:src="images.helpInfo"
:src="imageUrl('helpInfo')"
alt=""
style="display: block; width: 100%"
/>
<img
v-if="currentLangType == 'ar'"
v-smart-img
:src="images.helpInfoAr"
:src="imageUrl('helpInfoAr')"
alt=""
style="display: block; width: 100%"
/>
@ -402,6 +402,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { isInApp, viewUserInfo } from '@/utils/appBridge.js'
import { connectToApp } from '@/utils/appConnector.js'
import { useLangStore } from '@/stores/lang'
import { getPngUrl } from '@/config/imagePaths.js'
import { preloadImages } from '@/utils/imagePreloader.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 maskLayer from '@/components/MaskLayer.vue'
// vite
const imageModules = import.meta.glob('@/assets/images/Activities/Game/*.{png,jpg,svg}', {
eager: true,
})
//
const images = Object.fromEntries(
Object.entries(imageModules).map(([path, module]) => {
const fileName = path.split('/').pop().split('.')[0]
return [fileName, module.default]
})
)
// OSSURL
const imageUrl = (filename) => getPngUrl('Activities/Game/', filename)
const isInAppEnvironment = ref(false) // APP
const maskLayerShow = ref(false) //
@ -444,7 +437,7 @@ const Ranking = ref([]) // 排行榜
const myRanking = 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
const RankingHasTop3 = computed(() => {
@ -463,7 +456,6 @@ const RankingHasTop3 = computed(() => {
//
const showRanking = computed(() => {
return Ranking.value.filter((_, index) => index >= 3)
// return []
})
//
@ -586,11 +578,43 @@ const initData = () => {
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
const connectToAppHandler = async () => {
await connectToApp(() => {
//
initData() //
preloadCriticalImages() //
})
}
@ -622,7 +646,7 @@ onUnmounted(() => {
.bg {
width: 100vw;
min-height: 100vh;
background-image: url(../../../assets/images/Activities/Game/bg.png);
background-image: var(--bg-url);
background-size: 100% auto;
background-repeat: no-repeat;
position: relative;

View File

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