// 第一方广告渲染器: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); } }); })();