// 第一方广告渲染器:API 仅返回结构化字段,后台内容不会作为 HTML/JavaScript 执行。
(function () {
const slots = document.querySelectorAll("[data-ad-slot]");
const device = window.matchMedia("(max-width: 640px)").matches ? "mobile" : "desktop";
let googleLoader;
const loadGoogle = (publisherId) => {
if (googleLoader) return googleLoader;
googleLoader = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.async = true;
script.crossOrigin = "anonymous";
script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${encodeURIComponent(publisherId)}`;
script.onload = resolve;
script.onerror = reject;
document.head.append(script);
});
return googleLoader;
};
const clear = (slot) => slot.querySelectorAll("[data-ad-placeholder]").forEach((item) => item.remove());
const eventUrl = (slot, ad, eventType) =>
`/api/ads/${encodeURIComponent(ad.campaignId)}/events/${eventType}?placementKey=${encodeURIComponent(slot.dataset.adSlot)}&device=${device}`;
const recordEvent = (slot, ad, eventType) => {
if (!ad.campaignId || (ad.providerKind !== "direct" && ad.providerKind !== "house")) return;
fetch(eventUrl(slot, ad, eventType), { method: "POST", credentials: "same-origin", keepalive: true })
.catch(() => { /* 统计失败不影响广告跳转或页面体验。 */ });
};
const enableClickTracking = (link, slot, ad) => {
link.addEventListener("click", () => recordEvent(slot, ad, "click"), { once: true });
};
const renderImage = (slot, ad) => {
clear(slot);
const link = document.createElement("a");
link.href = ad.targetUrl;
link.target = "_blank";
link.rel = "sponsored noopener noreferrer";
enableClickTracking(link, slot, ad);
const image = document.createElement("img");
image.src = ad.imageUrl;
image.alt = "广告";
image.loading = "lazy";
link.append(image);
slot.append(link);
slot.classList.add("is-filled", "is-image-ad");
recordEvent(slot, ad, "impression");
};
const renderText = (slot, ad) => {
clear(slot);
const link = document.createElement("a");
link.href = ad.targetUrl;
link.target = "_blank";
link.rel = "sponsored noopener noreferrer";
link.className = "ad-text-creative";
enableClickTracking(link, slot, ad);
const headline = document.createElement("strong");
headline.textContent = ad.headline;
const body = document.createElement("small");
body.textContent = ad.bodyText;
link.append(headline, body);
if (ad.callToAction) {
const action = document.createElement("b");
action.textContent = ad.callToAction;
link.append(action);
}
slot.append(link);
slot.classList.add("is-filled", "is-text-ad");
recordEvent(slot, ad, "impression");
};
const renderGoogle = async (slot, ad) => {
clear(slot);
const unit = document.createElement("ins");
unit.className = "adsbygoogle";
unit.style.display = "block";
unit.dataset.adClient = ad.publisherId;
unit.dataset.adSlot = ad.slotId;
unit.dataset.adFormat = "auto";
unit.dataset.fullWidthResponsive = "true";
slot.append(unit);
try {
await loadGoogle(ad.publisherId);
(window.adsbygoogle = window.adsbygoogle || []).push({});
slot.classList.add("is-filled");
} catch (error) { console.error("Google 广告加载失败", error); }
};
const loadAutoAds = async () => {
try {
const response = await fetch("/api/ads-config", { headers: { Accept: "application/json" } });
if (response.status === 204 || !response.ok) return;
const config = await response.json();
if (config.autoAdsEnabled && config.publisherId) await loadGoogle(config.publisherId);
} catch (error) { console.error("自动广告配置请求失败", error); }
};
loadAutoAds();
slots.forEach(async (slot) => {
try {
const query = new URLSearchParams({ device });
if (slot.dataset.adCategoryId) query.set("categoryId", slot.dataset.adCategoryId);
const response = await fetch(`/api/ads/${encodeURIComponent(slot.dataset.adSlot)}?${query}`, { headers: { Accept: "application/json" } });
if (response.status === 204 || !response.ok) return;
const ad = await response.json();
if ((ad.kind === "direct" || ad.kind === "house") && ad.imageUrl && ad.targetUrl) renderImage(slot, ad);
else if (ad.kind === "text" && ad.headline && ad.bodyText && ad.targetUrl) renderText(slot, ad);
else if (ad.kind === "google" && ad.publisherId && ad.slotId) await renderGoogle(slot, ad);
} catch (error) { console.error("广告请求失败", error); }
});
})();