@rk911/tracker-sdk 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,483 @@
1
+ export class WebAdapter {
2
+ constructor(config) {
3
+ this.config = config
4
+ this._listeners = []
5
+ }
6
+
7
+ getPlatformName() {
8
+ return 'web'
9
+ }
10
+
11
+ // [PROJECT-ADAPT] 存储实现
12
+ // 当前依赖 uni.getStorageSync(uni-app 运行时)
13
+ // 非 uni-app 项目需替换为 localStorage.getItem
14
+
15
+ getStorage(key) {
16
+ try {
17
+ return uni.getStorageSync(key)
18
+ } catch (e) {
19
+ return null
20
+ }
21
+ }
22
+
23
+ setStorage(key, value) {
24
+ try {
25
+ uni.setStorageSync(key, value)
26
+ } catch (e) {}
27
+ }
28
+
29
+ removeStorage(key) {
30
+ try {
31
+ uni.removeStorageSync(key)
32
+ } catch (e) {}
33
+ }
34
+
35
+ getSessionStorage(key) {
36
+ try {
37
+ if (typeof sessionStorage !== 'undefined') {
38
+ const val = sessionStorage.getItem(key)
39
+ return val ? JSON.parse(val) : null
40
+ }
41
+ } catch (e) {}
42
+ return null
43
+ }
44
+
45
+ setSessionStorage(key, value) {
46
+ try {
47
+ if (typeof sessionStorage !== 'undefined') {
48
+ sessionStorage.setItem(key, JSON.stringify(value))
49
+ }
50
+ } catch (e) {}
51
+ }
52
+
53
+ removeSessionStorage(key) {
54
+ try {
55
+ if (typeof sessionStorage !== 'undefined') {
56
+ sessionStorage.removeItem(key)
57
+ }
58
+ } catch (e) {}
59
+ }
60
+
61
+ // ---- 多源路由检测(交叉验证,投票取准)----
62
+
63
+ getCurrentPagePath() {
64
+ var sources = []
65
+
66
+ // 来源 1: window.location.hash(浏览器地址栏,导航时同步更新,最可靠)
67
+ var hashRoute = this._getRouteFromHash()
68
+ if (hashRoute) sources.push(hashRoute)
69
+
70
+ // 来源 2: getCurrentPages() 栈顶(uni-app 内部,导航过程中可能延迟更新)
71
+ var stackRoute = this._getRouteFromPageStack()
72
+ if (stackRoute) sources.push(stackRoute)
73
+
74
+ // 来源 3: history.state(部分 uni-app 版本会写入路由信息)
75
+ var stateRoute = this._getRouteFromHistoryState()
76
+ if (stateRoute) sources.push(stateRoute)
77
+
78
+ if (sources.length === 0) return '/'
79
+
80
+ // 投票:多数一致取多数,全不一致取地址栏(地址栏是用户可见的 ground truth)
81
+ if (sources.length === 1) return sources[0]
82
+
83
+ var voteMap = {}
84
+ for (var i = 0; i < sources.length; i++) {
85
+ voteMap[sources[i]] = (voteMap[sources[i]] || 0) + 1
86
+ }
87
+
88
+ // 找出票数最多的
89
+ var maxVotes = 0
90
+ var winner = sources[0]
91
+ for (var route in voteMap) {
92
+ if (voteMap[route] > maxVotes) {
93
+ maxVotes = voteMap[route]
94
+ winner = route
95
+ }
96
+ }
97
+
98
+ // 地址栏是用户可见的 ground truth:导航过程中 pageStack/historyState 可能滞后,
99
+ // 只要 hash 有值且与投票结果不一致,以 hash 为准
100
+ if (hashRoute && winner !== hashRoute) {
101
+ return hashRoute
102
+ }
103
+
104
+ return winner
105
+ }
106
+
107
+ _getRouteFromHash() {
108
+ try {
109
+ if (typeof window === 'undefined') return null
110
+ var hash = window.location.hash || ''
111
+ if (!hash || hash === '#' || hash === '#/') return null
112
+ // uni-app H5 hash 格式: #/pages/index/index?query=xxx
113
+ var path = hash.replace(/^#/, '').split('?')[0]
114
+ if (path && path !== '/') return path
115
+ } catch (e) {}
116
+ return null
117
+ }
118
+
119
+ _getRouteFromPageStack() {
120
+ try {
121
+ var pages = getCurrentPages()
122
+ if (pages.length > 0) {
123
+ var route = pages[pages.length - 1].route || ''
124
+ if (route) {
125
+ return route.charAt(0) === '/' ? route : '/' + route
126
+ }
127
+ }
128
+ } catch (e) {}
129
+ return null
130
+ }
131
+
132
+ _getRouteFromHistoryState() {
133
+ try {
134
+ if (typeof window === 'undefined' || !window.history || !window.history.state) return null
135
+ var state = window.history.state
136
+ // uni-app 某些版本的 history.state 结构
137
+ if (state && state.current) {
138
+ var path = state.current.split('?')[0]
139
+ if (path && path !== '/') {
140
+ return path.charAt(0) === '/' ? path : '/' + path
141
+ }
142
+ }
143
+ } catch (e) {}
144
+ return null
145
+ }
146
+
147
+ getCurrentPageRoute() {
148
+ var path = this.getCurrentPagePath()
149
+ return path === '/' ? '' : path.replace(/^\//, '')
150
+ }
151
+
152
+ getPageTitle() {
153
+ try {
154
+ if (typeof document !== 'undefined' && document.title) {
155
+ return document.title
156
+ }
157
+ } catch (e) {}
158
+ return null
159
+ }
160
+
161
+ getToken() {
162
+ return this.getStorage(this.config.tokenKey || 'token')
163
+ }
164
+
165
+ // [PROJECT-ADAPT] 网络请求实现
166
+ // 当前依赖 uni.request(uni-app 运行时)
167
+ // 非 uni-app 项目需替换为 fetch/axios
168
+
169
+ request(options) {
170
+ return new Promise((resolve, reject) => {
171
+ uni.request({
172
+ ...options,
173
+ success: (res) => resolve(res),
174
+ fail: (err) => reject(err),
175
+ })
176
+ })
177
+ }
178
+
179
+ sendBeacon(url, data) {
180
+ try {
181
+ if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
182
+ return navigator.sendBeacon(
183
+ url,
184
+ typeof data === 'string' ? data : JSON.stringify(data)
185
+ )
186
+ }
187
+ } catch (e) {}
188
+ // 降级为同步请求
189
+ try {
190
+ uni.request({
191
+ method: 'POST',
192
+ url: url,
193
+ data: data,
194
+ header: { 'content-type': 'application/json' },
195
+ })
196
+ } catch (e) {}
197
+ return true
198
+ }
199
+
200
+ // ---- 网络状态 ----
201
+
202
+ getNetworkType() {
203
+ try {
204
+ if (typeof navigator !== 'undefined' && navigator.connection) {
205
+ return navigator.connection.effectiveType || 'unknown'
206
+ }
207
+ } catch (e) {}
208
+ return typeof navigator !== 'undefined' && navigator.onLine ? 'wifi' : 'offline'
209
+ }
210
+
211
+ // ---- 设备信息 ----
212
+
213
+ getDeviceInfo() {
214
+ try {
215
+ const info = uni.getSystemInfoSync()
216
+ var ua = (typeof navigator !== 'undefined' ? navigator.userAgent : '').toLowerCase()
217
+ return {
218
+ brand: info.brand || this._judgeBrand(ua, info.model),
219
+ model: info.model || '',
220
+ platform: info.platform || '',
221
+ system: info.system || '',
222
+ screenWidth: info.screenWidth || 0,
223
+ screenHeight: info.screenHeight || 0,
224
+ userAgent:
225
+ typeof navigator !== 'undefined' ? navigator.userAgent : '',
226
+ }
227
+ } catch (e) {
228
+ return {}
229
+ }
230
+ }
231
+
232
+ // ---- 生命周期事件 ----
233
+
234
+ onPageShow(callback) {
235
+ if (typeof window !== 'undefined') {
236
+ const handler = () => callback()
237
+ window.addEventListener('pageshow', handler)
238
+ this._listeners.push({ target: window, event: 'pageshow', handler })
239
+ }
240
+ }
241
+
242
+ onPageHide(callback) {
243
+ if (typeof document !== 'undefined') {
244
+ const handler = () => {
245
+ if (document.visibilityState === 'hidden') {
246
+ callback()
247
+ }
248
+ }
249
+ document.addEventListener('visibilitychange', handler)
250
+ this._listeners.push({
251
+ target: document,
252
+ event: 'visibilitychange',
253
+ handler,
254
+ })
255
+ }
256
+ }
257
+
258
+ onPageClose(callback) {
259
+ if (typeof window !== 'undefined') {
260
+ const handler = () => callback()
261
+ window.addEventListener('beforeunload', handler)
262
+ this._listeners.push({ target: window, event: 'beforeunload', handler })
263
+ }
264
+ }
265
+
266
+ // ---- DOM 能力 ----
267
+
268
+ supportsDOM() {
269
+ return typeof document !== 'undefined'
270
+ }
271
+
272
+ supportsIntersectionObserver() {
273
+ return typeof IntersectionObserver !== 'undefined'
274
+ }
275
+
276
+ addClickListener(handler) {
277
+ if (typeof document !== 'undefined') {
278
+ document.addEventListener('click', handler, true)
279
+ this._listeners.push({ target: document, event: 'click', handler, capture: true })
280
+ }
281
+ }
282
+
283
+ removeClickListener(handler) {
284
+ if (typeof document !== 'undefined') {
285
+ document.removeEventListener('click', handler, true)
286
+ }
287
+ }
288
+
289
+ addErrorListener(handler) {
290
+ if (typeof window !== 'undefined') {
291
+ window.addEventListener('error', handler, true)
292
+ this._listeners.push({ target: window, event: 'error', handler, capture: true })
293
+ }
294
+ }
295
+
296
+ addUnhandledRejectionListener(handler) {
297
+ if (typeof window !== 'undefined') {
298
+ window.addEventListener('unhandledrejection', handler)
299
+ this._listeners.push({
300
+ target: window,
301
+ event: 'unhandledrejection',
302
+ handler,
303
+ })
304
+ }
305
+ }
306
+
307
+ // ---- 路由监听 ----
308
+
309
+ onRouteChange(callback) {
310
+ if (typeof window === 'undefined' || typeof history === 'undefined') return
311
+
312
+ const originalPushState = history.pushState
313
+ const originalReplaceState = history.replaceState
314
+
315
+ history.pushState = function (...args) {
316
+ originalPushState.apply(this, args)
317
+ callback({ type: 'pushState' })
318
+ }
319
+ history.replaceState = function (...args) {
320
+ originalReplaceState.apply(this, args)
321
+ callback({ type: 'replaceState' })
322
+ }
323
+
324
+ const popstateHandler = () => callback({ type: 'popstate' })
325
+ window.addEventListener('popstate', popstateHandler)
326
+ this._listeners.push({
327
+ target: window,
328
+ event: 'popstate',
329
+ handler: popstateHandler,
330
+ })
331
+
332
+ // uni-app H5 使用 hash 路由,navigateTo/reLaunch 可能只触发 hashchange
333
+ const hashchangeHandler = () => callback({ type: 'hashchange' })
334
+ window.addEventListener('hashchange', hashchangeHandler)
335
+ this._listeners.push({
336
+ target: window,
337
+ event: 'hashchange',
338
+ handler: hashchangeHandler,
339
+ })
340
+
341
+ this._originalPushState = originalPushState
342
+ this._originalReplaceState = originalReplaceState
343
+ }
344
+
345
+ // ---- 设备注册信息 ----
346
+
347
+ getDetailedDeviceInfo() {
348
+ var info = {}
349
+ try {
350
+ info = uni.getSystemInfoSync() || {}
351
+ } catch (e) {}
352
+
353
+ var ua = (typeof navigator !== 'undefined' ? navigator.userAgent : '').toLowerCase()
354
+
355
+ return {
356
+ appVersion: '1.0',
357
+ browserType: info.browserName || '',
358
+ browserVersion: info.browserVersion || '',
359
+ deviceBrand: this._judgeBrand(ua, info.deviceModel || info.model),
360
+ deviceCode: info.deviceId || '',
361
+ deviceModel: info.deviceModel || '',
362
+ deviceType: this._getDeviceType(ua),
363
+ deviceVersion: info.osVersion || '',
364
+ }
365
+ }
366
+
367
+ // [PROJECT-ADAPT] 设备品牌/类型识别
368
+ // 当前偏向中国市场手机品牌,国际化项目需扩展
369
+ // 返回的 deviceType 数值需与后台枚举对齐
370
+ _judgeBrand(ua, model) {
371
+ if (ua.indexOf('iphone') > -1) return 'iPhone'
372
+ if (ua.indexOf('huawei') > -1 || ua.indexOf('honor') > -1) return 'Huawei'
373
+ if (ua.indexOf('oppo') > -1 || ua.indexOf('pacm00') > -1) return 'Oppo'
374
+ if (ua.indexOf('vivo') > -1) return 'vivo'
375
+ // 精确匹配小米,避免裸子串 'mi' 命中微信/企业微信 UA 里的 "MicroMessenger"
376
+ // (曾把微信内所有非小米安卓机误判成小米)。/\bmi[\s-]?\d/ 匹配 "Mi 11"/"MI9",不匹配 "micromessenger"
377
+ if (ua.indexOf('xiaomi') > -1 || ua.indexOf('redmi') > -1 ||
378
+ ua.indexOf('miui') > -1 || /\bmi[\s-]?\d/.test(ua)) return 'mi'
379
+ if (ua.indexOf('sm-') > -1) return 'Samsung'
380
+ if (ua.indexOf('lg-') > -1 || ua.indexOf('lm-') > -1) return 'LG'
381
+
382
+ // UA 未命中时按设备型号前缀兜底识别(微信 webview 常拿不到 UA 品牌,但 uni 能给型号)
383
+ var m = (model || '').toString().toUpperCase()
384
+ if (m) {
385
+ if (/^V\d{3,}/.test(m)) return 'vivo' // vivo: V2283A / V2055A ...
386
+ if (/^(PA|PB|PC|PD|PE|CPH)/.test(m)) return 'Oppo' // oppo/一加型号前缀
387
+ if (/^(MI |MI-|M\d{4}|2\d{6})/.test(m) || m.indexOf('REDMI') > -1) return 'mi'
388
+ if (/^(ELS|NOH|LIO|ANA|TAS|VOG|BRQ)/.test(m)) return 'Huawei'
389
+ if (m.indexOf('SM-') === 0) return 'Samsung'
390
+ }
391
+ return '其他型号手机'
392
+ }
393
+
394
+ _getDeviceType(ua) {
395
+ if (ua.indexOf('ipad') > -1 && ua.indexOf('mac') > -1) return 3
396
+ if (ua.indexOf('iphone') > -1 && ua.indexOf('mac os') > -1) return 6
397
+ if (ua.indexOf('iphone') > -1) return 2
398
+ if (ua.indexOf('android') > -1 && ua.indexOf('ipad') > -1) return 4
399
+ if (ua.indexOf('android') > -1) return 1
400
+ if (ua.indexOf('linux') > -1) return 7
401
+ if (ua.indexOf('windows') > -1) return 5
402
+ return 1
403
+ }
404
+
405
+ // ---- 工具 ----
406
+
407
+ generateId(prefix) {
408
+ return (
409
+ (prefix || '') +
410
+ '_' +
411
+ Date.now() +
412
+ '_' +
413
+ Math.random().toString(36).substr(2, 9)
414
+ )
415
+ }
416
+
417
+ getTimestamp() {
418
+ return Date.now()
419
+ }
420
+
421
+ getXPath(el) {
422
+ if (!el || !el.tagName) return ''
423
+ const parts = []
424
+ let current = el
425
+ while (current && current.nodeType === 1 && current !== document.body) {
426
+ let index = 1
427
+ let sibling = current.previousElementSibling
428
+ while (sibling) {
429
+ if (sibling.tagName === current.tagName) index++
430
+ sibling = sibling.previousElementSibling
431
+ }
432
+ parts.unshift(current.tagName.toLowerCase() + '[' + index + ']')
433
+ current = current.parentElement
434
+ }
435
+ return '/' + parts.join('/')
436
+ }
437
+
438
+ // [PROJECT-ADAPT] 同 click.js 的 __vue__ 检测依赖
439
+ findTrackableElement(el) {
440
+ const trackable = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'IMG']
441
+ let current = el
442
+ let depth = 0
443
+ let firstClickable = null
444
+
445
+ while (current && current !== document.body && depth < 10) {
446
+ // 优先:有 data-track-id 标记的
447
+ if (current.dataset && current.dataset.trackId) return current
448
+ // 标准可交互元素
449
+ if (trackable.indexOf(current.tagName) >= 0) return current
450
+ // uni-app H5: Vue 实例上有 click 事件监听
451
+ if (!firstClickable && current.__vue__) {
452
+ var vm = current.__vue__
453
+ if ((vm.$listeners && vm.$listeners.click) || (vm._events && vm._events.click)) {
454
+ firstClickable = current
455
+ }
456
+ }
457
+ // 有 onclick 属性
458
+ if (!firstClickable && current.onclick) {
459
+ firstClickable = current
460
+ }
461
+ current = current.parentElement
462
+ depth++
463
+ }
464
+
465
+ return firstClickable || el
466
+ }
467
+
468
+ // ---- 清理 ----
469
+
470
+ destroy() {
471
+ this._listeners.forEach(({ target, event, handler, capture }) => {
472
+ target.removeEventListener(event, handler, !!capture)
473
+ })
474
+ this._listeners = []
475
+
476
+ if (this._originalPushState) {
477
+ history.pushState = this._originalPushState
478
+ }
479
+ if (this._originalReplaceState) {
480
+ history.replaceState = this._originalReplaceState
481
+ }
482
+ }
483
+ }