test(图片网络加载): 网络加载图片、结合缓存、懒加载、预加载功能试点

This commit is contained in:
hzj 2025-12-02 15:27:51 +08:00
parent ca5ffdc042
commit 846debb8da
5 changed files with 201 additions and 95 deletions

View File

@ -1,7 +1,26 @@
<!-- src/components/itemCenter.vue -->
<template> <template>
<div style="position: relative; width: 100%"> <div style="position: relative; width: 100%" ref="containerRef">
<!-- 背景图 --> <!-- 背景图 -->
<img v-smart-img :src="imgUrl" :key="imgUrl" alt="" width="100%" style="display: block" /> <img
v-if="shouldShowImage"
v-smart-img
:src="imgUrl"
:key="imgUrl"
alt=""
width="100%"
style="display: block"
/>
<div
v-else
class="placeholder"
:style="{
width: '100%',
height: '100%',
backgroundColor: '#f0f0f0',
}"
></div>
<!-- 内容 --> <!-- 内容 -->
<div <div
style=" style="
@ -19,18 +38,76 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue'
const props = defineProps({ const props = defineProps({
imgUrl: { imgUrl: {
type: String, type: String,
required: true, required: true,
}, },
//
contentStyle: { contentStyle: {
type: String, type: String,
default: '', default: '',
}, },
//
lazy: {
type: Boolean,
default: false,
},
// lazy
immediate: {
type: Boolean,
default: false,
},
})
const containerRef = ref(null)
const isVisible = ref(false)
//
const shouldShowImage = computed(() => {
//
if (props.immediate || !props.lazy) {
return true
}
//
return isVisible.value
})
//
const createObserver = () => {
if (!props.lazy || props.immediate) return
if (!containerRef.value) return
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
isVisible.value = true
observer.unobserve(entry.target)
}
})
},
{
rootMargin: '100px', // 100px
}
)
observer.observe(containerRef.value)
}
onMounted(() => {
createObserver()
}) })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.placeholder {
transition: opacity 0.3s ease;
}
</style>

8
src/config/imagePaths.js Normal file
View File

@ -0,0 +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 getPngUrl = (imagePath, filename) => {
return `${OSS_BASE_URL}${imagePath}${filename}.png`
}

View File

@ -13,6 +13,7 @@ class ImageCacheManager {
await this.checkVersionAndUpdate() await this.checkVersionAndUpdate()
} }
// 版本检查逻辑
async checkVersionAndUpdate() { async checkVersionAndUpdate() {
const cachedVersion = localStorage.getItem(this.cacheVersionKey) const cachedVersion = localStorage.getItem(this.cacheVersionKey)
@ -91,6 +92,12 @@ class ImageCacheManager {
return true return true
} }
// 新增:允许缓存特定域名下的资源
const ossBaseUrl = 'https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5'
if (url.startsWith(ossBaseUrl)) {
return true
}
return false return false
} catch (error) { } catch (error) {
console.warn('判断资源类型时出错:', error) console.warn('判断资源类型时出错:', error)
@ -172,16 +179,26 @@ class ImageCacheManager {
} }
async loadImageFromNetwork(src, resolve, reject) { async loadImageFromNetwork(src, resolve, reject) {
const img = new Image() try {
img.onload = async () => { const response = await fetch(src)
resolve(img) if (response.ok) {
// 只有本地资源才缓存 const img = new Image()
if (this.isBuildAsset(src)) { img.onload = async () => {
await this.cacheImage(src) resolve(img)
// 缓存带有 ETag 的响应
if (this.isBuildAsset(src)) {
const cache = await caches.open(this.cacheName)
await cache.put(src, response.clone())
}
}
img.onerror = reject
img.src = URL.createObjectURL(await response.blob())
} else {
reject(new Error(`HTTP ${response.status}`))
} }
} catch (error) {
reject(error)
} }
img.onerror = reject
img.src = src
} }
} }

View File

@ -1,55 +1,33 @@
// src/utils/imagePreloader.js // src/utils/imagePreloader.js
import { imageCacheManager } from '@/utils/imageCacheManager'
/** export const preloadImages = (sources, options = {}) => {
* 简单图片预加载函数 const { useSmartCache = false } = options
* @param {Array|string|Object} sources - 图片源数组字符串或对象
*/
export const preloadImages = (sources) => {
let urls = [] let urls = []
if (Array.isArray(sources)) { if (Array.isArray(sources)) {
urls = sources urls = sources
} else if (typeof sources === 'string') {
urls = [sources]
} else if (typeof sources === 'object') { } else if (typeof sources === 'object') {
urls = Object.values(sources) urls = Object.values(sources)
} }
urls.forEach((url) => { if (useSmartCache) {
const img = new Image() // 使用 SmartImage 缓存系统
img.src = url urls.forEach((url) => {
}) if (imageCacheManager.isBuildAsset(url)) {
imageCacheManager.loadImage(url) // 触发缓存
} else {
const img = new Image()
img.src = url
}
})
} else {
// 使用浏览器默认缓存
urls.forEach((url) => {
const img = new Image()
img.src = url
})
}
console.log(`开始预加载 ${urls.length} 张图片`) console.log(`开始预加载 ${urls.length} 张图片`)
} }
/**
* 带进度监控的图片预加载函数
* @param {Array|string|Object} sources - 图片源
* @param {Function} onProgress - 进度回调
*/
export const preloadImagesWithProgress = (sources, onProgress) => {
let urls = []
if (Array.isArray(sources)) {
urls = sources
} else if (typeof sources === 'string') {
urls = [sources]
} else if (typeof sources === 'object') {
urls = Object.values(sources)
}
let loaded = 0
const total = urls.length
urls.forEach((url) => {
const img = new Image()
img.onload = img.onerror = () => {
loaded++
if (onProgress) {
onProgress({ loaded, total, url })
}
}
img.src = url
})
}

View File

@ -1,10 +1,10 @@
<!-- src/views/Activities/heroesDay/index.vue --> <!-- src/views/Activities/Oman/NationalDay/index.vue -->
<template> <template>
<div class="fullPage"> <div class="fullPage">
<!-- 新增的背景图层 --> <!-- 新增的背景图层 -->
<div class="background-overlay"></div> <div class="background-overlay"></div>
<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>
@ -23,25 +23,25 @@
<!-- 倒计时 --> <!-- 倒计时 -->
<div style="display: flex; justify-content: center; gap: 4px; margin-top: 1vw"> <div style="display: flex; justify-content: center; gap: 4px; margin-top: 1vw">
<div style="width: 20%"> <div style="width: 20%">
<itemCenter :imgUrl="images.timeDayBg"> <itemCenter :imgUrl="imageUrl('timeDayBg')">
<div class="timeText" style="margin-bottom: 20%">{{ Days }}D</div> <div class="timeText" style="margin-bottom: 20%">{{ Days }}D</div>
</itemCenter> </itemCenter>
</div> </div>
<div class="timeText timeGap" style="width: 6px"></div> <div class="timeText timeGap" style="width: 6px"></div>
<div style="width: 20%"> <div style="width: 20%">
<itemCenter :imgUrl="images.timeHourBg"> <itemCenter :imgUrl="imageUrl('timeHourBg')">
<div class="timeText" style="margin-bottom: 20%">{{ Hours }}</div> <div class="timeText" style="margin-bottom: 20%">{{ 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.timeMinBg"> <itemCenter :imgUrl="imageUrl('timeMinBg')">
<div class="timeText" style="margin-bottom: 20%">{{ Minutes }}</div> <div class="timeText" style="margin-bottom: 20%">{{ 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.timeSecBg"> <itemCenter :imgUrl="imageUrl('timeSecBg')">
<div class="timeText" style="margin-bottom: 20%">{{ Second }}</div> <div class="timeText" style="margin-bottom: 20%">{{ Second }}</div>
</itemCenter> </itemCenter>
</div> </div>
@ -49,21 +49,22 @@
<!-- 每周礼物 --> <!-- 每周礼物 -->
<div style="display: flex; justify-content: center; margin-top: 10px"> <div style="display: flex; justify-content: center; margin-top: 10px">
<itemCenter :imgUrl="images.giftsBg" :contentStyle="'paddingTop: 10%;'"> <itemCenter :imgUrl="imageUrl('giftsBg')" :contentStyle="'paddingTop: 10%;'">
<div style="width: 85%; display: flex; justify-content: space-around; margin-top: 10%"> <div style="width: 85%; display: flex; justify-content: space-around; margin-top: 10%">
<div <div
v-for="(gift, index) in gifts" v-for="(gift, index) in gifts"
:key="index" :key="index"
style="width: 30%; display: flex; flex-direction: column" style="width: 30%; display: flex; flex-direction: column"
> >
<itemCenter :imgUrl="images.giftItemBg" <itemCenter :imgUrl="imageUrl('giftItemBg')">
><img <img
v-smart-img v-smart-img
:src="gift.giftPhoto" :src="gift.giftPhoto"
alt="" alt=""
width="70%" width="70%"
style="display: block; object-fit: cover" style="display: block; object-fit: cover"
/></itemCenter> />
</itemCenter>
<div style="display: flex; justify-content: center; align-items: center; gap: 5px"> <div style="display: flex; justify-content: center; align-items: center; gap: 5px">
<img <img
v-smart-img v-smart-img
@ -84,14 +85,14 @@
<div style="display: flex; justify-content: space-around"> <div style="display: flex; justify-content: space-around">
<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"
@ -113,12 +114,12 @@
></div> ></div>
<div style="position: relative; z-index: 1"> <div style="position: relative; z-index: 1">
<!-- 前三名 --> <!-- 前三名 -->
<itemCenter :imgUrl="images.RankingMain" :contentStyle="`padding: 25% 7% 10%`"> <itemCenter :imgUrl="imageUrl('RankingMain')" :contentStyle="`padding: 25% 7% 10%`">
<div style="margin-top: 13%"> <div style="margin-top: 13%">
<!-- 第一 --> <!-- 第一 -->
<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"
@ -131,7 +132,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"
@ -141,7 +142,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"
@ -156,8 +157,11 @@
<div v-if="showRanking.length > 0" style="margin-bottom: 6vw; margin-top: -10vw"> <div v-if="showRanking.length > 0" style="margin-bottom: 6vw; margin-top: -10vw">
<itemCenter <itemCenter
v-for="listItem in showRanking" v-for="listItem in showRanking"
:imgUrl="images.RankingItem" :key="listItem.userId"
:imgUrl="imageUrl('RankingItem')"
:contentStyle="`padding: 0 10%`" :contentStyle="`padding: 0 10%`"
:lazy="true"
:immediate="index < 2"
@click="viewUserInfo(listItem.userId)" @click="viewUserInfo(listItem.userId)"
> >
<div style="width: 75%; min-width: 0; display: flex; align-items: center"> <div style="width: 75%; min-width: 0; display: flex; align-items: center">
@ -223,7 +227,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: -22vw; position: relative; z-index: 9" style="display: block; margin-top: -22vw; position: relative; z-index: 9"
@ -250,7 +254,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 || ''"
@ -284,7 +288,7 @@
background: linear-gradient(0deg, #6e1116 0%, #6e1116 100%), #d9d9d9; background: linear-gradient(0deg, #6e1116 0%, #6e1116 100%), #d9d9d9;
z-index: 2; z-index: 2;
" "
:imgUrl="images.myRankingBg" :imgUrl="imageUrl('myRankingBg')"
:contentStyle="`padding: 2% 5% 0`" :contentStyle="`padding: 2% 5% 0`"
> >
<div <div
@ -360,7 +364,7 @@
<maskLayer :maskLayerShow="maskLayerShow" @click="maskLayerShow = false"> <maskLayer :maskLayerShow="maskLayerShow" @click="maskLayerShow = false">
<!-- help弹窗 --> <!-- help弹窗 -->
<div style="margin: 20% 0" @click.stop> <div style="margin: 20% 0" @click.stop>
<img v-smart-img :src="images.helpInfo" alt="" style="display: block; width: 100%" /> <img v-smart-img :src="imageUrl('helpInfo')" alt="" style="display: block; width: 100%" />
</div> </div>
</maskLayer> </maskLayer>
</div> </div>
@ -368,28 +372,20 @@
</template> </template>
<script setup> <script setup>
import { isInApp, viewUserInfo } from '@/utils/appBridge.js'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { isInApp, viewUserInfo } from '@/utils/appBridge.js'
import { connectToApp } from '@/utils/appConnector.js'
import { getPngUrl } from '@/config/imagePaths.js'
import { preloadImages } from '@/utils/imagePreloader.js'
import { getThisWeekRewardsAndGifts, getRankingListAndMyRanking } from '@/api/activity.js'
import itemCenter from '@/components/itemCenter.vue' 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'
import { getThisWeekRewardsAndGifts, getRankingListAndMyRanking } from '@/api/activity.js'
import { connectToApp } from '@/utils/appConnector.js'
// vite // OSSURL
const imageModules = import.meta.glob( const imageUrl = (filename) => getPngUrl('Activities/Oman/NationalDay/', filename)
'@/assets/images/Activities/Oman/NationalDay/*.{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]
})
)
const isInAppEnvironment = ref(false) // APP const isInAppEnvironment = ref(false) // APP
const maskLayerShow = ref(false) // const maskLayerShow = ref(false) //
@ -409,7 +405,7 @@ const myRanking = ref({}) // 我的排名
const rewardsList = ref([]) // const rewardsList = ref([]) //
const gifts = ref([]) // const gifts = 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(() => {
@ -428,7 +424,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 []
}) })
// //
@ -522,11 +517,42 @@ const initData = () => {
getRewardsAndGifts() // getRewardsAndGifts() //
} }
//
const preloadCriticalImages = () => {
const criticalImages = [
imageUrl('bg'),
imageUrl('timeDayBg'),
imageUrl('timeHourBg'),
imageUrl('timeMinBg'),
imageUrl('timeSecBg'),
imageUrl('giftsBg'),
imageUrl('giftItemBg'),
imageUrl('rankingBt'),
imageUrl('rankingBtActive'),
imageUrl('rewardBtActive'),
imageUrl('rewardsBt'),
imageUrl('RankingMain'),
imageUrl('topOne'),
imageUrl('topTwo'),
imageUrl('topThree'),
imageUrl('RankingItem'),
imageUrl('RankingBottomBorder'),
imageUrl('myRankingBg'),
imageUrl('helpInfo'),
imageUrl('top1RewardBg'),
imageUrl('top2RewardBg'),
imageUrl('top3RewardBg'),
]
preloadImages(criticalImages, { useSmartCache: true })
}
// 使APP // 使APP
const connectToAppHandler = async () => { const connectToAppHandler = async () => {
await connectToApp(() => { await connectToApp(() => {
// //
initData() // initData() //
preloadCriticalImages() //
}) })
} }
@ -568,7 +594,7 @@ onUnmounted(() => {
.bg { .bg {
width: 100vw; width: 100vw;
min-height: 100vh; min-height: 100vh;
background-image: url(../../../../assets/images/Activities/Oman/NationalDay/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;