- 原生 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>
179 lines
5.6 KiB
TypeScript
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;
|