feat(首页): 添加神策数据埋点

- 在 Header 组件中添加 Banner 点击埋点
- 在 ModelCard 组件中添加 模型介绍点击 埋点
- 在 Operating 组件中添加 操作指引 埋点
- 在 Scene 组件中添加 创建问卷 埋点
- 优化了埋点数据的结构和命名
This commit is contained in:
Huangze
2025-06-18 14:45:12 +08:00
parent 91a3988be7
commit 4cbf5f6a47
9 changed files with 352 additions and 273 deletions

View File

@@ -5,16 +5,8 @@
<div class="link-action-wrapper" @click="clickAction">
<router-view />
</div>
<a-modal
centered
:width="700"
:visible="show"
:title="title"
:maskClosable="true"
:closable="true"
wrapClassName="custom-modal"
:footer="null"
>
<a-modal centered :width="700" :visible="show" :title="title" :maskClosable="true" :closable="true"
wrapClassName="custom-modal" :footer="null">
<template #closeIcon>
<i class="iconfont model-close" @click="show = false">&#xe68b;</i>
</template>
@@ -56,7 +48,7 @@ export default {
}
});
console.log(route);
// console.log(route);
const setToken = () => {
if (!!route.query.token) {
localStorage.setItem('plantToken', route.query.token);
@@ -117,6 +109,7 @@ export default {
* {
font-family: inherit;
}
* {
margin: 0;
padding: 0;
@@ -139,12 +132,15 @@ div {
.vxe-table--body-wrapper {
@extend .scroller;
}
.ant-table-header,
.ant-table-hide-scrollbar {
padding-right: 6px !important;
overflow-y: hidden !important;
}
@media screen and (min-width: 600px) {
html,
body {
& *:not(svg, p, strong, em, p span, label) {
@@ -161,6 +157,7 @@ div {
// }
/*<=600的设备*/
@media (max-width: 600px) {
html,
body {
& *:not(p, strong, em, p span, label) {
@@ -168,6 +165,7 @@ div {
}
}
}
strong {
// font-family: "Noto Sans CJK SC Medium", "Source Han Sans CN Medium" !important;
font-weight: bolder !important;

View File

@@ -78,34 +78,5 @@ installAntDesign(app);
app.config.globalProperties.emitter = emitter;
// 神策数据插件
app.use(sensorsData(), {
// 单页面配置,默认关闭。开启后自动监听 URL 有变化就会触发 $pageview 事件
is_track_single_page: function () {
return false;
},
scrollmap: {
// Web 视区停留
collect_url: function () {
function isCollectUrl(urls) {
return urls.some((url) => location.href.includes(url));
}
// 需要采集的页面
const urls = ['/ad/', '/share/'];
console.log(`collect_url`, isCollectUrl(urls));
// return isCollectUrl(urls);
return true;
}
},
heatmap: {
//是否开启触达图default 表示开启,自动采集 $WebStay 事件,可以设置 'not_collect' 表示关闭。
//需要 Web JS SDK 版本号大于 1.9.1
scroll_notice_map: 'default',
scroll_delay_time: 4000,
//单位秒,预置属性停留时长 event_duration 的最大值。默认5个小时也就是300分钟18000秒。
scroll_event_duration: 18000,
get_vtrack_config: true
}
});
app.use(sensorsData(), {});
app.use(store).use(router).mount('#app');

View File

@@ -12,7 +12,11 @@ import { jsonpUrl } from '../config.js';
import { useStore } from 'vuex';
import Creative from './route.creative'; // 创作中心路由
import MarketList from '@/views/TempMarket/components/TempMarketLayout';
import { sensors } from '@/utils/plugins/sa';
const sa = {
register: false,
instance: window.sa || null
};
const store = useStore();
@@ -617,33 +621,23 @@ const router = createRouter({
routes: constantRoutes
});
let pageStayTime = 0;
router.beforeEach(async (to, from, next) => {
// 离开页面的时候结束记录时间
const duration = Date.now() - pageStayTime;
pageStayTime = 0;
const collectUrl = ['/ad/', '/share/'];
console.log(
`is collect page`,
collectUrl.some((url) => from.path.startsWith(url))
);
// 判断是否离开需要采集的页面
if (true) {
// if (collectUrl.some((url) => from.path.startsWith(url))) {
// alert(`duration: ${duration}`);
sensors.track('pageStayTime', {
duration
});
}
if (to.meta?.title) document.title = to.meta.title;
// token
if (!to.meta.shared && !router.options.history.state.back) {
await getToken();
}
// 神策数据埋点
if (!sa.register && localStorage.getItem('plantUserInfo')) {
sa.instance = window.sa;
// 检测是否使用神策的登陆
const userInfo = JSON.parse(localStorage.getItem('plantUserInfo'));
const loginID = `id-${userInfo.id}.login_team_id-${userInfo.login_team_id}`;
sa.instance.login(globalThis.btoa(loginID));
sa.register = true;
}
if (!to.meta.noRedirectLogin) {
if (window.self === window.top) {
// window.parent.location.href = 'https://yip-uat.dctest.digitalyili.com/login';
@@ -662,10 +656,10 @@ router.beforeEach(async (to, from, next) => {
next();
});
router.afterEach((to, from) => {
// 页面导航结束的时候开始记录时间
pageStayTime = Date.now();
});
// router.afterEach((to, from) => {
// // 页面导航结束的时候开始记录时间
// pageStayTime = Date.now();
// });
const getToken = async () => {
try {

View File

@@ -21,13 +21,17 @@ export function sensorsData() {
// 注册页面公共属性
sensors.registerPage({
platform: 'h5',
platform: 'pc',
// 产品名称
production_name: 'ylst',
current_url: location.href,
referrer: document.referrer
});
// 提供全局注入的 sensors 实例
app.provide('sensors', sensors);
// 注册到window中
globalThis.sa = sensors;
// 注册 saTrack 自定义指令
registerDirective(app);
@@ -40,17 +44,26 @@ export function sensorsData() {
* @param {App} app - Vue 应用实例
*/
function registerDirective(app) {
app.directive('saTrack', {
mounted(el, binding) {
el.addEventListener('click', () => {
function bindTrackListener(binding) {
return () => {
const { arg: eventName, value: properties } = binding;
if (eventName) {
sensors.track(eventName, properties);
console.warn(properties);
} else {
console.warn('[sensorsData] 事件名未提供');
}
});
};
}
app.directive('saTrack', {
mounted(el, binding) {
el.addEventListener('click', bindTrackListener(binding));
},
unmounted(el, binding) {
// 清除绑定的事件
el.removeEventListener('click', bindTrackListener(binding));
}
});
}

View File

@@ -27,12 +27,9 @@
<!-- <video v-if="bannerInfo.type === 2" :src="bannerInfo.file_address" style="width: 100%;"></video>-->
<div v-if="bannerInfo.is_display_button" class="jump">
<a-button
type="primary"
class="custom-button mr-12"
@click="toJump">
<span>{{ bannerInfo.button_name }}</span>
<div class="jump">
<a-button type="primary" class="custom-button mr-12" @click="toJump">
<span>{{ bannerInfo.button_name }}12312312</span>
</a-button>
</div>
</div>
@@ -44,9 +41,8 @@
<script setup>
import Layout from '@/views/ProjectManage/components/Layout.vue';
import { onMounted, ref } from 'vue';
import { useRouter,useRoute } from 'vue-router';
import { useRoute } from 'vue-router';
import { queryBanner } from '@/api/home';
const router = useRouter();
const route = useRoute();
const bannerInfo = ref({});
@@ -75,9 +71,11 @@ const back = () => {
<style scoped lang="scss">
.detail {
padding: 10px 20%;
.time {
color: #7F7F7F;
}
.jump {
text-align: center;
margin-top: 20px;

View File

@@ -55,7 +55,7 @@
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { onBeforeUnmount, onMounted, ref, inject } from 'vue';
import Swiper, { Autoplay } from 'swiper';
import 'swiper/swiper-bundle.css';
import { useRouter } from 'vue-router';
@@ -85,6 +85,7 @@ const surveyData = ref({
})
const bannerList = ref([])
const sensors = inject('sensors')
// 获取数据
const getBannerData = async () => {
const res = await getBannerList()
@@ -168,6 +169,9 @@ const goToSlide = (index) => {
}
// 方法
const toBannerDetail = (item) => {
// 神策埋点
saTrack(item)
router.push({
path: '/home/bannerDetail',
query: {
@@ -189,6 +193,21 @@ onBeforeUnmount(() => {
bannerSwiper = null
}
})
function saTrack(record) {
const config = {
eventName: "ClickBanner",
properties: {
page: "首页",
module: "Banner",
position: "查看详情",
title: record.code,
clickTime: new Date().toLocaleString().toString()
}
}
sensors.track(config.eventName, config.properties);
}
</script>
<style scoped lang="scss">

View File

@@ -1,16 +1,12 @@
<template>
<div class="model-item"
@mouseover="hoverIndex = index"
@mouseleave="hoverIndex = -1"
<div class="model-item" @mouseover="hoverIndex = index" @mouseleave="hoverIndex = -1"
:style="{ backgroundImage: `url(${model.backgroundImage})` }">
<div class="flex align-items-center">
<img :src="model.image" alt="" style="height: 23px; margin-right: 8px" />
<p class="model-item-title">{{ model.title }}</p>
</div>
<p class="model-item-desc">{{ model.description }}</p>
<div
class="view-link"
@click="$emit('open')">
<div class="view-link" @click="handleModelClick">
<span>{{ getText(model, index) }}</span>
<img v-if="hoverIndex === index && index !== 3" src="@/assets/img/home/tob.png" alt="">
<img v-else src="@/assets/img/home/tow.png" alt="">
@@ -19,9 +15,9 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, inject } from 'vue'
const hoverIndex = ref(-1);
defineProps({
const { model,index } = defineProps({
model: {
type: Object,
required: true
@@ -30,12 +26,35 @@ defineProps({
type: Number,
}
})
const emit = defineEmits(['open'])
const sensors = inject('sensors')
const getText = (model, index) => {
if (hoverIndex.value === index) {
return model.sort === 103 ? "敬请期待" : "去查看";
}
return "去查看";
}
function handleModelClick() {
emit('open')
console.log(model);
saTrack(model)
function saTrack(record) {
const config = {
eventName: "IntroduceModel",
properties: {
page: "首页",
module: "模型介绍",
position: record.title,
buttonName: "去查看",
clickTime: new Date().toLocaleString().toString()
}
}
sensors.track(config.eventName, config.properties);
}
}
</script>
<style scoped lang="scss">
@@ -99,6 +118,7 @@ p {
// color: #3976D7;
//}
}
.model-item:hover {
.view-link {
background: #fff;
@@ -113,6 +133,7 @@ p {
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
& span {
color: #3976D7;
}

View File

@@ -7,11 +7,7 @@
<div style="height: 80%">
<div class="swiper-container" ref="swiperContainer">
<div class="swiper-wrapper">
<div
v-for="(item, index) in operatingList"
:key="index"
class="swiper-slide"
>
<div v-for="(item, index) in operatingList" :key="index" class="swiper-slide">
<div class="flex" style="justify-content: center">
<div style="margin-right: 9%">
<div class="flex-start operating-item">
@@ -40,19 +36,11 @@
</ul>
<a-button type="primary" @click="toSurveyInfo(item, index)">
立即体验
<img src="@/assets/img/home/tow.png" alt=""
style="margin-left: 5px;margin-bottom: 2px">
<img src="@/assets/img/home/tow.png" alt="" style="margin-left: 5px;margin-bottom: 2px">
</a-button>
</div>
<div>
<video
ref="videoPlayer"
class="video-player"
width="820"
height="420"
autoplay
muted
playsinline
<video ref="videoPlayer" class="video-player" width="820" height="420" autoplay muted playsinline
style="width: 100%;">
<source :src="getVideo(index)" type="video/mp4">
@@ -79,7 +67,7 @@
</div>
</template>
<script setup>
import {ref, onMounted, onBeforeUnmount} from 'vue'
import { ref, onMounted, onBeforeUnmount, inject } from 'vue'
import Swiper from 'swiper';
import 'swiper/swiper-bundle.css';
import { useRouter } from 'vue-router';
@@ -136,6 +124,8 @@ const operatingList = [
key: 'particulars'
}]
const sensors = inject('sensors')
const goPre = () => {
pauseCurrentVideo();
if (mySwiper) {
@@ -166,6 +156,9 @@ const pauseCurrentVideo = () => {
};
const toSurveyInfo = async (item, index) => {
// 调用神策埋点
saTrack(item, index)
let res = await getQueryUserSurvey()
const { sn } = res.data
const urlJson = {
@@ -194,6 +187,22 @@ const toSurveyInfo = async (item,index) => {
message.warning('您还没有问卷,快去创建一个吧~');
}
}
function saTrack(record, index) {
const operates = ["创建", "设计","投放","分析"]
// 操作引导埋点
const config = {
eventName: "OperatingGuide",
properties: {
page: "首页",
module: "操作指引",
position: operates[index]+"问卷",
buttonName: "立即体验",
clickTime: new Date().toLocaleString().toString()
}
}
sensors.track(config.eventName, config.properties);
}
}
const getVideo = (index) => {
@@ -309,51 +318,64 @@ p{
margin: 0;
padding: 0;
}
.fs-36 {
font-size: 36px;
}
.fs-34 {
font-size: 34px;
}
li {
list-style-type: none;
}
.operating-container {
margin-top: 20px;
min-height: 600px;
height: 600px;
.top {
margin-bottom: 40px;
}
&>div:first-child {
text-align: center;
}
.title {
margin-bottom: 5px;
}
.desc {
color: #7F7F81;
}
.operating-item {
.progress {
display: flex;
align-items: baseline;
letter-spacing: 1px;
& :nth-child(1) {
color: #70B937;
font-size: 45px;
font-weight: 400;
}
& :nth-child(2) {
height: 33px;
}
& :nth-child(3) {
color: #000;
font-size: 24px;
font-weight: 500;
}
}
.info {
font-size: 24px;
font-weight: bold;
@@ -361,6 +383,7 @@ li{
white-space: nowrap;
}
}
.desc-item {
font-weight: bold;
font-size: 12px;
@@ -368,15 +391,18 @@ li{
margin-left: 5%;
margin-bottom: 40px;
width: max-content;
li {
margin: 20px 0;
justify-content: flex-start;
.block {
width: 8px;
height: 8px;
background-color: #000;
margin-right: 10px;
}
.tag {
font-weight: 500;
font-size: 13px;
@@ -390,16 +416,19 @@ li{
}
}
}
:deep(.el-carousel__arrow--right) {
border: 1px solid #C1C1C2;
background: transparent;
color: #C1C1C2;
}
:deep(.el-carousel__arrow--left) {
border: 1px solid #C1C1C2;
background: transparent;
color: #C1C1C2;
}
:deep(.el-carousel__container) {
height: 100%;
}
@@ -453,10 +482,10 @@ video{
border-radius: 8px;
object-fit: fill;
}
//.video-player {
// //width: 100%;
// //height: auto; /* 自动计算高度 */
// //max-height: 100%; /* 防止视频超出容器 */
// border-radius: 8px;
//}
</style>
//}</style>

View File

@@ -7,28 +7,22 @@
</div>
<div class="flex mt20 sceneList">
<div v-for="(scene,index) in sceneList"
:key="index"
class="sceneItem"
:class="{'fix-scene':[0,1,2].includes(index)}"
@click="createScene(scene,index)">
<div v-for="(scene, index) in sceneList" :key="index" class="sceneItem"
:class="{ 'fix-scene': [0, 1, 2].includes(index) }" @click="createScene(scene, index)">
<div class="flex-start">
<img :src="scene.image" alt="" style="width: 28px;">
<p class="sceneItem-title" :class="{ 'sceneItem-title-color': index === 0 }">{{ scene.title }}</p>
</div>
<p class="sceneItem-desc" :style="{ 'minHeight': index === 0 ? 'auto' : '30px' }">{{ scene.description }}</p>
<div style="text-align: left">
<a-button
v-if="index === 0 "
class="createBtn"
type="primary"
>
<a-button v-if="index === 0" class="createBtn" type="primary">
立即创建
</a-button>
<div v-else>
<a-button class="custom-button toCreate"
:class="{'createG':[0,1,2].includes(index),
'create-normal':[11,13,15,16,17].includes(scene.code)}" type="text">
<a-button class="custom-button toCreate" :class="{
'createG': [0, 1, 2].includes(index),
'create-normal': [11, 13, 15, 16, 17].includes(scene.code)
}" type="text">
<span>去创建</span>
<!-- 'normal':[11,13,15,16,17].includes(scene.code)-->
<img src="@/assets/img/home/tob.png" alt=""
@@ -39,9 +33,7 @@
</div>
</div>
</div>
<create ref="createRef"
style="width: 1px;height: 0"
@update:ai-assistant-visible="getValue">
<create ref="createRef" style="width: 1px;height: 0" @update:ai-assistant-visible="getValue">
</create>
<!-- 概念测试 -->
@@ -52,7 +44,7 @@
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { onMounted, ref, inject } from 'vue';
import create from '@/views/ProjectManage/create/Index.vue'
import { useRouter } from 'vue-router';
import { getSceneListHome } from '@/api/home';
@@ -93,6 +85,8 @@ const fixedSceneList = [
const sceneList = ref([])
// ref声明需与组件ref属性值一致
const createRef = ref()
const sensors = inject('sensors')
onMounted(() => {
getSceneList()
})
@@ -113,6 +107,9 @@ const getSceneList=()=>{
* @param index
*/
const createScene = (record, index) => {
// 调用神策埋点
saTrack(record, index)
if ([0, 1, 2].includes(index)) {
if (createRef.value) {
// 检查 record 是否有 value 属性,如果没有则直接使用 record
@@ -170,6 +167,23 @@ const createScene = (record,index)=>{
});
}
}
function saTrack(record,index) {
const config = {
eventName: "SurveyCreate",
properties: {
page: "首页",
module: "创建问卷",
// 排除智能创建、快捷导入、空白创建,其他都是模板创建
position: [0,1,2].includes(index) ? record.title: `模板创建-${record.title}`,
// 第一个默认是智能创建
buttonName: index === 0 ? "立即创建" : "去创建",
clickTime: new Date().toLocaleString().toString()
}
}
sensors.track(config.eventName, config.properties);
}
/**
* 轮播图设置显隐
* @param val
@@ -198,17 +212,21 @@ p{
margin: 0;
padding: 0;
}
.scene-container {
text-align: center;
}
.scene-title {
margin: 32px 0 26px 4vw;
}
.more {
color: #70B937;
cursor: pointer;
}
.sceneList {
overflow-y: hidden;
//width: calc(100vw - 40px);
@@ -217,6 +235,7 @@ p{
padding-bottom: 15px;
cursor: pointer;
}
.sceneItem {
padding: 12px 12px 10px 12px;
border-radius: 7px;
@@ -228,17 +247,20 @@ p{
background: #fff;
border: 1px solid #EBEBEB;
}
.sceneItem-title {
font-weight: bold;
font-size: 12px;
color: #000000;
margin-left: 10px;
}
.sceneItem-title-color {
background: linear-gradient(135deg, #C352E7, #3962FF);
background-clip: text;
color: transparent;
}
.sceneItem-desc {
font-size: 10px;
color: #7F7F7F;
@@ -247,15 +269,18 @@ p{
text-wrap: wrap;
min-height: 30px
}
.fix-scene {
border: 1px solid rgba(98, 93, 248, 0.5);
background-image: url("../../../assets/img/home/sceneBg.png");
background-size: 100% 100%;
}
.normal {
color: #ccc;
border: 1px solid red !important;
}
.createBtn {
background-image: url("../../../assets/img/home/createbtn.png");
background-size: 100% 100%;
@@ -265,38 +290,49 @@ p{
text-align: left;
margin-top: 10px;
}
.toCreate {
padding-left: 0;
img {
margin-left: 5px;
margin-bottom: 2px;
}
}
.createG {
color: #5562FB !important;
}
.create-normal {
color: #3171f3 !important;
}
:deep(.ant-btn > span) {
font-size: 11px;
}
/* 强制显示滚动条WebKit 内核浏览器,如 Chrome/Safari */
::-webkit-scrollbar {
width: 8px; /* 纵向滚动条宽度 */
height: 6px; /* 向滚动条度 */
width: 8px;
/* 向滚动条度 */
height: 6px;
/* 横向滚动条高度 */
border: 1px solid red;
margin-top: 20px;
}
::-webkit-scrollbar-thumb {
background: #DFE1E7; /* 滚动条滑块颜色 */
border-radius: 4px; /* 滑块圆角 */
background: #DFE1E7;
/* 滚动条滑块颜色 */
border-radius: 4px;
/* 滑块圆角 */
}
/* 滚动条轨道 */
::-webkit-scrollbar-track {
background: #fff; /* 轨道颜色 */
background: #fff;
/* 轨道颜色 */
//border: 1px solid #D8D8D8;
}
</style>