20261104 #17

Merged
zzh merged 5 commits from test into master 2026-11-04 17:36:08 +08:00
7 changed files with 134 additions and 4 deletions
Showing only changes of commit 92cf28b73b - Show all commits

View File

@ -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_WECOM_KF_URL=https://work.weixin.qq.com/kfid/kfcae09c206f6c7045a
# 测试站可选:填同一 GA4 或单独测试媒体资源;空则不加载
# VITE_GA_MEASUREMENT_ID=

View File

@ -11,3 +11,7 @@ VITE_API_ASSET_ORIGIN=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
# Google Analytics 4(官网 https://ideship.com)
# 地域/设备/来源自动有;首页点击见事件 home_*
VITE_GA_MEASUREMENT_ID=G-1XWZ7LL1PW

4
src/env.d.ts vendored
View File

@ -53,4 +53,8 @@ interface ImportMetaEnv {
readonly VITE_API_ORIGIN?: string;
/** 静态资源 origin;本地默认测试 admin */
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;
}

View File

@ -19,6 +19,7 @@ import {
import { initAuthKeepAlive } from '@/utils/auth-keepalive';
import { installWeChatH5HistoryGuard } from '@/utils/device';
import { installChunkReloadHandlers, reloadOnChunkLoadError } from '@/utils/chunkReload';
import { initGa } from '@/utils/ga';
import { setAuthToken } from './api/rbac';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import { applyRbacUserToUserStore } from '@/utils/enter-system';
@ -138,6 +139,8 @@ async function bootstrap() {
;(window as any).__APP_MOUNTED__ = true;
installWeChatH5HistoryGuard(router);
initAuthKeepAlive();
// GA4:有 VITE_GA_MEASUREMENT_ID 才加载;地域/设备等自动采集
initGa();
// 登录页勿预热 VXE/Element:体积大且会注入全局 CSS,和首屏抢带宽
if (isHomePath(entryPath) && !isH5LikePath(entryPath) && !light) {

View File

@ -10,7 +10,12 @@ function setupPageGuard(router: Router) {
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();
});
}

80
src/utils/ga.ts Normal file
View 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 });
}

View File

@ -76,7 +76,7 @@
:active-thread-id="activeThreadId"
:routes-pending="routesPending"
@submit="onSubmit"
@intent="advisor.askIntent"
@intent="onIntent"
@human="onNav('contact')"
@wechat="openWecom"
@prefill="draft = $event"
@ -150,6 +150,7 @@
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
import { provideSiteLiquidGlass } from './lib/site-liquid-glass';
import { useSiteAdvisor } from './useSiteAdvisor';
import { trackSiteEvent } from '@/utils/ga';
import './styles/vendor.css';
import './styles/icons.css';
import './styles/main.css';
@ -181,9 +182,17 @@
function onOfficialLoginCapture(e: MouseEvent) {
const target = e.target as Element | 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') || '';
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);
}
@ -292,14 +301,22 @@
panel.value = id;
nav.value = id;
syncHash(id);
trackSiteEvent('home_panel_open', { panel_id: id });
}
function openRoutes(dest?: string) {
const destId = dest || 'all';
advisor.clearRoutesPending();
routesDest.value = dest || 'all';
routesDest.value = destId;
trackSiteEvent('home_dest_click', { dest_id: destId });
openPanel('routes');
}
function onIntent(intentId: string) {
trackSiteEvent('home_intent_click', { intent_id: intentId });
advisor.askIntent(intentId);
}
function closePanel(id?: string) {
if (id && panel.value && panel.value !== id) return;
if (!panel.value) return;
@ -314,6 +331,7 @@
historyOpen.value = false;
nav.value = 'home';
syncHash('home');
trackSiteEvent('home_nav', { nav_id: 'home' });
advisor.startNewChat();
}
@ -323,6 +341,7 @@
onHome();
return;
}
trackSiteEvent('home_nav', { nav_id: id });
if (NAV_PANELS.includes(id as PanelId)) {
openPanel(id as Exclude<PanelId, null>);
return;
@ -336,6 +355,10 @@
const fromRoutes = panel.value === 'routes';
closePanel();
draft.value = '';
trackSiteEvent('home_ask', {
intent_id: payload.intentId,
from_routes: fromRoutes,
});
advisor.ask(payload.text, { intentId: payload.intentId, fromRoutes });
}
@ -343,11 +366,16 @@
const text = draft.value.trim();
if (!text) return;
draft.value = '';
trackSiteEvent('home_ask', { intent_id: 'free_text' });
advisor.ask(text);
}
function onChoice(payload: { id: string; label: string; flowId: string }) {
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 });
}
@ -357,6 +385,7 @@
}
function openWecom() {
trackSiteEvent('home_wecom_click', { entry: 'advisor' });
if (shouldOpenWecomDirect()) {
window.location.href = WECOM_KF_URL;
return;
@ -365,6 +394,7 @@
}
function onTopicWechat() {
trackSiteEvent('home_wecom_click', { entry: 'topic' });
if (shouldOpenWecomDirect()) {
window.location.href = WECOM_KF_URL;
return;
@ -376,6 +406,7 @@
const next = lang.value === 'zh' ? 'en' : 'zh';
lang.value = next;
localStorage.setItem('ide-language', next);
trackSiteEvent('home_lang_toggle', { lang: next });
}
function onKeydown(e: KeyboardEvent) {