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>
