广告点击添加
This commit is contained in:
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
import { Suspense } from "react";
|
||||
import { AdClickReporter } from "@/components/ad/ad-click-reporter";
|
||||
import { AdDialogSlot } from "@/components/ad/ad-dialog-slot";
|
||||
import { AddressDialogSlot } from "@/components/address/address-dialog-slot";
|
||||
import { AgeGateDialogHost } from "@/components/age-gate/age-gate-dialog";
|
||||
@@ -99,6 +100,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
<SiteShell>{children}</SiteShell>
|
||||
<GuestAuthBootstrap />
|
||||
<AuthUnauthorizedBridge />
|
||||
<AdClickReporter />
|
||||
<LoginDialogHost />
|
||||
<Suspense fallback={null}>
|
||||
<DownloadDialogSlot />
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { adClickPayload, reportAdClick } from "@/lib/api/ads-actions";
|
||||
import { getAuthToken } from "@/lib/auth/session";
|
||||
|
||||
/**
|
||||
* 全站广告点击上报:document 捕获阶段监听 click / auxclick(中键),
|
||||
* 命中带 data-ads-id 的元素即异步上报;失败静默,不阻塞也不拦截广告跳转。
|
||||
*/
|
||||
export function AdClickReporter() {
|
||||
useEffect(() => {
|
||||
const onClick = (event: MouseEvent) => {
|
||||
if (event.type === "auxclick" && event.button !== 1) return;
|
||||
const payload = adClickPayload(event.target);
|
||||
if (!payload) return;
|
||||
void reportAdClick(payload, getAuthToken()).catch(() => {});
|
||||
};
|
||||
document.addEventListener("click", onClick, true);
|
||||
document.addEventListener("auxclick", onClick, true);
|
||||
return () => {
|
||||
document.removeEventListener("click", onClick, true);
|
||||
document.removeEventListener("auxclick", onClick, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { getAgeGateOpen, subscribeAgeGate } from "@/components/age-gate/age-gate
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
import { useChromeless } from "@/components/layout/route-visibility";
|
||||
import { lockBodyScroll } from "@/lib/browser/scroll-lock";
|
||||
import { adClickData } from "@/lib/api/ads-actions";
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
|
||||
/** 对齐 jha-1043 dialogAd.js:签名队列 + session 进度 / 关闭态 */
|
||||
@@ -284,6 +285,7 @@ export function AdDialogHost({
|
||||
href={ad.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer sponsored"
|
||||
{...adClickData(ad)}
|
||||
className="block"
|
||||
>
|
||||
{picture}
|
||||
@@ -330,6 +332,7 @@ function NineGridDialog({ ads, onClose }: { ads: Array<AdvertiseInfo & { images:
|
||||
target="_blank"
|
||||
rel="sponsored nofollow noopener noreferrer"
|
||||
title={item.title || undefined}
|
||||
{...adClickData(item)}
|
||||
onClick={onClose}
|
||||
className="flex flex-col items-center gap-2"
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
import { adClickData } from "@/lib/api/ads-actions";
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
import { IMG_PLACEHOLDER } from "@/lib/image/decrypt";
|
||||
|
||||
@@ -38,6 +39,7 @@ export function AdBanners({
|
||||
href={href}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
{...adClickData(ad)}
|
||||
className="block aspect-[720/150] overflow-hidden rounded-xl"
|
||||
>
|
||||
<DecryptedImage
|
||||
@@ -68,6 +70,7 @@ export function AdBanners({
|
||||
href={href}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
{...adClickData(ad)}
|
||||
className="block aspect-[720/150] overflow-hidden rounded-xl"
|
||||
>
|
||||
<DecryptedImage
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AdIconMarquee } from "@/components/common/ad-icon-marquee";
|
||||
import { DecryptedImage } from "@/components/common/decrypted-image";
|
||||
import { adClickData } from "@/lib/api/ads-actions";
|
||||
import type { AdvertiseInfo, AdvitiseData } from "@/lib/api/types";
|
||||
import { IMG_PLACEHOLDER } from "@/lib/image/decrypt";
|
||||
|
||||
@@ -45,6 +46,7 @@ export function AdSquares({ slot, variant = "pc", items, advitise, className = "
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "sponsored nofollow noopener noreferrer" : undefined}
|
||||
title={ad.title || undefined}
|
||||
{...adClickData(ad)}
|
||||
className={`group flex shrink-0 flex-col items-center gap-1.5 ${isH5 ? "w-[72px]" : "w-[120px]"}`}
|
||||
>
|
||||
<DecryptedImage
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { apiRequest } from "./client";
|
||||
import type { AdvertiseInfo } from "./types";
|
||||
|
||||
/** 广告服务里本站的 appId */
|
||||
export const AD_APP_ID = 12;
|
||||
|
||||
/** POST /api/app/ads/click 请求体(后端三个字段均必填,int64) */
|
||||
export type AdClickReq = {
|
||||
appId: number;
|
||||
adsId: number;
|
||||
location: number;
|
||||
};
|
||||
|
||||
/** 后端 id 为数字字符串(如 "1138200002");非正整数视为无效 */
|
||||
function positiveId(value: unknown) {
|
||||
const number =
|
||||
typeof value === "number"
|
||||
? value
|
||||
: typeof value === "string" && /^\d+$/.test(value.trim())
|
||||
? Number(value.trim())
|
||||
: NaN;
|
||||
return Number.isSafeInteger(number) && number > 0 ? number : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广告链接上的上报数据(展开到 <a> 上)。
|
||||
* id / location 任一无效时返回空对象,点击不上报。
|
||||
*/
|
||||
export function adClickData(ad: Pick<AdvertiseInfo, "id" | "location">) {
|
||||
const adsId = positiveId(ad.id);
|
||||
const location = positiveId(ad.location);
|
||||
if (!adsId || !location) return {};
|
||||
return { "data-ads-id": String(adsId), "data-ads-location": String(location) };
|
||||
}
|
||||
|
||||
/** 从点击目标向上找最近的广告元素,组装上报参数 */
|
||||
export function adClickPayload(target: EventTarget | null): AdClickReq | null {
|
||||
if (!(target instanceof Element)) return null;
|
||||
const el = target.closest<HTMLElement>("[data-ads-id]");
|
||||
if (!el) return null;
|
||||
const adsId = positiveId(el.dataset.adsId);
|
||||
const location = positiveId(el.dataset.adsLocation);
|
||||
return adsId && location ? { appId: AD_APP_ID, adsId, location } : null;
|
||||
}
|
||||
|
||||
/** 客户端:广告点击上报 POST /api/app/ads/click(keepalive,页面跳走也能发出) */
|
||||
export function reportAdClick(payload: AdClickReq, token?: string | null) {
|
||||
return apiRequest<null>("/api/app/ads/click", {
|
||||
method: "POST",
|
||||
body: payload,
|
||||
token,
|
||||
keepalive: true,
|
||||
});
|
||||
}
|
||||
@@ -30,6 +30,8 @@ type RequestOptions = {
|
||||
token?: string | null;
|
||||
headers?: Record<string, string>;
|
||||
signal?: AbortSignal;
|
||||
/** 页面卸载后仍发出(上报类接口) */
|
||||
keepalive?: boolean;
|
||||
};
|
||||
|
||||
function withClientHeaders(extra?: Record<string, string>) {
|
||||
@@ -41,7 +43,7 @@ function withClientHeaders(extra?: Record<string, string>) {
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { method = "GET", body, token, headers = {}, signal } = options;
|
||||
const { method = "GET", body, token, headers = {}, signal, keepalive } = options;
|
||||
const requestHeaders: Record<string, string> = withClientHeaders(headers);
|
||||
|
||||
if (body !== undefined) {
|
||||
@@ -56,6 +58,7 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
||||
headers: requestHeaders,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal,
|
||||
keepalive,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
Reference in New Issue
Block a user