Files
web_huakui_web/src/components/analytics/TrackerProvider.tsx
T
谢宇宁andClaude Opus 5.5 d2ddb1c321 [Ai] feat: 花魁短剧 Next.js 站点初始提交
- 原生 Next.js App Router + 严格 TypeScript 实现花魁短剧 PC/H5 页面,按 src/features/{catalog,content,playback} 组织,保留完整 SEO(metadata、JSON-LD、canonical、robots、llms、sitemap、IndexNow)。
- 主域名通过 SITE_URL=https://huakuidj.com 强制覆盖后端 foreverUrlAddr(config/*.env、src/server/config.ts、src/server/seo/shared.ts、src/server/services/shared.ts),支持邮箱改为 support@huakuidj.com。
- 移除登录与账户体系:删除 /account 全部页面、登录/注册/充值弹窗、收藏点赞、评论、金币解锁及对应 BFF 接口;旧账户地址 GET/HEAD 统一 301 到首页(src/lib/routes.ts REMOVED_ACCOUNT_PATHS),保留游客会话、反馈、图片上传、观看上报与搜索联想。
- 播放页:评论位改为同类推荐 + 热播榜单,锁定集改为 App 观看遮罩与底部弹层,新增简介标签、分集图例与独立视觉皮肤(src/features/playback、src/styles/pages/playback.css)。
- 卡片:短剧/视频卡状态与时长移至信息行胶囊标签,封面角标移至左上角(src/components/catalog/Cards.tsx、src/styles/foundations/base.css)。
- 同步更新单元测试、浏览器冒烟脚本、路由清单与 README。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:33:31 +07:00

179 lines
5.6 KiB
TypeScript

"use client";
import { useEffect, type ReactNode } from "react";
import type { Data } from "@/server/services/model";
import {
initializeTracker,
navigation,
pageClick,
pageView,
externalAd,
tracker,
track,
type TrackerBoot,
} from "./tracker";
function loadScript(src: string): Promise<void> {
return new Promise((resolve) => {
const existing = document.querySelector<HTMLScriptElement>(
`script[src="${src}"]`,
);
if (existing) {
resolve();
return;
}
const script = document.createElement("script");
script.src = src;
script.async = true;
script.onload = () => resolve();
script.onerror = () => resolve();
document.head.append(script);
});
}
export function TrackerProvider({
data,
children,
boot: explicitBoot,
}: {
data?: Data;
boot?: TrackerBoot;
children?: ReactNode;
}) {
useEffect(() => {
let disposed = false;
const boot: TrackerBoot = explicitBoot || {
appId: String(data?.trackerAppId || ""),
encryptedConfig:
data?.reportSdkConfig ??
data?.appConfig?.reportSdkConfig ??
data?.appConfig?.ReportSDKConfig ??
data?.appConfig?.reportSDKConfig,
appVersion: "1.0.0",
pageKey: data?.pageKey,
pageName: data?.pageName,
userType: data?.isVip ? "vip" : "normal",
uid: String(data?.userID || data?.userId || ""),
channel: data?.channel,
};
void loadScript("/static/common/web-sdk-v1.2.0.js?v=huakui-20261013-system")
.then(() => initializeTracker(boot))
.then(() => {
if (!disposed) pageView();
});
const google = "G-J58C18CTS0";
const yandex =
data?.isNavPage || data?.requestPath === "/navigation/" ? 112952789 : 112951899;
window.dataLayer ||= [];
window.gtag ||= (...args) => {
window.dataLayer!.push(args);
};
window.gtag("js", new Date());
window.gtag("config", google);
void loadScript(`https://www.googletagmanager.com/gtag/js?id=${google}`);
window.ym ||= Object.assign(
(...args: unknown[]) => {
window.ym!.a!.push(args);
},
{ a: [] as unknown[][], l: Date.now() },
);
window.ym(yandex, "init", {
ssr: true,
webvisor: true,
clickmap: true,
ecommerce: "dataLayer",
referrer: document.referrer,
url: location.href,
accurateTrackBounce: true,
trackLinks: true,
});
void loadScript(`https://mc.yandex.ru/metrika/tag.js?id=${yandex}`);
const click = (event: MouseEvent) => {
if (!(event.target instanceof Element)) return;
const target = event.target;
if (
!target.closest(
"[data-ad-close],[data-dialog-ad-close],[data-download-close],[data-login-close],[data-register-close],[data-recharge-close],[data-play-coin-close],[data-setting-password-close],[data-drawer-close]",
)
)
pageClick(event);
const anchor = target.closest<HTMLAnchorElement>(
"a[data-navigation-key],a[data-nav-key]",
);
if (anchor) navigation(anchor);
const ad = target.closest<HTMLElement>(
'[data-track-ad="external"][data-ad-id]',
);
if (ad) externalAd(ad, "click");
};
document.addEventListener("click", click, true);
const observer =
typeof IntersectionObserver === "undefined"
? null
: new IntersectionObserver(
(entries) => {
for (const entry of entries)
if (entry.isIntersecting) {
externalAd(entry.target as HTMLElement, "show");
observer?.unobserve(entry.target);
}
},
{ threshold: 0.1 },
);
const observed = new WeakSet<Element>();
const observe = () =>
document
.querySelectorAll(
'[data-track-ad="external"][data-ad-id]:not([data-ad-clone])',
)
.forEach((node) => {
if (!observed.has(node)) {
observed.add(node);
if (observer) observer.observe(node);
else externalAd(node as HTMLElement, "show");
}
});
observe();
const mutations = new MutationObserver(observe);
mutations.observe(document.body, { childList: true, subtree: true });
const external = (event: Event) => {
const detail = (
event as CustomEvent<{ element: HTMLElement; eventType: string }>
).detail;
if (detail?.element) externalAd(detail.element, detail.eventType);
};
const advertising = (event: Event) => {
const detail = (
event as CustomEvent<{ element: HTMLElement; eventType: string }>
).detail;
const node = detail?.element;
if (node)
void track("advertising", {
event_type: detail.eventType,
advertising_key: node.dataset.advertisingKey || "dialog_ad",
advertising_name: node.dataset.advertisingName || "弹窗广告",
advertising_id:
node.dataset.advertisingId || node.dataset.dialogAdId || "",
});
};
document.addEventListener("external-ad:track", external);
document.addEventListener("dialog-ad:track", advertising);
const flush = () => {
void tracker.flush();
};
window.addEventListener("pagehide", flush);
return () => {
disposed = true;
observer?.disconnect();
mutations.disconnect();
document.removeEventListener("click", click, true);
document.removeEventListener("external-ad:track", external);
document.removeEventListener("dialog-ad:track", advertising);
window.removeEventListener("pagehide", flush);
};
}, [data, explicitBoot]);
return children;
}
export function useTracker() {
return tracker;
}
export default TrackerProvider;