feat: 官网接入 GA4,埋首页点击与 SPA 浏览
All checks were successful
Build and Deploy Front / deploy (push) Successful in 9m42s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 9m42s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1f55a6d3ba
commit
92cf28b73b
@ -10,3 +10,6 @@ VITE_API_ASSET_ORIGIN=https://api.test.ideship.com/admin
|
|||||||
VITE_OMS_ADMIN_URL=https://api.test.ideship.com/admin
|
VITE_OMS_ADMIN_URL=https://api.test.ideship.com/admin
|
||||||
|
|
||||||
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
||||||
|
|
||||||
|
# 测试站可选:填同一 GA4 或单独测试媒体资源;空则不加载
|
||||||
|
# VITE_GA_MEASUREMENT_ID=
|
||||||
|
|||||||
@ -11,3 +11,7 @@ VITE_API_ASSET_ORIGIN=https://api.ideship.com/admin
|
|||||||
VITE_OMS_ADMIN_URL=https://api.ideship.com/admin
|
VITE_OMS_ADMIN_URL=https://api.ideship.com/admin
|
||||||
|
|
||||||
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
VITE_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
|
||||||
|
|
||||||
|
# Google Analytics 4(官网 https://ideship.com)
|
||||||
|
# 地域/设备/来源自动有;首页点击见事件 home_*
|
||||||
|
VITE_GA_MEASUREMENT_ID=G-1XWZ7LL1PW
|
||||||
|
|||||||
4
src/env.d.ts
vendored
4
src/env.d.ts
vendored
@ -53,4 +53,8 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_API_ORIGIN?: string;
|
readonly VITE_API_ORIGIN?: string;
|
||||||
/** 静态资源 origin;本地默认测试 admin */
|
/** 静态资源 origin;本地默认测试 admin */
|
||||||
readonly VITE_API_ASSET_ORIGIN?: string;
|
readonly VITE_API_ASSET_ORIGIN?: string;
|
||||||
|
/** 企微客服链接 */
|
||||||
|
readonly VITE_WECOM_KF_URL?: string;
|
||||||
|
/** Google Analytics 4 Measurement ID,如 G-XXXXXXXX;空则不加载 */
|
||||||
|
readonly VITE_GA_MEASUREMENT_ID?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
import { initAuthKeepAlive } from '@/utils/auth-keepalive';
|
import { initAuthKeepAlive } from '@/utils/auth-keepalive';
|
||||||
import { installWeChatH5HistoryGuard } from '@/utils/device';
|
import { installWeChatH5HistoryGuard } from '@/utils/device';
|
||||||
import { installChunkReloadHandlers, reloadOnChunkLoadError } from '@/utils/chunkReload';
|
import { installChunkReloadHandlers, reloadOnChunkLoadError } from '@/utils/chunkReload';
|
||||||
|
import { initGa } from '@/utils/ga';
|
||||||
import { setAuthToken } from './api/rbac';
|
import { setAuthToken } from './api/rbac';
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
import { applyRbacUserToUserStore } from '@/utils/enter-system';
|
import { applyRbacUserToUserStore } from '@/utils/enter-system';
|
||||||
@ -138,6 +139,8 @@ async function bootstrap() {
|
|||||||
;(window as any).__APP_MOUNTED__ = true;
|
;(window as any).__APP_MOUNTED__ = true;
|
||||||
installWeChatH5HistoryGuard(router);
|
installWeChatH5HistoryGuard(router);
|
||||||
initAuthKeepAlive();
|
initAuthKeepAlive();
|
||||||
|
// GA4:有 VITE_GA_MEASUREMENT_ID 才加载;地域/设备等自动采集
|
||||||
|
initGa();
|
||||||
|
|
||||||
// 登录页勿预热 VXE/Element:体积大且会注入全局 CSS,和首屏抢带宽
|
// 登录页勿预热 VXE/Element:体积大且会注入全局 CSS,和首屏抢带宽
|
||||||
if (isHomePath(entryPath) && !isH5LikePath(entryPath) && !light) {
|
if (isHomePath(entryPath) && !isH5LikePath(entryPath) && !light) {
|
||||||
|
|||||||
@ -10,7 +10,12 @@ function setupPageGuard(router: Router) {
|
|||||||
setRouteEmitter(to);
|
setRouteEmitter(to);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach(() => {
|
router.afterEach((to) => {
|
||||||
|
// SPA 页面浏览(未配置 VITE_GA_MEASUREMENT_ID 时为空操作)
|
||||||
|
const fullPath = to.fullPath || to.path || '/';
|
||||||
|
void import('@/utils/ga').then(({ trackPageView }) => {
|
||||||
|
trackPageView(fullPath);
|
||||||
|
});
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/utils/ga.ts
Normal file
80
src/utils/ga.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Google Analytics 4(gtag)
|
||||||
|
* 通过 VITE_GA_MEASUREMENT_ID(如 G-XXXXXXXX)启用;未配置则整段空操作。
|
||||||
|
* 地域分布、设备、来源等由 GA4 自动采集,业务点击用 trackEvent / trackSiteEvent。
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
dataLayer: unknown[];
|
||||||
|
gtag?: (...args: unknown[]) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MEASUREMENT_ID = String(import.meta.env.VITE_GA_MEASUREMENT_ID || '').trim();
|
||||||
|
|
||||||
|
let booted = false;
|
||||||
|
|
||||||
|
export function isGaEnabled(): boolean {
|
||||||
|
return /^G-[A-Z0-9]+$/i.test(MEASUREMENT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGaMeasurementId(): string {
|
||||||
|
return isGaEnabled() ? MEASUREMENT_ID : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注入 gtag 脚本并 config;可重复调用,仅首次生效 */
|
||||||
|
export function initGa(): void {
|
||||||
|
if (!isGaEnabled() || typeof window === 'undefined' || booted) return;
|
||||||
|
booted = true;
|
||||||
|
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.gtag = function gtag(...args: unknown[]) {
|
||||||
|
window.dataLayer.push(args);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.gtag('js', new Date());
|
||||||
|
window.gtag('config', MEASUREMENT_ID, {
|
||||||
|
// SPA 由路由 afterEach 主动发 page_view,避免首屏重复
|
||||||
|
send_page_view: false,
|
||||||
|
anonymize_ip: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.async = true;
|
||||||
|
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(MEASUREMENT_ID)}`;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackPageView(path: string, title?: string): void {
|
||||||
|
if (!isGaEnabled() || !window.gtag) return;
|
||||||
|
const pagePath = path || '/';
|
||||||
|
window.gtag('event', 'page_view', {
|
||||||
|
page_path: pagePath,
|
||||||
|
page_title: title || document.title,
|
||||||
|
page_location: typeof window !== 'undefined' ? window.location.href : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackEvent(
|
||||||
|
name: string,
|
||||||
|
params?: Record<string, string | number | boolean | undefined | null>,
|
||||||
|
): void {
|
||||||
|
if (!isGaEnabled() || !window.gtag) return;
|
||||||
|
const cleaned: Record<string, string | number | boolean> = {};
|
||||||
|
if (params) {
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value === undefined || value === null || value === '') return;
|
||||||
|
cleaned[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.gtag('event', name, cleaned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 官网营销事件(统一带 surface,方便在探索里筛选) */
|
||||||
|
export function trackSiteEvent(
|
||||||
|
name: string,
|
||||||
|
params?: Record<string, string | number | boolean | undefined | null>,
|
||||||
|
): void {
|
||||||
|
trackEvent(name, { surface: 'official_site', ...params });
|
||||||
|
}
|
||||||
@ -76,7 +76,7 @@
|
|||||||
:active-thread-id="activeThreadId"
|
:active-thread-id="activeThreadId"
|
||||||
:routes-pending="routesPending"
|
:routes-pending="routesPending"
|
||||||
@submit="onSubmit"
|
@submit="onSubmit"
|
||||||
@intent="advisor.askIntent"
|
@intent="onIntent"
|
||||||
@human="onNav('contact')"
|
@human="onNav('contact')"
|
||||||
@wechat="openWecom"
|
@wechat="openWecom"
|
||||||
@prefill="draft = $event"
|
@prefill="draft = $event"
|
||||||
@ -150,6 +150,7 @@
|
|||||||
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
|
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
|
||||||
import { provideSiteLiquidGlass } from './lib/site-liquid-glass';
|
import { provideSiteLiquidGlass } from './lib/site-liquid-glass';
|
||||||
import { useSiteAdvisor } from './useSiteAdvisor';
|
import { useSiteAdvisor } from './useSiteAdvisor';
|
||||||
|
import { trackSiteEvent } from '@/utils/ga';
|
||||||
import './styles/vendor.css';
|
import './styles/vendor.css';
|
||||||
import './styles/icons.css';
|
import './styles/icons.css';
|
||||||
import './styles/main.css';
|
import './styles/main.css';
|
||||||
@ -181,9 +182,17 @@
|
|||||||
function onOfficialLoginCapture(e: MouseEvent) {
|
function onOfficialLoginCapture(e: MouseEvent) {
|
||||||
const target = e.target as Element | null;
|
const target = e.target as Element | null;
|
||||||
const anchor = target?.closest?.('a[href]') as HTMLAnchorElement | null;
|
const anchor = target?.closest?.('a[href]') as HTMLAnchorElement | null;
|
||||||
if (!anchor || anchor.classList.contains('site-nav-login')) return;
|
if (!anchor) return;
|
||||||
const href = anchor.getAttribute('href') || '';
|
const href = anchor.getAttribute('href') || '';
|
||||||
if (!href.includes('from=official-site')) return;
|
if (!href.includes('from=official-site')) return;
|
||||||
|
trackSiteEvent('home_login_click', {
|
||||||
|
link_role: anchor.classList.contains('site-nav-login')
|
||||||
|
? 'nav_login'
|
||||||
|
: anchor.classList.contains('ai-welcome-cta-link')
|
||||||
|
? 'hero_cta'
|
||||||
|
: 'other',
|
||||||
|
});
|
||||||
|
if (anchor.classList.contains('site-nav-login')) return;
|
||||||
goOfficialLoginFromHref(router, href, e);
|
goOfficialLoginFromHref(router, href, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,14 +301,22 @@
|
|||||||
panel.value = id;
|
panel.value = id;
|
||||||
nav.value = id;
|
nav.value = id;
|
||||||
syncHash(id);
|
syncHash(id);
|
||||||
|
trackSiteEvent('home_panel_open', { panel_id: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRoutes(dest?: string) {
|
function openRoutes(dest?: string) {
|
||||||
|
const destId = dest || 'all';
|
||||||
advisor.clearRoutesPending();
|
advisor.clearRoutesPending();
|
||||||
routesDest.value = dest || 'all';
|
routesDest.value = destId;
|
||||||
|
trackSiteEvent('home_dest_click', { dest_id: destId });
|
||||||
openPanel('routes');
|
openPanel('routes');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onIntent(intentId: string) {
|
||||||
|
trackSiteEvent('home_intent_click', { intent_id: intentId });
|
||||||
|
advisor.askIntent(intentId);
|
||||||
|
}
|
||||||
|
|
||||||
function closePanel(id?: string) {
|
function closePanel(id?: string) {
|
||||||
if (id && panel.value && panel.value !== id) return;
|
if (id && panel.value && panel.value !== id) return;
|
||||||
if (!panel.value) return;
|
if (!panel.value) return;
|
||||||
@ -314,6 +331,7 @@
|
|||||||
historyOpen.value = false;
|
historyOpen.value = false;
|
||||||
nav.value = 'home';
|
nav.value = 'home';
|
||||||
syncHash('home');
|
syncHash('home');
|
||||||
|
trackSiteEvent('home_nav', { nav_id: 'home' });
|
||||||
advisor.startNewChat();
|
advisor.startNewChat();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,6 +341,7 @@
|
|||||||
onHome();
|
onHome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
trackSiteEvent('home_nav', { nav_id: id });
|
||||||
if (NAV_PANELS.includes(id as PanelId)) {
|
if (NAV_PANELS.includes(id as PanelId)) {
|
||||||
openPanel(id as Exclude<PanelId, null>);
|
openPanel(id as Exclude<PanelId, null>);
|
||||||
return;
|
return;
|
||||||
@ -336,6 +355,10 @@
|
|||||||
const fromRoutes = panel.value === 'routes';
|
const fromRoutes = panel.value === 'routes';
|
||||||
closePanel();
|
closePanel();
|
||||||
draft.value = '';
|
draft.value = '';
|
||||||
|
trackSiteEvent('home_ask', {
|
||||||
|
intent_id: payload.intentId,
|
||||||
|
from_routes: fromRoutes,
|
||||||
|
});
|
||||||
advisor.ask(payload.text, { intentId: payload.intentId, fromRoutes });
|
advisor.ask(payload.text, { intentId: payload.intentId, fromRoutes });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,11 +366,16 @@
|
|||||||
const text = draft.value.trim();
|
const text = draft.value.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
draft.value = '';
|
draft.value = '';
|
||||||
|
trackSiteEvent('home_ask', { intent_id: 'free_text' });
|
||||||
advisor.ask(text);
|
advisor.ask(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChoice(payload: { id: string; label: string; flowId: string }) {
|
function onChoice(payload: { id: string; label: string; flowId: string }) {
|
||||||
draft.value = '';
|
draft.value = '';
|
||||||
|
trackSiteEvent('home_choice', {
|
||||||
|
choice_id: payload.id,
|
||||||
|
flow_id: payload.flowId,
|
||||||
|
});
|
||||||
advisor.ask(payload.label, { kind: 'choice', flowId: payload.flowId, choice: payload.id });
|
advisor.ask(payload.label, { kind: 'choice', flowId: payload.flowId, choice: payload.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,6 +385,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openWecom() {
|
function openWecom() {
|
||||||
|
trackSiteEvent('home_wecom_click', { entry: 'advisor' });
|
||||||
if (shouldOpenWecomDirect()) {
|
if (shouldOpenWecomDirect()) {
|
||||||
window.location.href = WECOM_KF_URL;
|
window.location.href = WECOM_KF_URL;
|
||||||
return;
|
return;
|
||||||
@ -365,6 +394,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onTopicWechat() {
|
function onTopicWechat() {
|
||||||
|
trackSiteEvent('home_wecom_click', { entry: 'topic' });
|
||||||
if (shouldOpenWecomDirect()) {
|
if (shouldOpenWecomDirect()) {
|
||||||
window.location.href = WECOM_KF_URL;
|
window.location.href = WECOM_KF_URL;
|
||||||
return;
|
return;
|
||||||
@ -376,6 +406,7 @@
|
|||||||
const next = lang.value === 'zh' ? 'en' : 'zh';
|
const next = lang.value === 'zh' ? 'en' : 'zh';
|
||||||
lang.value = next;
|
lang.value = next;
|
||||||
localStorage.setItem('ide-language', next);
|
localStorage.setItem('ide-language', next);
|
||||||
|
trackSiteEvent('home_lang_toggle', { lang: next });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeydown(e: KeyboardEvent) {
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user