Commit 26b1b7e4 by yuzhenWang

合并操作日志代码,解决冲突

parents 47f57b61 829addd4
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
import dataHandling from "@/util/dataHandling"; import dataHandling from "@/util/dataHandling";
import { useH5IdleExit } from '@/util/useH5IdleExit' import { useH5IdleExit } from '@/util/useH5IdleExit'
import * as sso from '@/util/sso' import * as sso from '@/util/sso'
import tracker from '@/util/tracker/tracker.js' import { useTracker, syncTrackerFromQuery } from '@/util/tracker/index.js'
const idleExit = useH5IdleExit() const idleExit = useH5IdleExit()
let isAllInOneEnv = false let isAllInOneEnv = false
...@@ -19,18 +19,15 @@ ...@@ -19,18 +19,15 @@
const m = q.match(/[?&]bagType=([^&]+)/) const m = q.match(/[?&]bagType=([^&]+)/)
return m ? decodeURIComponent(m[1]) === 'allinone' : false return m ? decodeURIComponent(m[1]) === 'allinone' : false
} }
export default { export default {
data() { data() {
return { return {
invitationHandled: false, invitationHandled: false ,// 在data中明确定义,
dataToken: '', dataToken:'',
loginType: '', loginType:''
// ✅ 新增 debug 属性 }
debug: process.env.NODE_ENV === 'development' },
}
},
onLaunch: function(options) { onLaunch: function(options) {
console.log('App Launch'); console.log('App Launch');
//----嵌入一体机开启超时自动退出----- //----嵌入一体机开启超时自动退出-----
...@@ -39,8 +36,11 @@ ...@@ -39,8 +36,11 @@
window.addEventListener('pagehide', () => idleExit.stop()) window.addEventListener('pagehide', () => idleExit.stop())
window.addEventListener('beforeunload', () => idleExit.stop()) window.addEventListener('beforeunload', () => idleExit.stop())
//----嵌入一体机开启超时自动退出----- //----嵌入一体机开启超时自动退出-----
if (uni.getStorageSync('loginType')) { //操作日志上传:宿主 webview 外链带过来的 trackerSessionId / trackerVisitorCode 优先
syncTrackerFromQuery((options && options.query) || {})
if(uni.getStorageSync('loginType')){
this.loginType = uni.getStorageSync('loginType') this.loginType = uni.getStorageSync('loginType')
} }
if (uni.getStorageSync('dataToken')) { if (uni.getStorageSync('dataToken')) {
...@@ -108,12 +108,7 @@ ...@@ -108,12 +108,7 @@
console.log('App Show', options); console.log('App Show', options);
// ============================================================
// ✅ 修复:延迟执行,等待页面渲染完成 + tracker 初始化完成
// ============================================================
setTimeout(() => {
this._handlePageShow()
}, 300)
// 每次进应用显示时检查用户状态 // 每次进应用显示时检查用户状态
this.checkUserStatus(); this.checkUserStatus();
...@@ -129,9 +124,8 @@ ...@@ -129,9 +124,8 @@
onHide: function() { onHide: function() {
// 切后台/息屏/H5 被隐藏:停掉,避免后台跑定时器 // 切后台/息屏/H5 被隐藏:停掉,避免后台跑定时器
idleExit.stop() idleExit.stop()
// ✅ 应用切到后台时通知 tracker
tracker.onAppHide && tracker.onAppHide();
console.log('App Hide') console.log('App Hide')
useTracker()?.flush(true) // 切后台立即上报
}, },
onUnload() { onUnload() {
...@@ -139,54 +133,7 @@ ...@@ -139,54 +133,7 @@
}, },
methods: { methods: {
_handlePageShow() {
try {
const pages = getCurrentPages();
console.log('🔍 [App] pages:', pages) // ✅ 看 pages 是否有值
if (!pages || pages.length === 0) return;
const currentPage = pages[pages.length - 1];
const route = currentPage.route || '';
const options = currentPage.$page?.options || currentPage.options || {};
// 获取页面标题
let title = currentPage.$page?.title ||
currentPage._pageTitle ||
(currentPage.data && currentPage.data.pageTitle) ||
route.split('/').pop() ||
'未知页面';
// 构建完整路径
let fullPath = '/' + route;
const queryString = Object.keys(options)
.filter(k => k !== 'path' && k !== 'webviewId')
.map(k => k + '=' + encodeURIComponent(options[k]))
.join('&');
if (queryString) {
fullPath += '?' + queryString;
}
// 构建完整 URL
let url = fullPath;
// #ifdef H5
url = window.location.origin + fullPath;
// #endif
// ✅ 调用 tracker.trackPageView
tracker.trackPageView({
path: fullPath,
url: url,
title: title,
query: options
});
if (this.debug) {
console.log('[App] 页面切换:', route, '→', title);
console.log('[App] URL:', url);
}
} catch (e) {
console.warn('[App] 页面切换追踪异常:', e);
}
},
// 检查用户状态的方法 防止多端操作导致此用户不存在 // 检查用户状态的方法 防止多端操作导致此用户不存在
async checkUserStatus() { async checkUserStatus() {
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
<view class="content-box"> <view class="content-box">
<view style="margin: 40rpx 30rpx;"> <view style="margin: 40rpx 30rpx;">
<view class="inputItem"> <view class="inputItem">
<input class="uni-input" name="invitationCode" maxlength="11" placeholder="输入手机号" v-model="mobile" /> <input data-track-name="填写登录手机号" class="uni-input" name="invitationCode" maxlength="11" placeholder="输入手机号" v-model="mobile" />
</view> </view>
<view class="login-code"> <view class="login-code">
<input style="font-size: 26rpx;" name="form.code" placeholder="输入验证码" v-model="code" type="number" maxlength="6" /> <input data-track-name="填写登录验证码" style="font-size: 26rpx;" name="form.code" placeholder="输入验证码" v-model="code" type="number" maxlength="6" />
<text style="width: 200rpx;text-align: right;" @click="sendMessage()" :class="{'grey':disabledSendBtn}">{{sendCode}}</text> <text style="width: 200rpx;text-align: right;" @click="sendMessage()" :class="{'grey':disabledSendBtn}">{{sendCode}}</text>
</view> </view>
......
// main.js
import App from './App'; import App from './App';
import { scrmUrl } from "./environments/environment"; import { scrmUrl } from "./environments/environment";
import tracker from '@/util/tracker/tracker.js' import { initTracker, trackerMixin } from '@/util/tracker/index.js'
// 从 track-events.js 导入映射
import {
FUNCTION_EVENT_MAP,
FUNCTION_MODULE_MAP,
FUNCTION_NAMES,
MODULE_NAMES
} from '@/util/tracker/track-events.js'
// ====== 导入 pages.json 自动生成模块映射 ======
import pagesJson from '@/pages.json' import pagesJson from '@/pages.json'
import {
EVENTS, FUNCTIONS, MODULES,
buildTrackParams
} from '@/util/tracker/track-events'
// ====== 构建期直接从 pages.json 生成模块映射(一次执行,运行时零开销)======
function buildModuleMap() { function buildModuleMap() {
const map = {} const map = {}
const keywordList = []
function processPages(pages, root = '') { function add(path, meta = {}, title = '') {
(pages || []).forEach((page) => { if (!path) return
const meta = (page.style && page.style.meta) || {} const key = path[0] === '/' ? path : '/' + path
if (meta.moduleCode) { map[key] = {
const fullPath = '/' + root + page.path moduleCode: meta.moduleCode || '',
map[fullPath] = { moduleName: meta.moduleName || meta.moduleCode || '',
moduleType: meta.moduleCode, title: title || ''
moduleName: meta.moduleName || meta.moduleCode
}
keywordList.push({
keyword: meta.moduleCode,
moduleType: meta.moduleCode,
moduleName: meta.moduleName || meta.moduleCode
})
if (meta.moduleName && meta.moduleName !== meta.moduleCode) {
keywordList.push({
keyword: meta.moduleName,
moduleType: meta.moduleCode,
moduleName: meta.moduleName
})
}
}
})
} }
}
function processPages(pages, root = '') {
(pages || []).forEach((page) => {
const style = page.style || {}
const prefix = root ? (root.endsWith('/') ? root : root + '/') : ''
add(prefix + (page.path || ''), style.meta || {}, style.navigationBarTitleText)
})
}
try {
processPages(pagesJson.pages) processPages(pagesJson.pages)
;(pagesJson.subPackages || []).forEach((sub) => { ;(pagesJson.subPackages || []).forEach((sub) => {
const root = sub.root || '' processPages(sub.pages, sub.root || '')
processPages(sub.pages, root + '/')
}) })
} catch (e) {
console.warn('[Tracker] 构建页面模块映射失败:', e)
}
return { exact: map, keyword: keywordList } return map
} }
const moduleMapData = buildModuleMap() // #ifndef VUE3
import Vue from 'vue'
// ====== 初始化 tracker ====== App.mpType = 'app'
tracker.init({
reportUrl: `${scrmUrl}/scrm-api/log/client/batch/save`,
projectType: 'CFFP',
clientType: 'H5',
source: 'direct', // 访问来源,可从 URL 参数获取
// 注入功能映射 // Vue 2 全局挂载操作日志
functionEventMap: FUNCTION_EVENT_MAP, const trackerVue2 = initTracker({
functionModuleMap: FUNCTION_MODULE_MAP, enabled: true,
functionNameMap: FUNCTION_NAMES, projectCode: 'CFFP',
api: `${scrmUrl}/scrm-api/log/client/batch/save`,
pageMetaMap: buildModuleMap() // ← 注入 pages.json 映射表
})
// 模块映射 Vue.mixin(trackerMixin) // 三端通用:页面级自动采集
moduleMap: moduleMapData.exact, // 业务统一入口:传 funcCode 即可
keywordMap: moduleMapData.keyword, Vue.prototype.$trackFunc = (funcCode, extra = {}, overrides = {}) =>
trackerVue2.trackFunc(funcCode, extra, overrides)
// 完全自定义入口
Vue.prototype.$track = (eventCode, params = {}) =>
trackerVue2.track(Object.assign({ eventCode }, params))
// 常量暴露,业务代码免 import
Vue.prototype.$EVENTS = EVENTS
Vue.prototype.$FUNCTIONS = FUNCTIONS
Vue.prototype.$MODULES = MODULES
debug: process.env.NODE_ENV === 'development', const app = new Vue({
dryRun: true // ✅ 开启模拟模式(只打印不上报) ...App
}) })
// #ifdef VUE3 app.$mount()
import { createSSRApp } from 'vue'; // #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() { export function createApp() {
const app = createSSRApp(App); const app = createSSRApp(App)
const tracker = initTracker({
enabled: true, // 操作日志上报给后端的全局开关,
projectCode: 'CFFP',
api: `${scrmUrl}/scrm-api/log/client/batch/save`,
pageMetaMap: buildModuleMap() // ← 注入 pages.json 映射表
})
app.mixin(trackerMixin) // 三端通用:页面级自动采集
// 业务统一入口:传 funcCode 即可
app.config.globalProperties.$trackFunc = (funcCode, extra = {}, overrides = {}) =>
tracker.trackFunc(funcCode, extra, overrides)
// 挂载到全局 // 完全自定义入口
app.config.globalProperties.$tracker = tracker; app.config.globalProperties.$track = (eventCode, params = {}) =>
tracker.track(Object.assign({ eventCode }, params))
return { // 常量暴露,业务代码免 import
app app.config.globalProperties.$EVENTS = EVENTS
}; app.config.globalProperties.$FUNCTIONS = FUNCTIONS
app.config.globalProperties.$MODULES = MODULES
return {
app
}
} }
// #endif // #endif
// #ifndef VUE3 //#ifdef H5
import Vue from 'vue'; window.sessionStorage.setItem('firstEntryUrl',window.location.href.split('#')[0])
App.mpType = 'app';
const app = new Vue({
...App
});
app.$mount();
// #endif // #endif
// #ifdef H5
if (typeof window !== 'undefined' && window.sessionStorage) {
window.sessionStorage.setItem('firstEntryUrl', window.location.href.split('#')[0]);
}
// #endif
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
<text class="require">*</text>昵称 <text class="desTxt">(用于邀请好友及统计业绩显示)</text> <text class="require">*</text>昵称 <text class="desTxt">(用于邀请好友及统计业绩显示)</text>
</view> </view>
<view class=""> <view class="">
<input class="user-input" maxlength="10" type="text" placeholder="请输入2~10个字符" <input data-track-name="填写申请加盟昵称" class="user-input" maxlength="10" type="text" placeholder="请输入2~10个字符"
v-model="form.nickName" /> v-model="form.nickName" />
</view> </view>
</view> </view>
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
<text class="require">*</text>手机号码 <text class="desTxt">(账号唯一标识)</text> <text class="require">*</text>手机号码 <text class="desTxt">(账号唯一标识)</text>
</view> </view>
<view class="codeBox"> <view class="codeBox">
<input type="number" class="user-input" name="mobileNo" placeholder="请输入手机号" <input data-track-name="填写申请加盟手机号码" type="number" class="user-input" name="mobileNo" placeholder="请输入手机号"
v-model="form.mobile" maxlength="11" :disabled="editMobile" /> v-model="form.mobile" maxlength="11" :disabled="editMobile" />
<view :class="{'grey':disabledSendBtn}" @click="sendMessage()" <view :class="{'grey':disabledSendBtn}" @click="sendMessage()"
v-if="showVerificationCode"> v-if="showVerificationCode">
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<text class="require">*</text>验证码 <text class="require">*</text>验证码
</view> </view>
<view class=""> <view class="">
<input type="number" class="user-input" name="mobileNo" placeholder="请输入短信验证码" <input data-track-name="填写申请加盟验证码" type="number" class="user-input" name="mobileNo" placeholder="请输入短信验证码"
v-model="form.code" maxlength="6" /> v-model="form.code" maxlength="6" />
</view> </view>
</view> </view>
......
...@@ -12,28 +12,28 @@ ...@@ -12,28 +12,28 @@
<view class="content"> <view class="content">
<form> <form>
<view class="inputItem" v-if="loginType!=='agentlogin'"> <view class="inputItem" v-if="loginType!=='agentlogin'">
<input type="number" class="uni-input" name="mobileNo" placeholder="输入手机号" v-model="form.mobile" maxlength="11"/> <input data-track-name="填写登录手机号" type="number" class="uni-input" name="mobileNo" placeholder="输入手机号" v-model="form.mobile" maxlength="11"/>
</view> </view>
<view class="inputItem" v-if="loginType==='agentlogin'"> <view class="inputItem" v-if="loginType==='agentlogin'">
<input class="uni-input" name="mobileNo" placeholder="输入账号" v-model="form.account" maxlength="11"/> <input data-track-name="填写登录账号" class="uni-input" name="mobileNo" placeholder="输入账号" v-model="form.account" maxlength="11"/>
</view> </view>
<view class="inputItem" v-if="loginType === 'codelogin' || loginType==='register' || loginType === 'resetpw'"> <view class="inputItem" v-if="loginType === 'codelogin' || loginType==='register' || loginType === 'resetpw'">
<input class="uni-input codeBtn" name="code" placeholder="输入验证码" v-model="form.code" type="number" maxlength="6"/> <input data-track-name="填写登录验证码" class="uni-input codeBtn" name="code" placeholder="输入验证码" v-model="form.code" type="number" maxlength="6"/>
<text style="font-size: 28rpx;" @click="sendMessage()" :class="{'grey':disabledSendBtn}">{{sendCodeHtml}}</text> <text style="font-size: 28rpx;" @click="sendMessage()" :class="{'grey':disabledSendBtn}">{{sendCodeHtml}}</text>
</view> </view>
<view class="inputItem" v-if="loginType === 'pwlogin' || loginType==='register' || loginType==='agentlogin'"> <view class="inputItem" v-if="loginType === 'pwlogin' || loginType==='register' || loginType==='agentlogin'">
<input class="uni-input" type="text" name="password" placeholder="输入密码" v-if="!passwordFlag" v-model="form.password"/> <input data-track-name="填写登录密码" class="uni-input" type="text" name="password" placeholder="输入密码" v-if="!passwordFlag" v-model="form.password"/>
<input class="uni-input" type="text" password name="password" placeholder="输入密码" v-if="passwordFlag" v-model="form.password"/> <input data-track-name="填写登录密码" class="uni-input" type="text" password name="password" placeholder="输入密码" v-if="passwordFlag" v-model="form.password"/>
<text v-if="!passwordFlag" @click="passwordFlag=true">显示密码</text> <text v-if="!passwordFlag" @click="passwordFlag=true">显示密码</text>
<text v-if="passwordFlag" @click="passwordFlag=false">不显示密码</text> <text v-if="passwordFlag" @click="passwordFlag=false">不显示密码</text>
</view> </view>
<!--重置密码--> <!--重置密码-->
<view v-if="loginType === 'resetpw'"> <view v-if="loginType === 'resetpw'">
<view class="inputItem" > <view class="inputItem" >
<input class="uni-input" type="text" password name="password" placeholder="密码(6-20位字母数字组合)" v-model="form.password"/> <input data-track-name="填写重置密码" class="uni-input" type="text" password name="password" placeholder="密码(6-20位字母数字组合)" v-model="form.password"/>
</view> </view>
<view class="inputItem" > <view class="inputItem" >
<input class="uni-input" type="text" password name="password" placeholder="再次确认密码" v-model="doublePassword"/> <input data-track-name="填写重置再次确认密码" class="uni-input" type="text" password name="password" placeholder="再次确认密码" v-model="doublePassword"/>
</view> </view>
</view> </view>
</form> </form>
......
...@@ -221,6 +221,7 @@ ...@@ -221,6 +221,7 @@
</view> </view>
<view class="inputBox"> <view class="inputBox">
<input <input
:data-track-name="`修改【${updateItem.name}】备注`"
class="userInput" class="userInput"
v-model="remarkName" v-model="remarkName"
maxlength="10" maxlength="10"
......
...@@ -654,6 +654,7 @@ ...@@ -654,6 +654,7 @@
}, },
"lazyLoad": true "lazyLoad": true
} }
], ],
"subPackages": [ "subPackages": [
{ {
...@@ -911,4 +912,4 @@ ...@@ -911,4 +912,4 @@
} }
] ]
} }
} }
\ No newline at end of file
...@@ -30,13 +30,13 @@ ...@@ -30,13 +30,13 @@
<view class="liBox"> <view class="liBox">
<text>昵称 <text style="font-size: 22rpx;">(被邀请人可修改)</text> :</text> <text>昵称 <text style="font-size: 22rpx;">(被邀请人可修改)</text> :</text>
<text> <text>
<input v-model="dataForm.name" class="uni-input" maxlength="10" placeholder="昵称" <input data-track-name="填写邀请昵称" v-model="dataForm.name" class="uni-input" maxlength="10" placeholder="昵称"
style="width: 300rpx;" /> style="width: 300rpx;" />
</text> </text>
</view> </view>
<view class="liBox"> <view class="liBox">
<text>手机号:</text> <text>手机号:</text>
<text><input v-model="dataForm.mobileNumber" class="uni-input" type="number" maxlength="11" <text><input data-track-name="填写邀请手机号" v-model="dataForm.mobileNumber" class="uni-input" type="number" maxlength="11"
placeholder="手机号" /></text> placeholder="手机号" /></text>
</view> </view>
</view> </view>
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
</view> </view>
</view> </view>
<view class="two"> <view class="two">
<view class="commonTitle" style="margin-top: 40rpx;"> <view data-track-name="自定义月份" class="commonTitle" style="margin-top: 40rpx;">
自定义 自定义
</view> </view>
<view style="width: 100%;" class="timePickerBox"> <view style="width: 100%;" class="timePickerBox">
...@@ -104,7 +104,7 @@ ...@@ -104,7 +104,7 @@
</view> </view>
</view> </view>
<view class="bottomBtn" @click="comfirm"> <view class="bottomBtn" @click="comfirm">
<view class="bottonTxt"> <view class="bottonTxt" data-track-name="选择月份确定">
确定 确定
</view> </view>
</view> </view>
......
...@@ -18,11 +18,11 @@ ...@@ -18,11 +18,11 @@
<view class="ulBox"> <view class="ulBox">
<view class="liBox"> <view class="liBox">
<view>昵称:</view> <view>昵称:</view>
<view><input class="uni-input" v-model="eduPlanParam.name" maxlength="10" placeholder="昵称长度不超过10" /></view> <view><input data-track-name="填写小孩昵称" class="uni-input" v-model="eduPlanParam.name" maxlength="10" placeholder="昵称长度不超过10" /></view>
</view> </view>
<view class="liBox"> <view class="liBox">
<view>年龄:</view> <view>年龄:</view>
<view><input class="uni-input" v-model="eduPlanParam.age" type="number" maxlength="2" placeholder="请输入周岁年龄" /></view> <view><input data-track-name="填写小孩年龄" class="uni-input" v-model="eduPlanParam.age" type="number" maxlength="2" placeholder="请输入周岁年龄" /></view>
</view> </view>
<view class="multiline liBox"> <view class="multiline liBox">
<view class="multilineItem"> <view class="multilineItem">
...@@ -35,24 +35,24 @@ ...@@ -35,24 +35,24 @@
<view class="multilineItem" v-if="haveAcceptEduFlag"> <view class="multilineItem" v-if="haveAcceptEduFlag">
<view>最高学历:</view> <view>最高学历:</view>
<view> <view>
<uni-data-picker placeholder="请选择最高学历" popup-title="请选择最高学历" :localdata="dataTree" v-model="eduPlanParam.maxGrade" <uni-data-picker data-track-name="选择最高学历" placeholder="请选择最高学历" popup-title="请选择最高学历" :localdata="dataTree" v-model="eduPlanParam.maxGrade"
@change="onchange" @nodeclick="onnodeclick"> @change="onchange" @nodeclick="onnodeclick">
</uni-data-picker> </uni-data-picker>
</view> </view>
</view> </view>
<view class="multilineItem" v-if="!haveAcceptEduFlag"> <view class="multilineItem" v-if="!haveAcceptEduFlag">
<view>几年后上学:</view> <view>几年后上学:</view>
<view><input class="uni-input" v-model="eduPlanParam.ngoLearn" type="number" maxlength="1" placeholder="请输入整数" /></view> <view><input data-track-name="填写几年后上学" class="uni-input" v-model="eduPlanParam.ngoLearn" type="number" maxlength="1" placeholder="请输入整数" /></view>
</view> </view>
</view> </view>
<view class="planGradeRange liBox"> <view class="planGradeRange liBox">
<view>规划阶段</view> <view>规划阶段</view>
<view> <view>
<uni-data-picker placeholder="规划开始" popup-title="请选择规划开始阶段" :localdata="dataTree" v-model="eduPlanParam.startPlan" <uni-data-picker data-track-name="选择规划开始阶段" placeholder="规划开始" popup-title="请选择规划开始阶段" :localdata="dataTree" v-model="eduPlanParam.startPlan"
@change="onchange" @nodeclick="onnodeclick"> @change="onchange" @nodeclick="onnodeclick">
</uni-data-picker> </uni-data-picker>
<text></text> <text></text>
<uni-data-picker placeholder="规划结束" popup-title="请选择规划结束阶段" :localdata="dataTree" v-model="eduPlanParam.endPlan" <uni-data-picker data-track-name="选择规划结束阶段" placeholder="规划结束" popup-title="请选择规划结束阶段" :localdata="dataTree" v-model="eduPlanParam.endPlan"
@change="onchange" @nodeclick="onnodeclick"> @change="onchange" @nodeclick="onnodeclick">
</uni-data-picker> </uni-data-picker>
</view> </view>
...@@ -64,11 +64,11 @@ ...@@ -64,11 +64,11 @@
<view class="ulBox"> <view class="ulBox">
<view class="liBox"> <view class="liBox">
<view>已备教育资金总额:</view> <view>已备教育资金总额:</view>
<view><input class="uni-input" type="digit" maxlength="15" v-model="eduPlanParam.initEducationFund" placeholder="最高保留两位小数" />万元</view> <view><input data-track-name="填写已备教育资金总额" class="uni-input" type="digit" maxlength="15" v-model="eduPlanParam.initEducationFund" placeholder="最高保留两位小数" />万元</view>
</view> </view>
<view class="liBox"> <view class="liBox">
<view>每年预备投入教育资金总额:</view> <view>每年预备投入教育资金总额:</view>
<view><input class="uni-input" type="digit" maxlength="15" v-model="eduPlanParam.educationDeposit" placeholder="最高保留两位小数" />万元</view> <view><input data-track-name="填写每年预备投入教育资金总额" class="uni-input" type="digit" maxlength="15" v-model="eduPlanParam.educationDeposit" placeholder="最高保留两位小数" />万元</view>
</view> </view>
</view> </view>
</view> </view>
...@@ -77,11 +77,11 @@ ...@@ -77,11 +77,11 @@
<view class="ulBox"> <view class="ulBox">
<view class="liBox"> <view class="liBox">
<view>学费增长率:</view> <view>学费增长率:</view>
<view><input class="uni-input" type="digit" maxlength="5" v-model="eduPlanParam.tuitionAddRate" placeholder="最高保留两位小数" />%</view> <view><input data-track-name="填写学费增长率" class="uni-input" type="digit" maxlength="5" v-model="eduPlanParam.tuitionAddRate" placeholder="最高保留两位小数" />%</view>
</view> </view>
<view class="liBox"> <view class="liBox">
<view>资产投资回报率:</view> <view>资产投资回报率:</view>
<view><input class="uni-input" type="digit" maxlength="5" v-model="eduPlanParam.investmentRate" placeholder="最高保留两位小数" />%</view> <view><input data-track-name="填写资产投资回报率" class="uni-input" type="digit" maxlength="5" v-model="eduPlanParam.investmentRate" placeholder="最高保留两位小数" />%</view>
</view> </view>
</view> </view>
</view> </view>
...@@ -99,8 +99,8 @@ ...@@ -99,8 +99,8 @@
</view> </view>
<view class="liBox" v-for="item in schoolingLists" :key="item.value"> <view class="liBox" v-for="item in schoolingLists" :key="item.value">
<view>{{item.text}}</view> <view>{{item.text}}</view>
<view><input class="uni-input" maxlength="1" type="number" placeholder="最高不超过10" v-model="item.commonYears" @blur="updateSchoolingLists(item)"/></view> <view><input :data-track-name="'填写学制-' + item.text" class="uni-input" maxlength="1" type="number" placeholder="最高不超过10" v-model="item.commonYears" @blur="updateSchoolingLists(item)"/></view>
<view><input class="uni-input" maxlength="10" type="digit" v-model="item.publicTuition" @blur="updateSchoolingLists(item)"/></view> <view><input :data-track-name="'填写首年预估费用-' + item.text" class="uni-input" maxlength="10" type="digit" v-model="item.publicTuition" @blur="updateSchoolingLists(item)"/></view>
<view><view class="settingButton" @click="settingYBY(item)">逐年设定</view></view> <view><view class="settingButton" @click="settingYBY(item)">逐年设定</view></view>
</view> </view>
</view> </view>
...@@ -167,7 +167,7 @@ ...@@ -167,7 +167,7 @@
</view> </view>
<view class="liBox" v-for="(item,index) in settingItem.children"> <view class="liBox" v-for="(item,index) in settingItem.children">
<view>{{item.text}}</view> <view>{{item.text}}</view>
<view><input class="uni-input" type="digit" placeholder="最高保留两位小数" v-model="item.tuition"/></view> <view><input :data-track-name="'逐年设定-' + settingItem.text + '-' + item.text" class="uni-input" type="digit" placeholder="最高保留两位小数" v-model="item.tuition"/></view>
</view> </view>
</view> </view>
<button type="default" plain="true" @click="confirmOption()">确定</button> <button type="default" plain="true" @click="confirmOption()">确定</button>
......
...@@ -23,21 +23,21 @@ ...@@ -23,21 +23,21 @@
<view class="inputItem"> <view class="inputItem">
<text>几年后购房:</text> <text>几年后购房:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="number" placeholder="请输入" <input data-track-name="填写几年后购房" class="uni-input" type="number" placeholder="请输入"
maxlength="3" v-model="housePurchaseInfo.nyear" /> maxlength="3" v-model="housePurchaseInfo.nyear" />
<label for="">年</label> <label for="">年</label>
</view> </view>
</view> </view>
<view class="inputItem"> <view class="inputItem">
<text>预购城市:</text> <text>预购城市:</text>
<uni-data-picker placeholder="请选择地区" popup-title="请选择所在地区" :localdata="provinceList" <uni-data-picker data-track-name="选择预购城市" placeholder="请选择地区" popup-title="请选择所在地区" :localdata="provinceList"
@change="onchange" @nodeclick="onnodeclick" v-model="housePurchaseInfo.cityId"> @change="onchange" @nodeclick="onnodeclick" v-model="housePurchaseInfo.cityId">
</uni-data-picker> </uni-data-picker>
</view> </view>
<view class="inputItem"> <view class="inputItem">
<text>预购面积:</text> <text>预购面积:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" maxlength="6" <input data-track-name="填写预购面积" class="uni-input" type="digit" placeholder="请输入" maxlength="6"
v-model="housePurchaseInfo.preOrderSize" @blur="queryCommercialHousingPrice()" v-model="housePurchaseInfo.preOrderSize" @blur="queryCommercialHousingPrice()"
/><label for="">m²</label> /><label for="">m²</label>
</view> </view>
...@@ -45,33 +45,33 @@ ...@@ -45,33 +45,33 @@
<view class="inputItem"> <view class="inputItem">
<text>预购房产现在市场单价:</text> <text>预购房产现在市场单价:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" <input data-track-name="填写预购房产现在市场单价" class="uni-input" type="digit" placeholder="请输入"
v-model="housePurchaseInfo.targetPrice" maxlength="12" v-model="housePurchaseInfo.targetPrice" maxlength="12"
/><label for="">元/m²</label> /><label for="">元/m²</label>
</view> </view>
</view> </view>
<view class="inputItem"> <view class="inputItem">
<text>首付款最低成数:</text> <text>首付款最低成数:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="number" placeholder="请输入" <input data-track-name="填写首付款最低成数" class="uni-input" type="number" placeholder="请输入"
maxlength="3" v-model="housePurchaseInfo.downPaymentMin" maxlength="3" v-model="housePurchaseInfo.downPaymentMin"
/><label for="">%</label> /><label for="">%</label>
</view> </view>
</view> </view>
<view class="inputItem"> <view class="inputItem">
<text>新房贷款年限:</text> <text>新房贷款年限:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="number" placeholder="请输入" <input data-track-name="填写新房贷款年限" class="uni-input" type="number" placeholder="请输入"
maxlength="3" v-model="housePurchaseInfo.newLoanYear" maxlength="3" v-model="housePurchaseInfo.newLoanYear"
/><label for="">年</label> /><label for="">年</label>
</view> </view>
</view> </view>
<view class="inputItem"> <view class="inputItem">
<text>新房贷款年利率:</text> <text>新房贷款年利率:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" <input data-track-name="填写新房贷款年利率" class="uni-input" type="digit" placeholder="请输入"
maxlength="4" v-model="housePurchaseInfo.interestRate" maxlength="4" v-model="housePurchaseInfo.interestRate"
/><label for="">%</label> /><label for="">%</label>
</view> </view>
</view> </view>
<view class="inputItem"> <view class="inputItem">
...@@ -96,9 +96,9 @@ ...@@ -96,9 +96,9 @@
<view class="inputItem"> <view class="inputItem">
<text>已备首付:</text> <text>已备首付:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" <input data-track-name="填写已备首付" class="uni-input" type="digit" placeholder="请输入"
v-model.trim="housePurchaseInfo.firstHouseAmount" maxlength="8" v-model.trim="housePurchaseInfo.firstHouseAmount" maxlength="8"
/><label for="">万元</label> /><label for="">万元</label>
</view> </view>
</view> </view>
<view class="inputItem" style="position: relative;"> <view class="inputItem" style="position: relative;">
...@@ -107,9 +107,9 @@ ...@@ -107,9 +107,9 @@
<view style="position: absolute;left: 0;bottom: -1px;font-size: 24rpx;color: red;letter-spacing: 1px;">(从今年末开始投入,用于首付和按揭)</view> <view style="position: absolute;left: 0;bottom: -1px;font-size: 24rpx;color: red;letter-spacing: 1px;">(从今年末开始投入,用于首付和按揭)</view>
</view> </view>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" <input data-track-name="填写每年投入房产资金" class="uni-input" type="digit" placeholder="请输入"
v-model="housePurchaseInfo.repaymentSpecial" maxlength="8" v-model="housePurchaseInfo.repaymentSpecial" maxlength="8"
/><label for="">万元/年</label> /><label for="">万元/年</label>
</view> </view>
</view> </view>
</view> </view>
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
<view class="inputItem"> <view class="inputItem">
<text>交费年限:</text> <text>交费年限:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="number" placeholder="请输入" <input data-track-name="填写交费年限" class="uni-input" type="number" placeholder="请输入"
v-model.trim="irrAndSimpleCalcuteParam.paymentPeriod" maxlength="3" v-model.trim="irrAndSimpleCalcuteParam.paymentPeriod" maxlength="3"
/><label for="">年</label> /><label for="">年</label>
</view> </view>
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
<view class="inputItem"> <view class="inputItem">
<text>年交保费:</text> <text>年交保费:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" maxlength="17" <input data-track-name="填写年交保费" class="uni-input" type="digit" placeholder="请输入" maxlength="17"
v-model.trim="irrAndSimpleCalcuteParam.annualPremium" v-model.trim="irrAndSimpleCalcuteParam.annualPremium"
/><label for="">元</label> /><label for="">元</label>
</view> </view>
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
<view class="inputItem" v-show="calcuteType==='2'"> <view class="inputItem" v-show="calcuteType==='2'">
<text>万能账户首年交费<text style="font-size: 24rpx;">(如有)</text>:</text> <text>万能账户首年交费<text style="font-size: 24rpx;">(如有)</text>:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" maxlength="17" <input data-track-name="填写万能账户首年交费" class="uni-input" type="digit" placeholder="请输入" maxlength="17"
v-model.trim="irrAndSimpleCalcuteParam.allFirstPayment" @input="withdrawalTypeInit()" v-model.trim="irrAndSimpleCalcuteParam.allFirstPayment" @input="withdrawalTypeInit()"
/><label for="">元</label> /><label for="">元</label>
</view> </view>
...@@ -72,14 +72,14 @@ ...@@ -72,14 +72,14 @@
<view class="inputItem"> <view class="inputItem">
<text>从第几个保单年度开始提取:</text> <text>从第几个保单年度开始提取:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="number" placeholder="请输入" maxlength="3" <input data-track-name="填写从第几个保单年度开始提取" class="uni-input" type="number" placeholder="请输入" maxlength="3"
v-model="item.withdrawalStart"/><label for="">年</label> v-model="item.withdrawalStart"/><label for="">年</label>
</view> </view>
</view> </view>
<view class="inputItem"> <view class="inputItem">
<text style="white-space: nowrap;">每年提取金额:</text> <text style="white-space: nowrap;">每年提取金额:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" maxlength="17" <input data-track-name="填写每年提取金额" class="uni-input" type="digit" placeholder="请输入" maxlength="17"
v-model="item.yearWithdrawalAmount" /><label for="">元</label> v-model="item.yearWithdrawalAmount" /><label for="">元</label>
</view> </view>
</view> </view>
...@@ -94,11 +94,11 @@ ...@@ -94,11 +94,11 @@
<text class="withdrawNumber">年提取金额(元)</text> <text class="withdrawNumber">年提取金额(元)</text>
</view> </view>
<view class="tableTd" v-for="(item,idx) in yearWithdrawalInfos"> <view class="tableTd" v-for="(item,idx) in yearWithdrawalInfos">
<input class="uni-input" type="number" placeholder="请输入" <input data-track-name="填写起领年度(年初)" class="uni-input" type="number" placeholder="请输入"
v-model="item.withdrawalStart"/> v-model="item.withdrawalStart"/>
<input class="uni-input" type="number" placeholder="请输入" <input data-track-name="填写止领年度(年末)" class="uni-input" type="number" placeholder="请输入"
v-model="item.withdrawalEnd"/> v-model="item.withdrawalEnd"/>
<input class="uni-input withdrawNumber" type="digit" placeholder="请输入" maxlength="17" <input data-track-name="填写年提取金额(元)" class="uni-input withdrawNumber" type="digit" placeholder="请输入" maxlength="17"
v-model="item.yearWithdrawalAmount"/> v-model="item.yearWithdrawalAmount"/>
<view v-show="yearWithdrawalInfos.length>1" style="position: absolute;right: 0;width: 60rpx;height: 60rpx;"> <view v-show="yearWithdrawalInfos.length>1" style="position: absolute;right: 0;width: 60rpx;height: 60rpx;">
<image @click="reduce(idx)" style="width: 60rpx;height: 60rpx;" src="../../static/Vector.png" mode=""></image> <image @click="reduce(idx)" style="width: 60rpx;height: 60rpx;" src="../../static/Vector.png" mode=""></image>
...@@ -127,7 +127,7 @@ ...@@ -127,7 +127,7 @@
<view class="inputItem"> <view class="inputItem">
<text>第:</text> <text>第:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="number" placeholder="请输入" min='1' maxlength="3" <input data-track-name="填写现金价值年份" class="uni-input" type="number" placeholder="请输入" min='1' maxlength="3"
v-model="irrAndSimple.nyear"/> v-model="irrAndSimple.nyear"/>
<label for="">年</label> <label for="">年</label>
</view> </view>
...@@ -135,7 +135,7 @@ ...@@ -135,7 +135,7 @@
<view class="inputItem"> <view class="inputItem">
<text>{{irrAndSimpleCalcuteParam.allFirstPayment > 0 ? '总生存利益(现金价值+万能账户现价)' :'现金价值'}}:</text> <text>{{irrAndSimpleCalcuteParam.allFirstPayment > 0 ? '总生存利益(现金价值+万能账户现价)' :'现金价值'}}:</text>
<view class="inputContent"> <view class="inputContent">
<input class="uni-input" type="digit" placeholder="请输入" min='1' maxlength="17" <input :data-track-name="irrAndSimpleCalcuteParam.allFirstPayment > 0 ? '填写总生存利益(现金价值+万能账户现价)' :'填写现金价值'" class="uni-input" type="digit" placeholder="请输入" min='1' maxlength="17"
v-model="irrAndSimple.cashValue"/> v-model="irrAndSimple.cashValue"/>
<label for="">元</label> <label for="">元</label>
</view> </view>
......
import { resolvePageInfo } from './utils'
import { EVENTS, normalizeEventCode } from './track-events'
let _tracker = null
export function bindTracker(t) { _tracker = t }
const MP_EVENT_MAP = {
tap: EVENTS.CLICK,
longpress: EVENTS.CLICK,
change: EVENTS.INPUT,
blur: EVENTS.INPUT,
input: EVENTS.INPUT
}
/** 小程序里判断是不是输入框:靠 dataset 标记或占位符兜底 */
function isInputNode(node) {
if (!node || !node.dataset) return false
const ds = node.dataset
if (ds.trackType === 'input') return true
if (ds.trackInput === 'true') return true
// 兜底:没有 textContent、有 placeholder 的通常是 input
return !node.dataset.trackName && !node.dataset.trackId && false
}
function parseDataset(ds = {}, evtType = 'tap') {
let extraParams = {}
if (ds.trackExtra) {
try { extraParams = JSON.parse(ds.trackExtra) } catch (e) {}
}
let eventCode = ds.trackEvent || ''
if (!eventCode && !ds.func) {
eventCode = MP_EVENT_MAP[evtType] || EVENTS.CLICK
}
if (eventCode) eventCode = normalizeEventCode(eventCode)
return {
eventCode,
funcCode: ds.func || '',
moduleCode: ds.trackModule || '',
actionTargetType: ds.trackType || 'button',
actionTarget: ds.trackId || '',
actionTargetName: ds.trackName || '',
extraParams
}
}
export const trackerMixin = {
onShow() { _tracker && _tracker.enterPage() }, //this
onHide() { _tracker && _tracker.leavePage('hide') },
onUnload() { _tracker && _tracker.leavePage('unload') },
methods: {
__trackTap(e) {
const t = _tracker
if (!t || !t.enabled) return
const ds = (e.currentTarget && e.currentTarget.dataset) || {}
// ★★★ 关键:输入框的 tap 跳过,等 __trackBlur 再上报 ★★★
if (ds.trackType === 'input' || ds.trackInput === 'true') return
// 无任何标注的点击不采集
if (!ds.trackId && !ds.trackName && !ds.trackType && !ds.func) return
t.track(parseDataset(ds, 'tap'))
},
__trackBlur(e) {
const t = _tracker
if (!t || !t.enabled) return
const ds = (e.currentTarget && e.currentTarget.dataset) || {}
const val = (e.detail && e.detail.value) || ''
// ★ 值没变化不报(空点一下再离开)
const key = e.currentTarget
if (!key._trackLastVal === undefined) key._trackLastVal = undefined
if (key._trackLastVal === val) return
key._trackLastVal = val
const info = parseDataset(ds, 'blur')
info.eventCode = EVENTS.INPUT
info.actionTargetType = 'input'
info.actionTargetName = ds.trackName || ds.placeholder || '输入框'
info.extraParams.valueLen = val.length
t.track(info)
},
$trackFunc(funcCode, extra = {}, overrides = {}) {
_tracker && _tracker.trackFunc(funcCode, extra, overrides)
}
}
}
export function setupMpCollector(tracker) { bindTracker(tracker) }
\ No newline at end of file
export default {
enabled: false, // 全局开关(可由后端配置下发动态修改)
projectCode: 'CFFP',
api: '/api/log/batch',
batchSize: 10, // 攒多少条触发上报
maxBatch: 50, // 单次请求最多携带条数
flushInterval: 5000, // 定时上报间隔 ms
maxQueue: 200, // 队列上限
maxRetry: 3, // 失败重试次数
sessionTimeout: 30 * 60 * 1000, // 30 分钟无操作开新会话
sampleRate: 1, // 采样率 0~1
maxExtraSize: 2048, // extraParams 最大字节
dedupeWindow: 300, // 元素去重窗口 ms
initDelay: 800 ,// 延迟初始化 ms
reportPageLeave: false, // ← 是否上报 PAGE_LEAVE(默认 false = 不上报)
keepStayTime: true,// ← 关闭 LEAVE 时,是否把停留时长挂到下一次 PAGE_VIEW
// H5:全自动埋点(点击/输入由 DOM 采集器自动上报,页面无需写任何埋点代码)
// APP / 小程序:无可靠 DOM,维持手动埋点
// #ifdef H5
autoClickCapture: true,
// #endif
// #ifndef H5
autoClickCapture: false,
// #endif
}
\ No newline at end of file
import Tracker from './core'
import config from './config'
import { setupDomCollector } from './collector-dom'
import { setupMpCollector, trackerMixin, bindTracker } from './collector-mp'
import { syncTrackerIdentity } from './utils'
let _instance = null
export function initTracker(options = {}) {
if (_instance) return _instance
Object.assign(config, options)
_instance = new Tracker(config)
bindTracker(_instance)
// 延迟初始化,避免抢占首屏资源
setTimeout(() => {
// #ifdef H5 || APP-PLUS
setupDomCollector(_instance)
// #endif
// #ifdef MP
setupMpCollector(_instance)
// #endif
}, config.initDelay)
return _instance
}
/**
* 宿主(微信小程序 / APP 的 webview)通过 URL 带入 trackerSessionId / trackerVisitorCode 时调用。
* 优先级:入参 query > URL 参数。实例未初始化时先落缓存,初始化时自动读取。
* @param {Object} query 一般传 App.vue onLaunch 的 options.query
*/
export function syncTrackerFromQuery(query = {}) {
const { visitorCode, sessionId } = syncTrackerIdentity({
visitorCode: query.trackerVisitorCode || '',
sessionId: query.trackerSessionId || ''
})
if (!visitorCode && !sessionId) return false
if (_instance) _instance.setIdentity({ visitorCode, sessionId })
return true
}
export function useTracker() { return _instance }
export function setTrackerEnabled(v) { _instance && _instance.setEnabled(v) }
export { trackerMixin }
/**
* 手动埋点统一入口(业务页面唯一该调的埋点 API)
* ============================================================
* 定位:业务层 → 上报协议 的防腐层。这里只做三件事:
* 1. 收口参数(funcCode / eventCode / actionTarget* / remark 的组装规则)
* 2. 统一 try/catch —— 埋点异常绝不允许打断业务(跳转、赋值、请求)
* 3. 统一口径 —— 敏感值(金额/手机号等)只传长度,不传明文
*
* 不在这里做的事:队列、批量、重试、sendBeacon —— 那些在 core.js
*
* 用法:
* trackClick(FUNCTIONS.TOOLS_PENSION, 'pension_calc', '开始计算')
* trackField(FUNCTIONS.TOOLS_PENSION, 'monthPaymentWage', val, { fieldName: '本年月缴费工资', actionType: 'input' })
* trackView(FUNCTIONS.TOOLS_PENSION_DETAIL, 'pension_result', '养老金测算结果', { hasResult: '1' })
* trackEvent(FUNCTIONS.WECHAT_LOGIN, { target: 'login_phone_success', name: '手机号快捷登录成功' }) // eventCode 由 funcCode 推导
*/
import { useTracker } from './index'
import { EVENTS } from './track-events'
function _report(funcCode, extra, overrides) {
try {
const t = useTracker()
if (!t || !t.enabled) return
t.trackFunc(funcCode, extra, overrides)
} catch (e) {
console.warn('[Tracker] 手动埋点上报失败', e)
}
}
/**
* 底层入口:eventCode 不传时沿用 core 的推导规则(funcCode → FUNCTION_EVENT_MAP)
* @param {string} funcCode FUNCTIONS.xxx
* @param {Object} opts { target, name, targetType, eventCode, extra, remark }
*/
export function trackEvent(funcCode, opts = {}) {
try {
const { target = '', name = '', targetType = 'button', eventCode, extra = {}, remark } = opts
const overrides = {
actionTargetType: targetType,
actionTarget: target,
actionTargetName: name || target,
remark: remark || name
}
if (eventCode) overrides.eventCode = eventCode
_report(funcCode, Object.assign({ actionTarget: target }, extra), overrides)
} catch (e) {
console.warn('[Tracker] 手动埋点上报失败', e)
}
}
/**
* 点击类:按钮、图标、展开收起等
*/
export function trackClick(funcCode, target, name, extra = {}) {
trackEvent(funcCode, {
target,
name,
targetType: 'button',
eventCode: EVENTS.CLICK,
extra,
remark: `点击${name}`
})
}
/**
* 值变更类:下拉选中、输入框失焦
* @param {*} value 用户输入/选择的值(不会明文上报,只取长度)
* @param {Object} opts { fieldName, actionType: 'select'|'input', target, extra, includeValue }
* includeValue=true 时才把明文放进 extraParams.value(仅限非敏感字段)
*/
export function trackField(funcCode, field, value, opts = {}) {
try {
const { fieldName = field, actionType = 'select', target, extra = {}, includeValue = false } = opts
const v = value === undefined || value === null ? '' : String(value)
const payload = Object.assign({
field,
actionType,
valueLen: v.length,
hasValue: v ? '1' : '0'
}, includeValue ? { value: v } : {}, extra)
trackEvent(funcCode, {
target: target || `field_${field}`,
name: fieldName,
targetType: actionType === 'input' ? 'input' : 'picker',
eventCode: EVENTS.INPUT,
extra: payload,
remark: `${actionType === 'input' ? '编辑' : '选择'}${fieldName}`
})
} catch (e) {
console.warn('[Tracker] 手动埋点上报失败', e)
}
}
/**
* 曝光类:详情页/结果页展示
*/
export function trackView(funcCode, target, name, extra = {}, targetType = 'page') {
trackEvent(funcCode, {
target,
name,
targetType,
eventCode: EVENTS.VIEW_DETAIL,
extra,
remark: `查看${name}`
})
}
let _device = null
let _clientType = null
export function formatTime(ts = Date.now()) {
const d = new Date(ts)
const p = (n, l = 2) => String(n).padStart(l, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +
`${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}
/** 设备信息只取一次,同步 API 不重复调用 */
export function getDeviceInfo() {
if (_device) return _device
const info = { platform: '', osVersion: '', brand: '', model: '', screenWidth: 0, screenHeight: 0 }
try {
const s = uni.getSystemInfoSync()
info.platform = s.platform || ''
info.osVersion = s.osVersion || ''
info.brand = s.brand || ''
info.model = s.model || ''
info.screenWidth = s.screenWidth || 0
info.screenHeight = s.screenHeight || 0
} catch (e) {
console.warn('[Tracker] 获取设备信息失败:', e)
}
_device = info
return info
}
export function detectClientType() {
if (_clientType) return _clientType
let type = 'UNKNOWN'
// #ifdef APP-PLUS
try {
type = 'APP_' + (plus.os.name === 'iOS' ? 'IOS' : 'ANDROID')
} catch (e) { type = 'APP' }
// #endif
// #ifdef MP-WEIXIN
type = 'MP'
// #endif
// #ifdef H5
const ua = (navigator.userAgent || '').toLowerCase()
if (ua.includes('micromessenger')) {
type = 'MP'
} else {
const isMobile = /android|iphone|ipad|ipod|blackberry|windows phone/i.test(ua)
type = isMobile ? 'H5' : 'PC'
}
// #endif
_clientType = type
return type
}
export const STORAGE_KEY_VISITOR = 'tracker_visitor_code'
export const STORAGE_KEY_SESSION = 'tracker_sessionId'
/**
* 读取 URL 上的 query 参数(宿主 webview 内嵌时由外部传入)
* 同时兼容 hash 路由:/#/pages/index?a=1 与 /?a=1#/pages/index
*/
export function getUrlParam(name) {
// #ifdef H5
try {
const raws = []
const search = (typeof window !== 'undefined' && window.location.search) || ''
const hash = (typeof window !== 'undefined' && window.location.hash) || ''
if (search) raws.push(search)
const i = hash.indexOf('?')
if (i > -1) raws.push(hash.slice(i))
const reg = new RegExp('[?&]' + name + '=([^&]*)')
for (let k = 0; k < raws.length; k++) {
const m = raws[k].match(reg)
if (m && m[1]) return decodeURIComponent(m[1])
}
} catch (e) {}
// #endif
// #ifdef APP-PLUS
try {
const wv = typeof plus !== 'undefined' && plus.webview && plus.webview.currentWebview()
const q = (wv && wv.query) || {}
if (q[name]) return String(q[name])
} catch (e) {}
// #endif
return ''
}
/**
* 运行时同步身份标识(优先级:显式入参 > URL 参数)
* @returns {Object} 最终生效的 { visitorCode, sessionId }(未变更的字段为 '')
*/
export function syncTrackerIdentity({ visitorCode = '', sessionId = '' } = {}) {
const code = visitorCode || getUrlParam('trackerVisitorCode')
const sess = sessionId || getUrlParam('trackerSessionId')
if (code) uni.setStorageSync(STORAGE_KEY_VISITOR, code)
if (sess) uni.setStorageSync(STORAGE_KEY_SESSION, sess)
return { visitorCode: code, sessionId: sess }
}
export function getVisitorCode() {
// URL 参数优先,且必须覆盖旧缓存(宿主每次外链都会带最新值)
let code = getUrlParam('trackerVisitorCode') || uni.getStorageSync(STORAGE_KEY_VISITOR)
if (!code) code = 'vis_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8)
uni.setStorageSync(STORAGE_KEY_VISITOR, code)
return code
}
/**
* 生成/读取 sessionId
* @param {boolean} forceNew true = 强制生成新会话(空闲超时续期用,忽略 URL/缓存)
* false = URL 参数 > 缓存 > 生成
*/
export function genSessionId(forceNew = false) {
if (!forceNew) {
const fromUrl = getUrlParam('trackerSessionId')
if (fromUrl) {
uni.setStorageSync(STORAGE_KEY_SESSION, fromUrl)
return fromUrl
}
const stored = uni.getStorageSync(STORAGE_KEY_SESSION)
if (stored) return stored
}
const sessionId = 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6)
uni.setStorageSync(STORAGE_KEY_SESSION, sessionId)
return sessionId
}
/**
* 三端通用:解析当前页面的 route / title / moduleCode / moduleName
* @param {Object} page 可选,页面实例
* @param {Object} pageMetaMap 可选,main.js 注入的 pages.json 映射表
*/
export function resolvePageInfo(page, pageMetaMap = {}) {
let route = ''
let pageInstance = page
try {
const pages = getCurrentPages()
const p = page || pages[pages.length - 1]
if (p) {
pageInstance = p
route = p.route || (p.$page && p.$page.path) || ''
}
} catch (e) {}
if (!route) return { url: '', title: '', moduleCode: '', moduleName: '' }
const key = route[0] === '/' ? route : '/' + route
// 1. 运行时 $page.meta(部分端/场景可用,作为覆盖)
let runtimeMeta = {}
try {
runtimeMeta = (pageInstance && pageInstance.$page && pageInstance.$page.meta) || {}
} catch (e) {}
// 2. main.js 注入的 pages.json 映射
const cfg = pageMetaMap[key] || {}
return {
url: key,
title: runtimeMeta.navigationBarTitleText || cfg.title || '',
moduleCode: runtimeMeta.moduleCode || cfg.moduleCode || '',
moduleName: runtimeMeta.moduleName || cfg.moduleName || ''
}
}
/** 安全 JSON 序列化(带大小校验) */
export function safeStringify(obj, maxSize) {
try {
const s = JSON.stringify(obj || {})
return s.length > maxSize ? null : s
} catch (e) {
return null
}
}
// #ifdef H5
export function sendBeacon(url, payload) {
try {
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' })
return navigator.sendBeacon(url, blob)
}
} catch (e) {}
return false
}
// #endif
const IDLE_TIMEOUT = 30 * 60 * 1000 // const IDLE_TIMEOUT = 30 * 60 * 1000
// const IDLE_TIMEOUT = 3 * 1000 const IDLE_TIMEOUT = 5 * 1000
const EXIT_COUNTDOWN = 30 const EXIT_COUNTDOWN = 30
const WATCH_INTERVAL = 10 * 1000 const WATCH_INTERVAL = 10 * 1000
const ACTIVITY_THROTTLE = 300 const ACTIVITY_THROTTLE = 300
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment