@rxdrag/website-lib-react 0.0.3 → 0.0.6
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.
- package/dist/ReactModalTrigger-9207e763.js +26 -0
- package/dist/ReactModalTrigger-9207e763.js.map +1 -0
- package/dist/components/RichTextOutline/parseOutline.d.ts +5 -0
- package/dist/components/all.d.ts +0 -21
- package/dist/components/index.d.ts +0 -5
- package/dist/forms.d.ts +1 -0
- package/dist/forms.mjs +1647 -0
- package/dist/forms.mjs.map +1 -0
- package/dist/index.mjs +9 -3918
- package/dist/index.mjs.map +1 -1
- package/dist/jsx-runtime-c02cc059.js +325 -0
- package/dist/jsx-runtime-c02cc059.js.map +1 -0
- package/dist/media.d.ts +1 -0
- package/dist/media.mjs +613 -0
- package/dist/media.mjs.map +1 -0
- package/dist/richtext.d.ts +1 -0
- package/dist/richtext.mjs +191 -0
- package/dist/richtext.mjs.map +1 -0
- package/dist/ui.d.ts +10 -0
- package/dist/ui.mjs +687 -0
- package/dist/ui.mjs.map +1 -0
- package/dist/video.d.ts +2 -0
- package/dist/video.mjs +426 -0
- package/dist/video.mjs.map +1 -0
- package/forms.ts +1 -0
- package/index.ts +1 -0
- package/media.ts +1 -0
- package/package.json +41 -6
- package/richtext.ts +1 -0
- package/src/components/Analytics/eventHandlers.ts +173 -0
- package/src/components/Analytics/index.tsx +21 -0
- package/src/components/Analytics/singleton.ts +214 -0
- package/src/components/Analytics/tracking.ts +221 -0
- package/src/components/Analytics/types.ts +60 -0
- package/src/components/Analytics/utils.ts +95 -0
- package/src/components/AttachmentIcon/index.tsx +53 -0
- package/src/components/BackgroundHlsVideoPlayer.tsx +97 -0
- package/src/components/BackgroundVideoPlayer.tsx +32 -0
- package/src/components/Bulletin.tsx +30 -0
- package/src/components/ContactForm/ContactForm.tsx +290 -0
- package/src/components/ContactForm/FileUpload2.tsx +423 -0
- package/src/components/ContactForm/Input.tsx +48 -0
- package/src/components/ContactForm/Input2.tsx +59 -0
- package/src/components/ContactForm/Submit.tsx +48 -0
- package/src/components/ContactForm/TelInput.tsx +215 -0
- package/src/components/ContactForm/TelInput2.tsx +213 -0
- package/src/components/ContactForm/Textarea.tsx +48 -0
- package/src/components/ContactForm/Textarea2.tsx +89 -0
- package/src/components/ContactForm/countryDialCodes.ts +243 -0
- package/src/components/ContactForm/factory.tsx +60 -0
- package/src/components/ContactForm/funcs.ts +64 -0
- package/src/components/ContactForm/hooks/useInlineLabelPadding.ts +43 -0
- package/src/components/ContactForm/hooks/useTelControl.ts +81 -0
- package/src/components/ContactForm/index.ts +7 -0
- package/src/components/ContactForm/types.ts +68 -0
- package/src/components/Icon/index.tsx +20 -0
- package/src/components/Medias/MainMedia.tsx +257 -0
- package/src/components/Medias/Thumbnail.tsx +62 -0
- package/src/components/Medias/VideoPlayer.tsx +114 -0
- package/src/components/Medias/index.tsx +271 -0
- package/src/components/ProductCard/ProductCard.tsx +24 -0
- package/src/components/ProductCard/ProductCta/index.tsx +28 -0
- package/src/components/ProductCard/ProductCta/style.css +4 -0
- package/src/components/ProductCard/ProductDescription/index.tsx +13 -0
- package/src/components/ProductCard/ProductDescription/style.css +6 -0
- package/src/components/ProductCard/ProductMedia/index.tsx +35 -0
- package/src/components/ProductCard/ProductMedia/style.css +6 -0
- package/src/components/ProductCard/ProductTitle/index.tsx +7 -0
- package/src/components/ProductCard/ProductTitle/style.css +4 -0
- package/src/components/ProductCard/ProductView.tsx +36 -0
- package/src/components/ProductCard/index.ts +5 -0
- package/src/components/ProductCard/useQueryProduct.ts +32 -0
- package/src/components/ReactModalTrigger.tsx +28 -0
- package/src/components/ReactVideoPlayer.tsx +332 -0
- package/src/components/RichTextOutline/index.tsx +74 -0
- package/src/components/RichTextOutline/parseOutline.ts +63 -0
- package/src/components/RichTextOutline/useAcitviedHeading.ts +142 -0
- package/src/components/RichTextOutline/useAnchorScroll.ts +24 -0
- package/src/components/Scroller.tsx +39 -0
- package/src/components/SearchInput.tsx +21 -0
- package/src/components/Share/index.tsx +86 -0
- package/src/components/Share/socials.tsx +80 -0
- package/src/components/Share//350/265/204/346/226/231.md +7 -0
- package/src/components/ToTop.tsx +72 -0
- package/src/components/VideoPlayIcon.tsx +43 -0
- package/src/components/all.ts +25 -0
- package/src/components/index.ts +12 -0
- package/src/forms.ts +1 -0
- package/src/index.ts +1 -0
- package/src/media.ts +1 -0
- package/src/richtext.ts +1 -0
- package/src/types/view-model.ts +37 -0
- package/src/ui.ts +10 -0
- package/src/video.ts +2 -0
- package/ui.ts +1 -0
- package/video.ts +1 -0
- package/dist/style.css +0 -17
package/package.json
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rxdrag/website-lib-react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"main": "dist/index.mjs",
|
|
5
6
|
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./ui": {
|
|
14
|
+
"types": "./src/ui.ts",
|
|
15
|
+
"default": "./ui.ts"
|
|
16
|
+
},
|
|
17
|
+
"./forms": {
|
|
18
|
+
"types": "./src/forms.ts",
|
|
19
|
+
"default": "./forms.ts"
|
|
20
|
+
},
|
|
21
|
+
"./media": {
|
|
22
|
+
"types": "./src/media.ts",
|
|
23
|
+
"default": "./media.ts"
|
|
24
|
+
},
|
|
25
|
+
"./richtext": {
|
|
26
|
+
"types": "./src/richtext.ts",
|
|
27
|
+
"default": "./richtext.ts"
|
|
28
|
+
},
|
|
29
|
+
"./video": {
|
|
30
|
+
"types": "./src/video.ts",
|
|
31
|
+
"default": "./video.ts"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
6
34
|
"files": [
|
|
7
|
-
"dist"
|
|
35
|
+
"dist",
|
|
36
|
+
"index.ts",
|
|
37
|
+
"ui.ts",
|
|
38
|
+
"forms.ts",
|
|
39
|
+
"media.ts",
|
|
40
|
+
"richtext.ts",
|
|
41
|
+
"video.ts",
|
|
42
|
+
"src"
|
|
8
43
|
],
|
|
9
44
|
"keywords": [
|
|
10
45
|
"react-component"
|
|
@@ -17,15 +52,15 @@
|
|
|
17
52
|
"@types/react-dom": "^19.1.0",
|
|
18
53
|
"eslint": "^9.39.2",
|
|
19
54
|
"typescript": "^5",
|
|
20
|
-
"@rxdrag/
|
|
21
|
-
"@rxdrag/
|
|
55
|
+
"@rxdrag/eslint-config-custom": "0.2.13",
|
|
56
|
+
"@rxdrag/tsconfig": "0.2.1"
|
|
22
57
|
},
|
|
23
58
|
"dependencies": {
|
|
24
59
|
"@iconify/react": "^5.0.2",
|
|
25
60
|
"clsx": "^2.1.0",
|
|
26
61
|
"hls.js": "^1.6.13",
|
|
27
|
-
"@rxdrag/
|
|
28
|
-
"@rxdrag/
|
|
62
|
+
"@rxdrag/rxcms-models": "0.3.115",
|
|
63
|
+
"@rxdrag/tiptap-preview": "0.0.3"
|
|
29
64
|
},
|
|
30
65
|
"peerDependencies": {
|
|
31
66
|
"react": "^18.0.0 || ^19.0.0",
|
package/richtext.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/richtext";
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Analytics 事件处理函数
|
|
2
|
+
import type { AnalyticsState, PageData } from './types';
|
|
3
|
+
import { trackPageEvent, trackPageVisit } from './tracking';
|
|
4
|
+
|
|
5
|
+
// 页面离开事件处理
|
|
6
|
+
export function createPageLeaveHandler(
|
|
7
|
+
state: AnalyticsState,
|
|
8
|
+
pageData: PageData,
|
|
9
|
+
apiEndpoint: string,
|
|
10
|
+
debugLog: (...args: unknown[]) => void
|
|
11
|
+
) {
|
|
12
|
+
return function handlePageLeave(eventSource?: string): void {
|
|
13
|
+
// 检查当前页面是否已经被跟踪离开
|
|
14
|
+
if (state.pageLeaveTracked) {
|
|
15
|
+
console.log(`Page leave already tracked, skipping (source: ${eventSource || 'unknown'})`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 检查是否有页面访问记录,如果从来没有跟踪过任何页面则不发送离开事件
|
|
20
|
+
if (state.lastTrackTime === 0) {
|
|
21
|
+
console.log(`No page visit ever tracked, skipping page leave (source: ${eventSource || 'unknown'})`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`Page leaving, tracking pageLeave event (source: ${eventSource || 'unknown'})`);
|
|
26
|
+
state.pageLeaveTracked = true;
|
|
27
|
+
trackPageEvent("pageLeave", pageData, state, apiEndpoint, debugLog);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Astro before-swap 事件处理
|
|
32
|
+
export function createAstroBeforeSwapHandler(
|
|
33
|
+
handlePageLeave: (eventSource?: string) => void
|
|
34
|
+
) {
|
|
35
|
+
return function handleAstroBeforeSwap(): void {
|
|
36
|
+
console.log("astro:before-swap triggered - page leaving");
|
|
37
|
+
handlePageLeave('astro:before-swap');
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Astro after-swap 事件处理
|
|
42
|
+
export function createAstroAfterSwapHandler(
|
|
43
|
+
state: AnalyticsState,
|
|
44
|
+
pageData: PageData,
|
|
45
|
+
apiEndpoint: string,
|
|
46
|
+
debugLog: (...args: unknown[]) => void
|
|
47
|
+
) {
|
|
48
|
+
return function handleAstroAfterSwap(): void {
|
|
49
|
+
console.log("astro:after-swap triggered");
|
|
50
|
+
state.isAstroEnvironment = true;
|
|
51
|
+
|
|
52
|
+
// 直接处理页面切换,不依赖 resetPageTracking
|
|
53
|
+
const newUrl = window.location.href;
|
|
54
|
+
if (newUrl !== state.currentPageUrl) {
|
|
55
|
+
console.log(
|
|
56
|
+
"URL changed in astro:after-swap from",
|
|
57
|
+
state.currentPageUrl,
|
|
58
|
+
"to",
|
|
59
|
+
newUrl
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// 更新 referrer:页面切换时,referrer 是上一个页面
|
|
63
|
+
state.currentReferrer = state.currentPageUrl;
|
|
64
|
+
debugLog("Updated referrer to:", state.currentReferrer);
|
|
65
|
+
|
|
66
|
+
// 更新页面数据
|
|
67
|
+
// 更新页面状态
|
|
68
|
+
state.currentPageUrl = newUrl;
|
|
69
|
+
pageData.url = newUrl;
|
|
70
|
+
pageData.path = window.location.pathname;
|
|
71
|
+
pageData.start_time = Date.now();
|
|
72
|
+
state.pageLoadTime = Date.now();
|
|
73
|
+
state.isPageTracked = false; // 重置跟踪状态
|
|
74
|
+
// 注意:不重置 pageLeaveTracked,让它保持当前状态,避免影响离开事件
|
|
75
|
+
// 重置活跃时间追踪
|
|
76
|
+
state.activeTime = 0;
|
|
77
|
+
state.lastActiveTime = Date.now();
|
|
78
|
+
state.isPageVisible = !document.hidden;
|
|
79
|
+
|
|
80
|
+
// 直接发送统计
|
|
81
|
+
trackPageVisit(pageData, state, apiEndpoint, debugLog);
|
|
82
|
+
|
|
83
|
+
// 新页面开始跟踪后,重置页面离开状态,为下次离开做准备
|
|
84
|
+
state.pageLeaveTracked = false;
|
|
85
|
+
|
|
86
|
+
// 标记初始加载已处理,防止后续的初始加载事件重复处理
|
|
87
|
+
state.initialLoadHandled = true;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 初始加载事件处理
|
|
93
|
+
export function createInitialLoadHandler(
|
|
94
|
+
state: AnalyticsState,
|
|
95
|
+
pageData: PageData,
|
|
96
|
+
apiEndpoint: string,
|
|
97
|
+
debugLog: (...args: unknown[]) => void
|
|
98
|
+
) {
|
|
99
|
+
return function handleInitialLoad(eventType: string): void {
|
|
100
|
+
console.log(
|
|
101
|
+
eventType + " triggered, isAstroEnvironment:",
|
|
102
|
+
state.isAstroEnvironment,
|
|
103
|
+
"initialLoadHandled:",
|
|
104
|
+
state.initialLoadHandled,
|
|
105
|
+
"isPageTracked:",
|
|
106
|
+
state.isPageTracked
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 如果初始加载已经处理过,跳过(可能是 astro:after-swap 已经处理了)
|
|
110
|
+
if (state.initialLoadHandled) {
|
|
111
|
+
console.log(`Skipping initial load - already handled (${eventType})`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 如果页面已经被跟踪(可能是 astro:after-swap 已经处理了),跳过
|
|
116
|
+
if (state.isPageTracked) {
|
|
117
|
+
console.log(`Skipping initial load - page already tracked (${eventType})`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 页面刷新或直接访问时,确保统计
|
|
122
|
+
state.initialLoadHandled = true;
|
|
123
|
+
console.log("Handling initial page load via", eventType);
|
|
124
|
+
trackPageVisit(pageData, state, apiEndpoint, debugLog);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 页面可见性变化事件处理
|
|
129
|
+
export function createVisibilityChangeHandler(state: AnalyticsState) {
|
|
130
|
+
return function handleVisibilityChange(): void {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
|
|
133
|
+
if (document.visibilityState === "visible") {
|
|
134
|
+
// 页面变为可见
|
|
135
|
+
console.log("Page became visible");
|
|
136
|
+
state.isPageVisible = true;
|
|
137
|
+
state.lastActiveTime = now;
|
|
138
|
+
} else {
|
|
139
|
+
// 页面变为隐藏
|
|
140
|
+
console.log("Page became hidden");
|
|
141
|
+
if (state.isPageVisible) {
|
|
142
|
+
// 更新活跃时间到隐藏前
|
|
143
|
+
state.activeTime += now - state.lastActiveTime;
|
|
144
|
+
console.log("Updated active time:", state.activeTime, "ms");
|
|
145
|
+
}
|
|
146
|
+
state.isPageVisible = false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 用户交互事件处理
|
|
152
|
+
export function createUserInteractionHandler(state: AnalyticsState) {
|
|
153
|
+
return function handleUserInteraction(): void {
|
|
154
|
+
if (!state.isPageVisible || state.interactionThrottled) return;
|
|
155
|
+
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
// 节流:最多每秒检查一次
|
|
158
|
+
if (now - state.lastInteractionTime < 1000) return;
|
|
159
|
+
|
|
160
|
+
state.lastInteractionTime = now;
|
|
161
|
+
state.interactionThrottled = true;
|
|
162
|
+
|
|
163
|
+
// 如果超过30秒没有交互,重置活跃时间计算
|
|
164
|
+
if (now - state.lastActiveTime > 30000) {
|
|
165
|
+
state.lastActiveTime = now;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 解除节流
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
state.interactionThrottled = false;
|
|
171
|
+
}, 1000);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import type { AnalyticsProps } from './types';
|
|
3
|
+
import AnalyticsSingleton from './singleton';
|
|
4
|
+
|
|
5
|
+
export function Analytics({
|
|
6
|
+
apiEndpoint = "/api/track-visitor"
|
|
7
|
+
}: AnalyticsProps) {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
// 获取单例实例并初始化
|
|
10
|
+
const analytics = AnalyticsSingleton.getInstance(apiEndpoint);
|
|
11
|
+
analytics.init();
|
|
12
|
+
|
|
13
|
+
// 组件卸载时不清理单例,让它继续工作
|
|
14
|
+
return () => {
|
|
15
|
+
// 不做任何清理,让单例继续工作
|
|
16
|
+
};
|
|
17
|
+
}, [apiEndpoint]);
|
|
18
|
+
|
|
19
|
+
// 组件不渲染任何可见内容
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Analytics 单例管理器
|
|
2
|
+
import type { AnalyticsState, PageData } from './types';
|
|
3
|
+
import {
|
|
4
|
+
createDebugLogger,
|
|
5
|
+
generateId,
|
|
6
|
+
getVisitorId
|
|
7
|
+
} from './utils';
|
|
8
|
+
import { trackPageEvent } from './tracking';
|
|
9
|
+
|
|
10
|
+
class AnalyticsSingleton {
|
|
11
|
+
private static instance: AnalyticsSingleton | null = null;
|
|
12
|
+
private initialized = false;
|
|
13
|
+
private state: AnalyticsState;
|
|
14
|
+
private pageData: PageData;
|
|
15
|
+
private apiEndpoint: string;
|
|
16
|
+
private debugLog: (...args: any[]) => void;
|
|
17
|
+
private eventHandlers: Array<() => void> = [];
|
|
18
|
+
|
|
19
|
+
private constructor(apiEndpoint: string) {
|
|
20
|
+
this.apiEndpoint = apiEndpoint;
|
|
21
|
+
this.debugLog = createDebugLogger();
|
|
22
|
+
|
|
23
|
+
// 初始化访客和会话信息
|
|
24
|
+
const visitorId = getVisitorId();
|
|
25
|
+
let sessionId = sessionStorage.getItem('session_id');
|
|
26
|
+
if (!sessionId) {
|
|
27
|
+
sessionId = generateId('s');
|
|
28
|
+
sessionStorage.setItem('session_id', sessionId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 页面信息
|
|
32
|
+
this.pageData = {
|
|
33
|
+
visitor_id: visitorId,
|
|
34
|
+
session_id: sessionId,
|
|
35
|
+
url: window.location.href,
|
|
36
|
+
path: window.location.pathname,
|
|
37
|
+
referrer: document.referrer || null,
|
|
38
|
+
user_agent: navigator.userAgent,
|
|
39
|
+
start_time: Date.now(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// 状态管理
|
|
43
|
+
this.state = {
|
|
44
|
+
isPageTracked: false,
|
|
45
|
+
currentPageUrl: window.location.href,
|
|
46
|
+
pageLoadTime: Date.now(),
|
|
47
|
+
lastTrackTime: 0,
|
|
48
|
+
currentReferrer: document.referrer || null,
|
|
49
|
+
activeTime: 0,
|
|
50
|
+
lastActiveTime: Date.now(),
|
|
51
|
+
isPageVisible: !document.hidden,
|
|
52
|
+
requestQueue: [],
|
|
53
|
+
isRequestPending: false,
|
|
54
|
+
isAstroEnvironment: false,
|
|
55
|
+
pageLeaveTracked: false,
|
|
56
|
+
initialLoadHandled: false,
|
|
57
|
+
interactionThrottled: false,
|
|
58
|
+
lastInteractionTime: 0,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static getInstance(apiEndpoint = "/api/track-visitor"): AnalyticsSingleton {
|
|
64
|
+
if (!AnalyticsSingleton.instance) {
|
|
65
|
+
AnalyticsSingleton.instance = new AnalyticsSingleton(apiEndpoint);
|
|
66
|
+
}
|
|
67
|
+
return AnalyticsSingleton.instance;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static reset(): void {
|
|
71
|
+
if (AnalyticsSingleton.instance) {
|
|
72
|
+
AnalyticsSingleton.instance.cleanup();
|
|
73
|
+
AnalyticsSingleton.instance = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
init(): void {
|
|
78
|
+
if (this.initialized) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.initialized = true;
|
|
83
|
+
this.setupEventListeners();
|
|
84
|
+
this.handleInitialPageLoad();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private setupEventListeners(): void {
|
|
88
|
+
// 页面离开事件
|
|
89
|
+
const handlePageLeave = () => {
|
|
90
|
+
if (this.state.pageLeaveTracked || this.state.lastTrackTime === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.state.pageLeaveTracked = true;
|
|
95
|
+
trackPageEvent("pageLeave", this.pageData, this.state, this.apiEndpoint, this.debugLog);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// 页面离开事件监听器
|
|
99
|
+
const handleBeforeUnload = () => handlePageLeave();
|
|
100
|
+
const handlePageHide = () => handlePageLeave();
|
|
101
|
+
|
|
102
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
103
|
+
window.addEventListener('pagehide', handlePageHide);
|
|
104
|
+
|
|
105
|
+
// Astro 事件监听
|
|
106
|
+
const handleAstroBeforeSwap = () => {
|
|
107
|
+
handlePageLeave();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleAstroAfterSwap = () => {
|
|
111
|
+
this.state.isAstroEnvironment = true;
|
|
112
|
+
|
|
113
|
+
const newUrl = window.location.href;
|
|
114
|
+
if (newUrl !== this.state.currentPageUrl) {
|
|
115
|
+
// 更新 referrer:页面切换时,referrer 是上一个页面
|
|
116
|
+
this.state.currentReferrer = this.state.currentPageUrl;
|
|
117
|
+
|
|
118
|
+
// 更新页面状态
|
|
119
|
+
this.state.currentPageUrl = newUrl;
|
|
120
|
+
this.pageData.url = newUrl;
|
|
121
|
+
this.pageData.path = window.location.pathname;
|
|
122
|
+
this.pageData.start_time = Date.now();
|
|
123
|
+
this.state.pageLoadTime = Date.now();
|
|
124
|
+
this.state.isPageTracked = false;
|
|
125
|
+
this.state.pageLeaveTracked = false;
|
|
126
|
+
this.state.activeTime = 0;
|
|
127
|
+
this.state.lastActiveTime = Date.now();
|
|
128
|
+
this.state.isPageVisible = !document.hidden;
|
|
129
|
+
|
|
130
|
+
// 发送新页面统计
|
|
131
|
+
this.trackPageVisit();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
document.addEventListener('astro:before-swap', handleAstroBeforeSwap);
|
|
136
|
+
document.addEventListener('astro:after-swap', handleAstroAfterSwap);
|
|
137
|
+
|
|
138
|
+
// 页面可见性变化
|
|
139
|
+
const handleVisibilityChange = () => {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
if (document.visibilityState === 'visible') {
|
|
142
|
+
this.state.isPageVisible = true;
|
|
143
|
+
this.state.lastActiveTime = now;
|
|
144
|
+
} else {
|
|
145
|
+
if (this.state.isPageVisible) {
|
|
146
|
+
this.state.activeTime += now - this.state.lastActiveTime;
|
|
147
|
+
}
|
|
148
|
+
this.state.isPageVisible = false;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
153
|
+
|
|
154
|
+
// 用户交互事件
|
|
155
|
+
const handleUserInteraction = () => {
|
|
156
|
+
if (!this.state.isPageVisible || this.state.interactionThrottled) return;
|
|
157
|
+
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
if (now - this.state.lastInteractionTime < 1000) return;
|
|
160
|
+
|
|
161
|
+
this.state.lastInteractionTime = now;
|
|
162
|
+
this.state.interactionThrottled = true;
|
|
163
|
+
|
|
164
|
+
if (now - this.state.lastActiveTime > 30000) {
|
|
165
|
+
this.state.lastActiveTime = now;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
this.state.interactionThrottled = false;
|
|
170
|
+
}, 1000);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const interactionEvents = ['click', 'keydown', 'scroll'];
|
|
174
|
+
interactionEvents.forEach((event) => {
|
|
175
|
+
document.addEventListener(event, handleUserInteraction, {
|
|
176
|
+
passive: true,
|
|
177
|
+
capture: false,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 保存事件处理器引用,用于清理
|
|
182
|
+
this.eventHandlers = [
|
|
183
|
+
() => window.removeEventListener('beforeunload', handleBeforeUnload),
|
|
184
|
+
() => window.removeEventListener('pagehide', handlePageHide),
|
|
185
|
+
() => document.removeEventListener('astro:before-swap', handleAstroBeforeSwap),
|
|
186
|
+
() => document.removeEventListener('astro:after-swap', handleAstroAfterSwap),
|
|
187
|
+
() => document.removeEventListener('visibilitychange', handleVisibilityChange),
|
|
188
|
+
...interactionEvents.map(event =>
|
|
189
|
+
() => document.removeEventListener(event, handleUserInteraction)
|
|
190
|
+
)
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private handleInitialPageLoad(): void {
|
|
195
|
+
if (this.state.isPageTracked || this.state.initialLoadHandled) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.state.initialLoadHandled = true;
|
|
200
|
+
this.trackPageVisit();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private trackPageVisit(): void {
|
|
204
|
+
trackPageEvent("pageView", this.pageData, this.state, this.apiEndpoint, this.debugLog);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
cleanup(): void {
|
|
208
|
+
this.eventHandlers.forEach(cleanup => cleanup());
|
|
209
|
+
this.eventHandlers = [];
|
|
210
|
+
this.initialized = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default AnalyticsSingleton;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Analytics 跟踪相关函数
|
|
2
|
+
import type { AnalyticsState, PageData, TrackingData } from './types';
|
|
3
|
+
import {
|
|
4
|
+
getDeviceInfo,
|
|
5
|
+
getPlatform,
|
|
6
|
+
getBrowserLanguage,
|
|
7
|
+
detectPageType,
|
|
8
|
+
getCookie
|
|
9
|
+
} from './utils';
|
|
10
|
+
|
|
11
|
+
// 更新活跃时间
|
|
12
|
+
export function updateActiveTime(state: AnalyticsState): void {
|
|
13
|
+
if (state.isPageVisible) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
state.activeTime += now - state.lastActiveTime;
|
|
16
|
+
state.lastActiveTime = now;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 重置页面跟踪(用于页面切换)
|
|
21
|
+
export function resetPageTracking(state: AnalyticsState, pageData: PageData): void {
|
|
22
|
+
const newUrl = window.location.href;
|
|
23
|
+
// 只有当 URL 真正改变时才重置
|
|
24
|
+
if (newUrl !== state.currentPageUrl) {
|
|
25
|
+
state.currentPageUrl = newUrl;
|
|
26
|
+
pageData.start_time = Date.now();
|
|
27
|
+
state.pageLoadTime = Date.now();
|
|
28
|
+
state.isPageTracked = false;
|
|
29
|
+
// 重置活跃时间追踪
|
|
30
|
+
state.activeTime = 0;
|
|
31
|
+
state.lastActiveTime = Date.now();
|
|
32
|
+
state.isPageVisible = !document.hidden;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 检查是否为真正的页面访问(简化版)
|
|
37
|
+
export function isRealPageVisit(state: AnalyticsState): boolean {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
|
|
40
|
+
// 如果页面已经被跟踪过,则不是新访问
|
|
41
|
+
if (state.isPageTracked) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 检查是否在短时间内重复跟踪(全局防重复)
|
|
46
|
+
if (state.lastTrackTime > 0 && now - state.lastTrackTime < 1000) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 检查页面是否可见(避免在后台标签页中统计)
|
|
51
|
+
if (document.hidden || document.visibilityState === "hidden") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 处理队列中的下一个请求
|
|
59
|
+
export function processQueue(state: AnalyticsState, apiEndpoint: string): void {
|
|
60
|
+
state.isRequestPending = false;
|
|
61
|
+
|
|
62
|
+
if (state.requestQueue.length > 0) {
|
|
63
|
+
const { data, eventType } = state.requestQueue.shift()!;
|
|
64
|
+
// 小延迟避免请求过于频繁
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
sendImmediately(data, eventType, state, apiEndpoint);
|
|
67
|
+
}, 100);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 立即发送数据
|
|
72
|
+
export function sendImmediately(
|
|
73
|
+
data: TrackingData,
|
|
74
|
+
eventType: string,
|
|
75
|
+
state: AnalyticsState,
|
|
76
|
+
apiEndpoint: string
|
|
77
|
+
): void {
|
|
78
|
+
state.isRequestPending = true;
|
|
79
|
+
|
|
80
|
+
fetch(apiEndpoint, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(data),
|
|
87
|
+
keepalive: true,
|
|
88
|
+
})
|
|
89
|
+
.then(() => {
|
|
90
|
+
if (eventType === "pageView") {
|
|
91
|
+
state.isPageTracked = true;
|
|
92
|
+
state.lastTrackTime = Date.now();
|
|
93
|
+
|
|
94
|
+
// 更新全局状态(用于跨组件实例共享)
|
|
95
|
+
if (typeof window !== 'undefined') {
|
|
96
|
+
(window as any).__analyticsGlobalState = {
|
|
97
|
+
lastPageUrl: window.location.href,
|
|
98
|
+
lastTrackTime: Date.now(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 处理队列中的下一个请求
|
|
104
|
+
processQueue(state, apiEndpoint);
|
|
105
|
+
})
|
|
106
|
+
.catch(() => {
|
|
107
|
+
processQueue(state, apiEndpoint);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 优化的网络请求函数
|
|
112
|
+
export function sendTrackingData(
|
|
113
|
+
data: TrackingData,
|
|
114
|
+
eventType: string,
|
|
115
|
+
state: AnalyticsState,
|
|
116
|
+
apiEndpoint: string
|
|
117
|
+
): void {
|
|
118
|
+
// 对于 pageLeave 事件,立即发送(用户可能马上离开)
|
|
119
|
+
if (eventType === "pageLeave") {
|
|
120
|
+
sendImmediately(data, eventType, state, apiEndpoint);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 对于 pageView 事件,可以稍微延迟或批量处理
|
|
125
|
+
if (state.isRequestPending) {
|
|
126
|
+
// 如果有请求正在进行,加入队列
|
|
127
|
+
state.requestQueue.push({ data, eventType });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
sendImmediately(data, eventType, state, apiEndpoint);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 创建跟踪数据
|
|
135
|
+
export function createTrackingData(
|
|
136
|
+
pageData: PageData,
|
|
137
|
+
state: AnalyticsState,
|
|
138
|
+
eventType: string,
|
|
139
|
+
effectiveDuration: number
|
|
140
|
+
): TrackingData {
|
|
141
|
+
return {
|
|
142
|
+
visitor_id: pageData.visitor_id,
|
|
143
|
+
session_id: pageData.session_id,
|
|
144
|
+
url: window.location.href,
|
|
145
|
+
path: window.location.pathname,
|
|
146
|
+
referrer: state.currentReferrer,
|
|
147
|
+
user_agent: pageData.user_agent,
|
|
148
|
+
duration_ms: effectiveDuration, // 使用活跃时间
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
type: eventType, // 添加事件类型
|
|
151
|
+
// 设备和平台信息
|
|
152
|
+
device: getDeviceInfo(),
|
|
153
|
+
platform: getPlatform(),
|
|
154
|
+
browser_language: getBrowserLanguage(),
|
|
155
|
+
// 页面信息
|
|
156
|
+
page_title: document.title || null,
|
|
157
|
+
// 屏幕信息
|
|
158
|
+
screen_width: screen.width,
|
|
159
|
+
screen_height: screen.height,
|
|
160
|
+
viewport_width: window.innerWidth,
|
|
161
|
+
viewport_height: window.innerHeight,
|
|
162
|
+
// 时区信息
|
|
163
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
164
|
+
// 是否新用户(简单判断:第一次访问该域名)
|
|
165
|
+
is_new_user:
|
|
166
|
+
!getCookie("visitor_id") ||
|
|
167
|
+
getCookie("visitor_id") === pageData.visitor_id,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 发送访客数据(支持不同类型)
|
|
172
|
+
export function trackPageEvent(
|
|
173
|
+
eventType: string,
|
|
174
|
+
pageData: PageData,
|
|
175
|
+
state: AnalyticsState,
|
|
176
|
+
apiEndpoint: string,
|
|
177
|
+
debugLog: (...args: any[]) => void
|
|
178
|
+
): void {
|
|
179
|
+
// 对于 pageView 事件,检测页面类型
|
|
180
|
+
if (eventType === "pageView") {
|
|
181
|
+
eventType = detectPageType(eventType);
|
|
182
|
+
debugLog("Detected page type:", eventType);
|
|
183
|
+
|
|
184
|
+
// 检查是否为真正的页面访问
|
|
185
|
+
if (!isRealPageVisit(state)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 计算活跃时间
|
|
191
|
+
let effectiveDuration = 0;
|
|
192
|
+
if (eventType === "pageLeave") {
|
|
193
|
+
updateActiveTime(state); // 更新到当前时间
|
|
194
|
+
effectiveDuration = state.activeTime;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
debugLog("Using referrer:", state.currentReferrer, "for event:", eventType);
|
|
198
|
+
|
|
199
|
+
const data = createTrackingData(pageData, state, eventType, effectiveDuration);
|
|
200
|
+
|
|
201
|
+
// 优化的网络请求处理
|
|
202
|
+
sendTrackingData(data, eventType, state, apiEndpoint);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 兼容性函数,保持向后兼容
|
|
206
|
+
export function trackPageVisit(
|
|
207
|
+
pageData: PageData,
|
|
208
|
+
state: AnalyticsState,
|
|
209
|
+
apiEndpoint: string,
|
|
210
|
+
debugLog: (...args: any[]) => void
|
|
211
|
+
): void {
|
|
212
|
+
// 检查是否在短时间内(200ms)重复调用同一个 URL 的 pageView
|
|
213
|
+
const currentUrl = window.location.href;
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
|
|
216
|
+
if (state.isPageTracked && state.currentPageUrl === currentUrl && now - state.lastTrackTime < 200) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
trackPageEvent("pageView", pageData, state, apiEndpoint, debugLog);
|
|
221
|
+
}
|