ylj20011123 178c3a8179 update
2025-09-15 09:27:13 +08:00

176 lines
5.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="lazy-image-wrapper" :style="wrapStyle">
<!-- 仅在可视区才真正挂载 image避免无意义的下载+解码 -->
<image v-if="visible" class="img" :src="realSrc" :mode="mode" :lazy-load="true" webp="true"
show-menu-by-longpress="false" :style="{ opacity: imageLoaded ? 1 : 0 }" @load="onImageLoad"
@error="onImageError" />
<view v-if="!imageLoaded && !showError" class="placeholder">
<view class="loading-bg"></view>
</view>
<view v-if="showError" class="error-placeholder">
<image class="fallback" src="/static/images/home/defaultIcon.png" :mode="mode" />
</view>
</view>
</template>
<script>
/** ===== 全局简单并发池(同域连接数受限时很有用) ===== */
const POOL_MAX = 6;
const queue = [];
let loadingCount = 0;
function schedule(task) {
if (loadingCount < POOL_MAX) {
loadingCount++;
task(() => {
loadingCount--;
const next = queue.shift();
if (next) schedule(next);
});
} else {
queue.push(task);
}
}
export default {
name: 'LazyImage',
props: {
src: { type: String, default: '' },
width: { type: String, default: '100%' },
height: { type: String, default: '100%' },
mode: { type: String, default: 'aspectFill' },
/** 后端/CDN若支持按需裁剪可传如 'w=360' 自动拼接到 url 上 */
query: { type: String, default: '' },
},
data() {
return {
imageLoaded: false,
showError: false,
visible: false,
retry: 0,
io: null,
release: null,
};
},
computed: {
wrapStyle() {
return `width:${this.width};height:${this.height};`;
},
optimizedSrc() {
if (!this.src) return '';
if (!this.query) return this.src;
const joiner = this.src.includes('?') ? '&' : '?';
return `${this.src}${joiner}${this.query}`;
},
// 只有在可视区且进入并发池后才真正给 <image> 一个 src
realSrc() {
return this.visible ? this.optimizedSrc : '';
},
},
mounted() {
// 视口观察:进入视区才 visible=true离开且已加载则释放长列表更稳
// #ifdef MP-WEIXIN
this.io = this.createIntersectionObserver();
this.io.relativeToViewport({ top: 200, bottom: 200 }).observe('.lazy-image-wrapper', (res) => {
const inView = res?.intersectionRatio > 0;
if (inView && !this.visible) {
this.visible = true;
// 进入视区后,放入并发池启动加载
schedule((done) => {
this.release = done;
// realSrc 已绑定,微信会开始拉取
});
} else if (!inView && this.visible && this.imageLoaded) {
// 离开视区:释放内存,下次滚回来再加载(已缓存,几乎瞬开)
this.visible = false;
this.imageLoaded = false;
this.showError = false;
this.retry = 0;
}
});
// #endif
},
beforeDestroy() {
if (this.io) this.io.disconnect();
},
methods: {
onImageLoad() {
this.imageLoaded = true;
this.showError = false;
if (this.release) this.release();
},
onImageError() {
if (this.release) this.release();
if (this.retry < 2) {
// 指数退避重试(常见于弱网/CDN偶发 499/超时)
this.retry++;
const delay = 200 * Math.pow(2, this.retry); // 200ms, 400ms
setTimeout(() => {
// 复位触发二次下载(依然通过并发池)
this.imageLoaded = false;
this.showError = false;
schedule((done) => {
this.release = done;
// realSrc 不变,微信会再次尝试(有些机型需先置空再赋值,可按需启用下面两行)
// this.visible = false;
// this.$nextTick(() => (this.visible = true));
});
}, delay);
} else {
this.showError = true;
}
},
},
};
</script>
<style lang="less" scoped>
.lazy-image-wrapper {
position: relative;
overflow: hidden;
.img,
.fallback {
display: block;
width: 100%;
height: 100%;
transition: opacity .25s ease;
will-change: opacity;
}
.placeholder,
.error-placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
z-index: 1;
}
.error-placeholder {
z-index: 2;
}
.loading-bg {
width: 100%;
height: 100%;
background: linear-gradient(90deg, rgba(0, 0, 0, .06), rgba(0, 0, 0, .12), rgba(0, 0, 0, .06));
background-size: 200% 100%;
animation: shimmer 1.2s infinite linear;
border-radius: 4rpx;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
}
</style>