151 lines
3.3 KiB
Vue
151 lines
3.3 KiB
Vue
<template>
|
||
<view class="test-page">
|
||
<view class="desc-card">
|
||
<text class="title">IntersectionObserver 仅首次触发演示</text>
|
||
<text class="subtitle">向下滚动,观察各模块首次进入视口时的淡入效果</text>
|
||
</view>
|
||
|
||
<view class="section-title">场景:仅首次触发 — 视口内淡入动画</view>
|
||
<view
|
||
class="fade-box"
|
||
v-for="(item, index) in fadeList"
|
||
:key="index"
|
||
:id="`fade-${index}`"
|
||
:class="{ 'fade-in': item.visible }"
|
||
>
|
||
<text class="box-text">模块 {{ index + 1 }}</text>
|
||
<text class="box-status">{{ item.triggered ? '✅ 已触发(不再重复)' : '⏳ 等待首次进入视口...' }}</text>
|
||
</view>
|
||
|
||
<view class="footer-space"></view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
fadeList: Array.from({ length: 10 }, () => ({
|
||
visible: false,
|
||
triggered: false
|
||
})),
|
||
// ⭐ 关键:用一个数组存放每个元素独立的 observer,避免互相覆盖
|
||
observers: []
|
||
}
|
||
},
|
||
onReady() {
|
||
this.initFadeObserver();
|
||
},
|
||
onUnload() {
|
||
// 页面卸载时,断开所有 observer
|
||
this.observers.forEach(obs => obs.disconnect());
|
||
},
|
||
methods: {
|
||
initFadeObserver() {
|
||
this.fadeList.forEach((_, index) => {
|
||
// ⭐ 核心修复:每个元素创建独立的 IntersectionObserver 实例
|
||
// 同一个实例多次调用 observe() 会被覆盖,只有最后一个生效
|
||
const observer = uni.createIntersectionObserver(this, {
|
||
thresholds: [0]
|
||
});
|
||
|
||
observer.relativeToViewport();
|
||
|
||
observer.observe(`#fade-${index}`, (result) => {
|
||
const item = this.fadeList[index];
|
||
|
||
// 进入视口 且 之前没触发过
|
||
if (result.intersectionRatio > 0 && !item.triggered) {
|
||
console.log(`模块 ${index + 1} 首次进入视口`);
|
||
|
||
this.$set(this.fadeList, index, {
|
||
visible: true,
|
||
triggered: true
|
||
});
|
||
|
||
// 触发后断开这个 observer,彻底释放
|
||
observer.disconnect();
|
||
}
|
||
});
|
||
|
||
// 保存到数组,方便 onUnload 统一清理
|
||
this.observers.push(observer);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.test-page {
|
||
background-color: #f5f6f8;
|
||
padding: 30rpx;
|
||
}
|
||
|
||
.desc-card {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-radius: 24rpx;
|
||
padding: 40rpx;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.title {
|
||
color: #ffffff;
|
||
font-size: 40rpx;
|
||
font-weight: bold;
|
||
display: block;
|
||
}
|
||
|
||
.subtitle {
|
||
color: rgba(255, 255, 255, 0.85);
|
||
font-size: 26rpx;
|
||
margin-top: 16rpx;
|
||
display: block;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin: 40rpx 0 20rpx;
|
||
padding-left: 20rpx;
|
||
border-left: 8rpx solid #ff416c;
|
||
}
|
||
|
||
.fade-box {
|
||
height: 240rpx;
|
||
background: #ffffff;
|
||
border-radius: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||
opacity: 0;
|
||
transform: translateY(60rpx);
|
||
transition: all 0.6s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||
}
|
||
|
||
.fade-box.fade-in {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.box-text {
|
||
font-size: 36rpx;
|
||
color: #333;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.box-status {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.footer-space {
|
||
height: 100rpx;
|
||
}
|
||
</style>
|