uniapp 观察列表每个元素的曝光时间

App.vue

复制代码
onShow: function() {
		
			uni.$emit('pageShow');
		},

onHide: function(){
		    // 触发全局自定义事件
		    uni.$emit('pageHide');
		  },

exposureTracker.vue 组件

复制代码
<template>
	<view class="exposure-tracker">
		<!-- 插槽用于传入列表内容 -->
		<slot></slot>
	</view>
</template>

<script>
	export default {
		name: 'ExposureTracker',
		props: {
			// 观察的类名选择器
			itemSelector: {
				type: String,
				default: '.exposure-item'
			},
			// 观察的类名选择器
			containerSelector: {
				type: String,
				default: '.container'
			},
			// 曝光阈值
			threshold: {
				type: Number,
				default: 0.5
			},
			// 上报策略配置
			reportStrategy: {
				type: Object,
				default: () => ({
					minExposureTime: 1000, // 最小曝光时间(小于此时间不计入)
					periodicReport: 5000, // 周期性上报间隔
					maxExposureTime: 30000 // 最大单次曝光时间(超过按此值计算)
				})
			},
			//上报配置
			report: {
				type: Object,
				default: () => ({
					pagePath: '', //事件发生的页面路径
					event: '', //事件
					eventParameter: [] //发生事件的参数
				})
			}
		},
		data() {
			return {
				observer: null,
				exposureData: new Map(), // 存储 { startTime, lastReportTime, timer, totalDuration }
				isPageActive: true
			}
		},
		mounted() {
			this.initObserver();
			this.bindPageEvents();
		},
		beforeDestroy() {
			this.cleanup();
		},
		methods: {
			initObserver() {
				this.$nextTick(() => {
					try {
						this.observer = uni.createIntersectionObserver(this, {
							thresholds: [this.threshold], //设置为0,或者设置多个观察点 0, 0.1, 0.5, 0.9, 1
							observeAll: true
						});

						//this.observer.relativeToViewport().observe(this.itemSelector, (res) => {
						this.observer.relativeTo(this.containerSelector).observe(this.itemSelector, (res) => {
							console.log(res);

							const id = res.dataset.id;
							if (!id) return;

							const isIntersecting = res.intersectionRatio > 0;

							if (isIntersecting) {
								this.handleElementEnter(id);
							} else {
								this.handleElementLeave(id);
							}
						});
					} catch (error) {
						console.error('曝光观察器初始化失败:', error);
					}
				});
			},

			handleElementEnter(id) {
				if (!this.exposureData.has(id)) {
					const startTime = Date.now();
					// 创建周期性上报计时器
					const timer = setInterval(() => {
						if (this.isPageActive) {
							this.reportPeriodicExposure(id);
						}
					}, this.reportStrategy.periodicReport);

					this.exposureData.set(id, {
						startTime,
						lastReportTime: startTime,
						timer,
						totalDuration: 0
					});

					console.log(`元素 ${id} 开始曝光`);
					this.$emit('exposure-start', {
						id,
						startTime
					});
				}
			},

			handleElementLeave(id) {
				console.log("不重叠");
				const data = this.exposureData.get(id);
				if (data) {
					this.finalizeExposure(id, data);
				}
			},

			reportPeriodicExposure(id) {
				const data = this.exposureData.get(id);
				if (data && this.isPageActive) {
					const currentTime = Date.now();
					const periodDuration = currentTime - data.lastReportTime;
					data.totalDuration += periodDuration;
					data.lastReportTime = currentTime;

					console.log(`元素 ${id} 周期性上报,当前累计: ${data.totalDuration}ms`);
					this.$emit('exposure-periodic', {
						id,
						currentDuration: data.totalDuration,
						startTime: data.startTime
					});
				}
			},

			finalizeExposure(id, data) {
				clearInterval(data.timer);

				const currentTime = Date.now();
				const currentDuration = currentTime - data.lastReportTime;
				const totalDuration = data.totalDuration + currentDuration;

				// 应用上报策略
				if (totalDuration < this.reportStrategy.minExposureTime) {
					console.log(`元素 ${id} 曝光时间过短,忽略上报`);
					this.$emit('exposure-ignored', {
						id,
						duration: totalDuration,
						reason: 'below_min_threshold'
					});
				} else {
					const finalDuration = Math.min(totalDuration, this.reportStrategy.maxExposureTime);

					console.log(`元素 ${id} 最终曝光时长: ${finalDuration}ms`);
					this.$emit('exposure-end', {
						id,
						startTime: data.startTime,
						endTime: currentTime,
						duration: finalDuration,
						originalDuration: totalDuration
					});
				}

				this.exposureData.delete(id);


				//console.log(99999);
				//console.log(this.report);

			},

			// 绑定页面事件
			bindPageEvents() {
				// 使用全局事件监听页面状态
				uni.$on('pageHide', () => {
					this.isPageActive = false;
					this.pauseAllTimers();
				});

				uni.$on('pageShow', () => {
					this.isPageActive = true;
					this.resumeAllTimers();
				});
			},

			// 页面隐藏时暂停所有计时器
			pauseAllTimers() {
				console.log("页面隐藏,暂停计时");
				const currentTime = Date.now();
				this.exposureData.forEach((data) => {
					clearInterval(data.timer);
					// 更新总时长(从最后一次上报到暂停的时间)
					data.totalDuration += (currentTime - data.lastReportTime);
				});
			},

			// 页面显示时恢复所有计时器
			resumeAllTimers() {
				console.log("页面显示,恢复计时");
				const currentTime = Date.now();
				this.exposureData.forEach((data, id) => {
					data.lastReportTime = currentTime;
					// 重新创建计时器
					data.timer = setInterval(() => {
						this.reportPeriodicExposure(id);
					}, this.reportStrategy.periodicReport);
				});
			},

			// 页面卸载时的最终上报
			finalReportAll() {
				this.exposureData.forEach((data, id) => {
					this.finalizeExposure(id, data);
				});
			},

			// 清理资源
			cleanup() {
				this.finalReportAll();
				if (this.observer) {
					this.observer.disconnect();
				}
				// 移除事件监听
				uni.$off('pageHide');
				uni.$off('pageShow');
			},

			// 外部调用:手动触发曝光结束
			manualTriggerExposureEnd(id) {
				const data = this.exposureData.get(id);
				if (data) {
					this.finalizeExposure(id, data);
				}
			},

			// 外部调用:获取当前正在曝光的元素
			getCurrentExposingItems() {
				return Array.from(this.exposureData.keys());
			}
		}
	}
</script>

<style scoped>
	.exposure-tracker {
		width: 100%;
	}
</style>

在 exposureTrackerTest.vue 中测试

复制代码
<template>
	<view>
		<exposure-tracker item-selector=".list-item" container-selector="container" :threshold="0" :report-strategy="{
						minExposureTime: 1000,
						periodicReport: 5000,
						maxExposureTime: 30000
					}" :report="{pagePath:'pages/daygoodness/index',event:'stay',eventParameter:[{
					'bizName': '停留xxx2',
					'bizTable': 'table_name2',
					'bizId': '112222'
				}]}" @exposure-start="handleExposureStart" @exposure-end="handleExposureEnd"
			@exposure-periodic="handleExposurePeriodic" @exposure-ignored="handleExposureIgnored">
			<scroll-view scroll-y style="height: 600px;background-color: aqua" class="container">
				<view v-for="item in list" :key="item.id" class="list-item" :data-id="item.id" style="height: 200px;">
					{{ item.text }}
				</view>
			</scroll-view>
		</exposure-tracker>
	</view>
</template>

<script>
	import ExposureTracker from '@/components/exposureTracker/exposureTracker.vue';
	import {
		trackingpointConfig
	} from '@/utils/trackingpoint';

	export default {
		components: {
			ExposureTracker
		},
		data() {
			return {
				list: [

					{
						id: 1,
						text: "11111"
					},
					{
						id: 2,
						text: "22222"
					},
					{
						id: 3,
						text: "33333"
					},
					{
						id: 4,
						text: "44444"
					},
					{
						id: 5,
						text: "55555"
					},
					{
						id: 6,
						text: "66666"
					},
					{
						id: 7,
						text: "77777"
					},
					{
						id: 8,
						text: "88888"
					},
					{
						id: 9,
						text: "99999"
					},
					{
						id: 10,
						text: "10101010"
					},
					{
						id: 11,
						text: "11111111111111"
					},
					{
						id: 12,
						text: "12121212"
					},
					{
						id: 13,
						text: "131313131313"
					},
					{
						id: 14,
						text: "141414141414"
					},
					{
						id: 15,
						text: "151515151515"
					},
					{
						id: 16,
						text: "1616161616"
					},
					{
						id: 17,
						text: "171717"
					},
					{
						id: 18,
						text: "181818"
					},
					{
						id: 19,
						text: "19191919"
					},
					{
						id: 20,
						text: "20202020"
					},
					{
						id: 21,
						text: "21212121"
					},
					{
						id: 22,
						text: "22222222"
					},
					{
						id: 23,
						text: "23232323"
					},
					{
						id: 24,
						text: "24242424"
					},
					{
						id: 25,
						text: "25252525"
					},
					{
						id: 26,
						text: "2626262626"
					},
					{
						id: 27,
						text: "2727272727"
					},
					{
						id: 28,
						text: "2828282828"
					},
					{
						id: 29,
						text: "29292929"
					},
					{
						id: 30,
						text: "3030303030"
					},
					{
						id: 31,
						text: "3131313131"
					},
					{
						id: 32,
						text: "3232323232"
					},
					{
						id: 33,
						text: "3333333333"
					},
					{
						id: 34,
						text: "343434343434"
					},
					{
						id: 35,
						text: "353535353535"
					},
				]
			}
		},
		methods: {
			handleExposureStart(event) {
				//console.log('开始曝光:', event.id);
				// 记录开始曝光
			},

			handleExposureEnd(event) {
				//console.log(`曝光结束: ${event.id}, 时长: ${event.duration}ms`);
				// 上报到服务器
				this.reportToServer({
					elementId: event.id,
					exposureDuration: event.duration,
					startTime: event.startTime,
					endTime: event.endTime
				});
			},

			handleExposurePeriodic(event) {
				//console.log(`周期性上报: ${event.id}, 当前时长: ${event.currentDuration}ms`);
				// 中间状态处理
			},

			handleExposureIgnored(event) {
				//console.log(`曝光被忽略: ${event.id}, 原因: ${event.reason}`);
			},

			reportToServer(data) {
				// 你的上报逻辑
				/* uni.request({
					url: '你的上报接口',
					method: 'POST',
					data: data
				}); */
			}
		}
	}
</script>

<style>

</style>
相关推荐
WYiQIU25 分钟前
面了一次字节前端岗,我才知道何为“造火箭”的极致!
前端·javascript·vue.js·react.js·面试
小夏同学呀28 分钟前
在 Vue 2 中实现 “点击下载条码 → 打开新窗口预览 → 自动唤起浏览器打印” 的功能
前端·javascript·vue.js
芳草萋萋鹦鹉洲哦28 分钟前
【vue】导航栏变动后刷新router的几种方法
前端·javascript·vue.js
zero13_小葵司33 分钟前
JavaScript性能优化系列(八)弱网环境体验优化 - 8.3 数据预加载与缓存:提前缓存关键数据
javascript·缓存·性能优化
1***y17838 分钟前
Vue项目性能优化案例
前端·vue.js·性能优化
Irene199141 分钟前
FileList 对象总结(附:不支持迭代的类数组对象表)
javascript·类数组对象·filelist·不支持迭代
谢尔登1 小时前
【CSS】样式隔离
前端·css
百***58842 小时前
Redis 通用命令
前端·redis·bootstrap
Liu.7742 小时前
vue3 路由缓存导致onMounted无效
前端·javascript·vue.js