Netflix 人氣影集《怪奇物語 Stranger Things》第四季終於在影迷的熱烈敲碗下正式上線,而從先前曝光的前導預告及海報中便能窺見本季故事線將會有前所未有的發展:Netflix《怪奇物語4》最新海報公開 11 重剃平頭、穿實驗袍!網友驚:她又回去那個鬼地方嗎?。值得一提的是,第四季《怪奇物語》率先進行第一集首映會,結果也不出意外獲得高度評價!
本站圖片部分取自於網路,如有版權使用疑慮煩請告知。
左右滑動查看更多 →
-
這個世界很吵,但你不必每一場都回嘴。有些人不是想懂你,他們只是缺一個情緒垃圾桶。你不回應,不是輸;是你終於醒了。注意力是貨幣,別亂付費。惡意不需要被澄清,它需要的是舞台。而你,可以直接撤場。情商高,不是忍,是知道誰不值得你浪費人生。 ...
-
恐懼像夜裡的海浪它會拍上礁石,
也會退去。
你不需要一次征服整片海,只要學會~
和每一道浪一起呼吸,
浪就會慢慢變得溫柔 ...
-
當妳遇見上帝,祂沒有金色的光,沒有審判的眼,只是坐在妳心裡的一個角落,靜靜地微笑,像老朋友一樣。祂不問妳信什麼、錯過了誰、失去了什麼,只問妳:「孩子,累了就回家,我給妳靠。」那一刻,妳所有的盔甲都化成了淚水,原來,祂不是來檢查妳過得多好,而 ...
-
〈她狂唱,而我微笑〉颱風天的夜晚,妳在做什麼呢? 風敲打著窗,像是在替內心的聲音伴奏。我心中有一頭野獸,她披著光與暗的皮毛,在深夜裡咆哮, 在夢裡奔跑。她不壞,只是太久沒被擁抱。 ...
-
有時,我們害怕失去,於是開始囤積~囤積食物、訊息、回憶、關係,囤積那些以為能讓自己安全的東西。我們說那是「愛」,其實...只是恐懼的形狀。真正的愛,從不需要被堆疊。它會呼吸、會流動、會在放手之間長出新的自己。 ...
-
宇宙,其實一直在護妳,當桃花是劫,也是課。遇到桃花劫時,妳以為那是愛的降臨,其實是靈魂的召喚。宇宙不曾要妳受傷,祂只是用愛的形狀,帶妳照見還沒療好的自己。 ...
-
2025 年第 60 屆金鐘獎於本月 17、18 日在台北流行音樂中心盛大舉行。今(17)日登場的是節目類頒獎典禮,台下星光熠熠、氣氛熱烈。今年「綜藝節目獎」競爭激烈,入圍節目包括《夜市王》的 Lulu、阿達、美麗本人,《小姐不熙娣》的小 ...
-
SANDRO 與 Clarks Originals 攜手合作,推出 2025 秋冬聯名系列,聚焦兩款經典鞋履:今年迎來 75 週年的 Desert BootTM,以及 WallabeeTM。此次合作展現了 SANDRO 現代巴黎美學與 Cl ...
-
在網路科技發達的現代,有一個都市傳說應該一直困擾著許多人,那就是我們如果與朋友們討論一些商品或品牌,手機就會開始出現相關的內容以及產品的廣告,就好像我們講話被竊聽了一樣,而系統也就自行推播了相關產品給我們,藉此運用科技賺取金錢。雖然這個問題 ...
-
尊鴻餐飲集團旗下義式料理品牌 嵩 sung 攜手韓國頂尖料理藝術家、《黑白大廚》林熙元主廚,正式發表跨界聯名企劃——「有形無界 Boundless Within Form」。以超過百種台灣在地食材與醬汁為創作語彙,結合主廚擅長的蒸、煮、川燙 ...
-
「『對話』韓國單色畫家-昔永浩個展」將於 114 年 10 月 24 日至 11 月 02 日在文化部所屬國立中正紀念堂管理處 3 展廳展出,開幕典禮訂於 10 月 24 日下午 2 時 30 分舉行,誠摯邀請各界共襄盛舉。 ...
的情形。
var params = "";
var r18 = '0';
var page = 1;
var host = window.location.protocol + "//" + window.location.host;
var isLoading = false;
var loadedArticleIds = [aid]; // 追蹤已載入的文章ID,防止重複
var articleElements = []; // 追蹤所有文章元素和其資訊
var currentArticleId = aid; // 目前顯示的文章ID
var isUpdatingUrl = false; // 防止URL更新衝突
var lastScrollTop = 0; // 追蹤滾動位置
var scrollVelocity = 0; // 滾動速度
var isScrollLocked = false; // 滾動鎖定狀態
// 瀑布流配置參數(臨時直接定義,避免變數傳遞問題)
// 🔥 GAM 分類名稱映射(中文→GAM adUnit 用)
var gamCategoryMapping = {
'時尚潮流': 'fashion',
'造型搭配': 'style',
'話題潮鞋': 'shoes',
'明星娛樂': 'star',
'運動賽事': 'sport',
'生活休閒': 'life',
'全球KOL': 'star', // 歸類到 star
'潮速報': 'theme'
};
// 根據中文分類名稱取得 GAM 用的英文分類
function getGamCategory(catName) {
if (!catName) return 'star'; // 預設
var mapped = gamCategoryMapping[catName];
if (mapped) return mapped;
// 嘗試部分匹配
for (var key in gamCategoryMapping) {
if (catName.indexOf(key) !== -1 || key.indexOf(catName) !== -1) {
return gamCategoryMapping[key];
}
}
return 'star'; // 預設
}
// 🔥 診斷:檢查 $cat_parent_name 是否正確傳遞
var rawCatParentName = '明星娛樂';
var rawCatName = '影劇';
console.log('🔍 [分類診斷] PHP 傳遞的值:', {
cat_parent_name: rawCatParentName || '(空值)',
cat_name: rawCatName || '(空值)',
cat_id: '41'
});
var waterfallConfig = {
enabled: true,
maxArticles: 4,
maxPages: 5,
estimatedHeight: 800,
triggerDistance: 1200,
loadingDelay: 300,
apiUrl: '/getarticle/juksy2', // API 端點
categoryId: 41, // 文章分類ID
categoryName: '影劇', // 文章分類名稱(中文)
categoryNameRaw: rawCatParentName, // 🔥 原始分類名稱(用於診斷)
categoryNameGam: getGamCategory('明星娛樂'), // GAM 用分類名稱
isAdult: 0, // R18 內容標記
gaTracking: {
enabled: true,
sendVirtualPageview: true
},
// 🔥 瀑布流廣告額外版位開關(預設關閉,需要增加廣告量時開啟)
extraAds: {
cover: false, // 是否在瀑布流中顯示 cover/takeover 廣告
sticky: true // 是否在瀑布流中顯示 sticky 廣告
}
};
console.log('瀑布流配置:', waterfallConfig);
console.log('當前文章 ID:', aid);
console.log('文章分類 ID:', waterfallConfig.categoryId);
console.log('🏷️ GAM 分類名稱:', waterfallConfig.categoryNameGam, '(原始值:', waterfallConfig.categoryNameRaw, ')');
console.log('R18 狀態:', waterfallConfig.isAdult);
console.log('已載入文章 IDs:', loadedArticleIds);
// 🔥 後台廣告配置診斷
if (typeof window.ad_slot_status !== 'undefined') {
console.log('✅ ad_slot_status 已載入,keys:', Object.keys(window.ad_slot_status).join(', '));
if (window.ad_slot_status.bottom_1 || window.ad_slot_status.bottom_2) {
console.log('📊 bottom ad 配置:', {
bottom_1: window.ad_slot_status.bottom_1 ? '有配置' : '無',
bottom_2: window.ad_slot_status.bottom_2 ? '有配置' : '無'
});
} else {
console.warn('⚠️ ad_slot_status 中找不到 bottom_1 或 bottom_2 配置');
}
} else {
console.warn('⚠️ window.ad_slot_status 未定義,瀑布流 bottom ad 將使用預設配置');
}
// 偵錯:檢查 DOM 載入狀態
if (document.readyState === 'loading') {
console.log('瀑布流:DOM 還在載入中,等待 DOMContentLoaded');
} else {
console.log('瀑布流:DOM 已載入完成');
}
// 初始化第一篇文章資訊
// 🔥 修正:使用更精確的選擇器,並確保主文章有 data-article-id 屬性
var mainArticleElement = document.querySelector('.articleWrap[data-article-id="' + aid + '"]') ||
document.querySelector('section.js-articleWrap .articleWrap') ||
document.querySelector('.articleWrap');
// 🔥 為主文章元素添加 data-article-id 屬性(如果沒有的話)
if (mainArticleElement && !mainArticleElement.hasAttribute('data-article-id')) {
mainArticleElement.setAttribute('data-article-id', aid);
console.log('🔧 已為主文章添加 data-article-id:', aid);
}
var mainArticleTitle = document.title;
var mainArticleUrl = window.location.pathname + (window.location.search || '');
articleElements.push({
id: aid,
title: mainArticleTitle,
element: mainArticleElement,
url: mainArticleUrl,
page: 0,
isMain: true // 🔥 標記這是主文章
});
console.log('📌 主文章初始化:', {
id: aid,
title: mainArticleTitle,
url: mainArticleUrl,
element: mainArticleElement ? '已找到' : '未找到'
});
var getData = {
getArticle: function() {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/getarticle/juksy2', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var r = JSON.parse(xhr.responseText);
if (r.status) {
// 檢查是否已載入過這篇文章,防止重複
if (loadedArticleIds.includes(r.aid)) {
console.log('文章重複,跳過載入:', r.aid);
isLoading = false;
hideAllWaterfallLoadingIndicators();
return;
}
// 標記文章開始載入
waterfallArticleStates.loading.add(r.aid);
console.log('開始載入瀑布流文章:', r.aid);
// 隱藏所有載入指示器
hideAllWaterfallLoadingIndicators();
// 預留空間避免 CLS,使用配置的預估高度
var estimatedHeight = waterfallConfig.estimatedHeight;
var relatedWrap = document.querySelector('div.relatedWrap');
// 檢查 relatedWrap 是否存在
if (!relatedWrap) {
console.error('瀑布流錯誤:找不到 relatedWrap 容器');
console.log('DOM 中存在的容器:', document.querySelectorAll('div[class*="related"], div[class*="Related"]'));
isLoading = false;
return;
}
console.log('瀑布流:找到 relatedWrap 容器', relatedWrap);
// 創建包裝容器 - 使用更智慧的高度預測
var waterfallContainer = document.createElement('div');
waterfallContainer.className = 'waterfall-article-container';
// 創建臨時容器來測量實際內容高度
var measurementContainer = document.createElement('div');
measurementContainer.style.cssText = `
position: absolute;
left: -10000px;
top: -10000px;
width: ${relatedWrap.offsetWidth || 1000}px;
visibility: hidden;
pointer-events: none;
contain: layout;
`;
measurementContainer.innerHTML = r.data;
document.body.appendChild(measurementContainer);
// 使用 requestAnimationFrame 確保測量準確
requestAnimationFrame(function() {
var actualHeight = measurementContainer.offsetHeight;
var safeHeight = Math.max(actualHeight + 100, estimatedHeight); // 加100px安全邊距
// 清理測量容器
document.body.removeChild(measurementContainer);
// 設置動態高度容器(完全避免重疊問題)
waterfallContainer.style.cssText = `
height: auto;
min-height: auto;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
will-change: opacity;
overflow: visible;
position: relative;
box-sizing: border-box;
margin: 30px 0;
`;
console.log('瀑布流容器設定為完全動態高度,不使用預估高度避免重疊');
// 修正:為每篇瀑布流文章創建獨立的區域
// 不插入到 relatedWrap 內部,而是在它之前創建新的獨立區域
var waterfallSection = document.createElement('section');
waterfallSection.className = 'waterfall-article-section';
// 統一層級管理 - 桌面版和手機版都使用遞減z-index避免覆蓋
var isMobile = window.innerWidth <= 768;
var articleCount = loadedArticleIds.length;
var zIndexValue = 100 - articleCount; // 統一使用遞減層級:100, 99, 98...
waterfallSection.style.cssText = `
width: 100%;
margin: 0;
padding: 0;
position: relative;
z-index: ${zIndexValue};
isolation: isolate;
clear: both;
${isMobile ? 'transform: none; margin: 40px 0;' : ''}
`;
console.log('瀑布流區域層級設定 - 平台:', isMobile ? '手機版' : '桌面版', 'Z-index:', zIndexValue, '文章編號:', articleCount);
// 在 relatedWrap 之前插入獨立的瀑布流區域
relatedWrap.parentNode.insertBefore(waterfallSection, relatedWrap);
// 將瀑布流容器插入到獨立區域中
waterfallSection.appendChild(waterfallContainer);
console.log('瀑布流容器已插入到獨立的 section 區域,第', loadedArticleIds.length, '篇文章');
// 強制重繪
waterfallContainer.offsetHeight;
// 設置內容前的檢查
console.log('準備插入瀑布流內容,文章ID:', r.aid);
console.log('內容長度:', r.data.length, '字元');
console.log('內容開頭:', r.data.substring(0, 200));
console.log('內容結尾:', r.data.substring(r.data.length - 200));
// 設置內容
waterfallContainer.innerHTML = r.data;
console.log('瀑布流內容已插入,容器內容長度:', r.data.length, '字元');
console.log('容器 DOM 子元素數量:', waterfallContainer.children.length);
// 🔥🔥🔥 關鍵修復:瀑布流文章 HTML 由同一個 template 渲染,
// GPT div id(如 div-gpt-ad-1690282150377-0-m)與主文章重複。
// DOM 內出現重複 id 會導致 document.getElementById() 永遠回主文章那個元素,
// 後續 showWaterfallAd 把該元素 id 改名(加 -atl1)等於偷走主文章 div 給瀑布流用,
// 結果是「主文章還沒來得及完整載入廣告就被改 id,瀑布流綁到的 slot 又指向被偷走的主文章 div」,
// 造成瀑布流位置永遠空白、且只有主文章位置會出現廣告。
// 解法:插入新文章 HTML 後立即把所有 GPT div id 加上 -{aid} 後綴,確保 DOM id 唯一。
try {
var gptDivs = waterfallContainer.querySelectorAll('[id^="div-gpt-ad-1690282150377-"]');
gptDivs.forEach(function(gptDiv) {
var oldId = gptDiv.id;
// 已經帶 aid 的話跳過
if (oldId.indexOf('-' + r.aid) !== -1) return;
// 將後綴 -d / -m 改成 -{aid}-d / -{aid}-m
var newId = oldId.replace(/-([dm])$/, '-' + r.aid + '-$1');
if (newId !== oldId) {
gptDiv.id = newId;
console.log('🔧 瀑布流 GPT div id 唯一化:', oldId, '→', newId);
}
});
} catch (e) {
console.warn('瀑布流 GPT div id 唯一化失敗:', e);
}
// 🔥 使用 LazyLoad 機制初始化瀑布流廣告 - 避免 CLS
setTimeout(function() {
console.log('🌊 開始初始化瀑布流文章廣告 (LazyLoad):', r.aid);
// 🔢 計算當前文章索引,用於 atl 輪替
// 🔥 修正:使用 loadedArticleIds 陣列來計算索引(更可靠)
var waterfallArticleIndex = 0;
var articleIdStr = String(r.aid);
for (var i = 0; i < loadedArticleIds.length; i++) {
if (String(loadedArticleIds[i]) === articleIdStr) {
waterfallArticleIndex = i;
break;
}
}
// 如果找不到(剛加入還沒 push),使用當前長度
if (waterfallArticleIndex === 0 && loadedArticleIds[0] !== articleIdStr) {
waterfallArticleIndex = loadedArticleIds.length;
}
var atlIndex = (waterfallArticleIndex % 4) + 1;
console.log('📍 文章索引:', waterfallArticleIndex, '→ atl' + atlIndex, '(文章ID:', r.aid, '/ 已載入:', loadedArticleIds.length + ')');
// 查找該文章內的所有廣告容器
var articleContainer = document.querySelector('[data-article-id="' + r.aid + '"]');
if (!articleContainer) {
console.error('❌ 找不到文章容器:', r.aid);
return;
}
console.log('✅ 找到文章容器:', articleContainer);
// 🔥 優先使用 ajax 回傳的「該篇瀑布流文章自己」的 ad_slot_status,
// 過去誤用 window.ad_slot_status 會導致每篇瀑布流文章都拿到主文章的 campaign 設定
// (PM 後台針對每篇/每個位置設定的活動名稱會被吃掉,全部顯示成主文章的名字)。
var perArticleAdSlotStatus = r.ad_slot_status || window.ad_slot_status;
if (typeof perArticleAdSlotStatus === 'undefined' || !perArticleAdSlotStatus) {
console.error('❌ ad_slot_status 未定義 (article:', r.aid + ')');
return;
}
console.log('✅ 使用瀑布流文章自己的 ad_slot_status:', r.aid, Object.keys(perArticleAdSlotStatus));
var deviceType = window.innerWidth <= 768 ? 'mobile' : 'desktop';
console.log('📱 設備類型:', deviceType);
var waterfallSlots = [];
// 🎯 檢測頁面上是否已有 LiTV ppnio 影音廣告(避免重複 sticky 衝突)
var hasExistingLiTV = document.querySelector('.ppn-container, [data-campaign*="LiTV"], [id^="ppn-"]');
if (hasExistingLiTV) {
console.log('⚠️ 頁面上已存在 LiTV ppnio 廣告,瀑布流將跳過 LiTV 廣告避免 sticky 衝突');
}
// 🎯 先定義所有廣告位,但不立即刷新 (避免 CLS)
['bottom_1', 'bottom_2'].forEach(function(adKey) {
console.log('🔍 檢查廣告:', adKey);
var adConfig = perArticleAdSlotStatus?.[adKey]?.[deviceType];
if (!adConfig) {
console.warn('⚠️ 沒有廣告配置:', adKey, deviceType);
return;
}
// 🎯 檢查是否為 LiTV/內部廣告活動
var campaignResource = adConfig.campaignResource;
var campaignName = '';
var isInternalAd = false; // provider === 0 表示內部廣告
var hasIframeSrc = false; // 檢查是否有 iframe 來源
if (campaignResource) {
var campaign = Array.isArray(campaignResource) ? campaignResource[0] : campaignResource;
campaignName = campaign?.name || campaign?.datas?.name || '';
// 內部廣告的判斷:provider 為 0 或 campaignResource 有 datas 但沒有 adunitKey
isInternalAd = !campaign?.datas?.adunitKey;
// 🔧 也檢查 iframeSrc 是否包含 ad-assets(LiTV 內部廣告特徵)
var iframeSrc = campaign?.datas?.iframeSrc || '';
hasIframeSrc = iframeSrc.includes('ad-assets') || iframeSrc.includes('litv');
}
var isLiTVAd = campaignName.includes('LiTV') || campaignName.includes('Videoplayer') || hasIframeSrc;
// 🔥 如果頁面已有 LiTV 且這是 LiTV 內部廣告,改用 GAM
var useGAMInstead = hasExistingLiTV && isLiTVAd;
if (useGAMInstead) {
console.log('🔄 頁面已有 LiTV,瀑布流 bottom 改用 GAM:', adKey, '(原 campaign:', campaignName, ', isInternal:', isInternalAd, ', hasIframe:', hasIframeSrc + ')');
}
console.log('✅ 找到廣告配置:', adKey, adConfig, useGAMInstead ? '(改用GAM)' : '');
// 🔥 使用包含 atl 索引的唯一 div ID,確保每個瀑布流文章都建立新的 slot
var adDiv = 'div-gpt-ad-1690282150377-' + (adKey === 'bottom_1' ? '0' : '1') + '-' + r.aid + '-atl' + atlIndex;
var container = document.getElementById(adDiv);
// 🔥 如果容器不存在,嘗試查找原始 ID 的容器並修改其 ID
if (!container) {
var originalAdDiv = 'div-gpt-ad-1690282150377-' + (adKey === 'bottom_1' ? '0' : '1') + '-' + r.aid;
container = document.getElementById(originalAdDiv);
if (container) {
container.id = adDiv; // 修改為新的唯一 ID
console.log('🔄 已修改容器 ID:', originalAdDiv, '→', adDiv);
}
}
console.log('🔍 尋找容器:', adDiv, container ? '找到' : '未找到');
if (container && adConfig.campaignResource) {
// 🔧 預設容器最小高度,防止 CLS
// 🔧 SSP 廣告優化:不使用 contain: layout,避免影響廣告尺寸和 viewability
container.style.minHeight = '250px';
console.log('✅ 已設置容器樣式:', adDiv, 'minHeight: 250px');
var campaign = Array.isArray(adConfig.campaignResource)
? adConfig.campaignResource[0]
: adConfig.campaignResource;
var adUnit = campaign?.datas?.adunitKey;
var adSize = campaign?.datas?.dimensions;
// � 如果是 LiTV 內部廣告且需要改用 GAM
if (useGAMInstead || (!adUnit && isInternalAd)) {
// 清除內部廣告的 HTML 內容
var parentContainer = container.closest('.adBottom');
if (parentContainer) {
// 移除 LiTV 的 data-campaign 標記
parentContainer.removeAttribute('data-campaign');
// 清空內部廣告內容,只保留 GAM 容器
var existingContent = parentContainer.querySelector('[id^="ppn-"], script, .ppn-container');
if (existingContent) {
console.log('🗑️ 清除 LiTV 內部廣告內容');
// 清除所有 LiTV 相關元素
parentContainer.querySelectorAll('[id^="ppn-"], .ppn-container').forEach(function(el) {
el.remove();
});
}
}
// 🔥 使用分類專屬的 GAM bottom 廣告單元
// 格式: /21697024903,65167615/juksy_{category}_content_bottom_ad1
var gamCategory = waterfallConfig.categoryNameGam || 'star';
adUnit = '/21697024903,65167615/juksy_' + gamCategory + '_content_bottom_ad1';
adSize = [[1,1],[300,250],[336,280],[250,250],[200,200]];
console.log('🔄 改用 GAM 廣告單元 (分類:', gamCategory + '):', adUnit);
}
// �🔀 添加 atl 後綴(bottom 瀑布流專用單元)
if (adUnit && !adUnit.includes('_atl') && !adUnit.includes('_passback')) {
adUnit = adUnit + '_atl' + atlIndex;
console.log('🔀 使用瀑布流專用單元:', adUnit, '(文章', waterfallArticleIndex + 1, '→ atl' + atlIndex + ')');
}
console.log('📊 廣告詳情:', {adKey: adKey, adUnit: adUnit, adSize: adSize, adDiv: adDiv});
if (adUnit && adSize) {
googletag.cmd.push(function() {
// � 瀑布流廣告必須使用新的 slot 和 _atl 後綴的 adUnit
// 不要複用舊 slot,因為舊 slot 的 adUnit 是固定的(沒有 _atl)
var existingSlots = googletag.pubads().getSlots();
var existingSlot = null;
for (var i = 0; i < existingSlots.length; i++) {
if (existingSlots[i].getSlotElementId() === adDiv) {
existingSlot = existingSlots[i];
break;
}
}
if (existingSlot) {
// 🔥 已存在的 slot,檢查是否使用正確的 atl adUnit
var existingAdUnit = existingSlot.getAdUnitPath();
if (existingAdUnit.includes('_atl')) {
console.log('♻️ 廣告位已存在且使用正確 atl 單元,直接複用:', adKey, adDiv, existingAdUnit);
waterfallSlots.push({slot: existingSlot, container: container, adDiv: adDiv, adKey: adKey});
} else {
// 舊 slot 沒有 _atl,無法修改,需要銷毀並重建
console.warn('⚠️ 舊 slot 沒有 atl 後綴,無法修改。建議使用新的容器 ID:', adKey, existingAdUnit);
// GAM 不支援銷毀單一 slot,所以只能跳過
waterfallSlots.push({slot: existingSlot, container: container, adDiv: adDiv, adKey: adKey});
}
} else {
console.log('📌 定義新的瀑布流廣告 (含 _atl 後綴):', adKey, adDiv, adUnit);
var slot = googletag.defineSlot(adUnit, adSize, adDiv)
.addService(googletag.pubads());
googletag.display(adDiv);
waterfallSlots.push({slot: slot, container: container, adDiv: adDiv, adKey: adKey});
console.log('✅ 廣告位已定義 (atl' + atlIndex + '):', adDiv, adUnit);
}
});
} else {
console.error('❌ 廣告單元或尺寸缺失:', {adUnit: adUnit, adSize: adSize});
}
} else {
if (!container) console.error('❌ 容器不存在:', adDiv);
if (!adConfig.campaignResource) console.error('❌ 沒有廣告資源:', adKey);
}
});
['right_1', 'right_2'].forEach(function(adKey) {
console.log('🔍 檢查廣告:', adKey);
var adConfig = perArticleAdSlotStatus?.[adKey]?.[deviceType];
if (!adConfig) {
console.warn('⚠️ 沒有廣告配置:', adKey, deviceType);
return;
}
// 🔥 使用包含 atl 索引的唯一 div ID,確保每個瀑布流文章都建立新的 slot
var adDiv = 'div-gpt-ad-169028' + (adKey === 'right_1' ? '2046839' : '2073535') + '-0-' + r.aid + '-atl' + atlIndex;
var container = document.getElementById(adDiv);
// 🔥 如果容器不存在,嘗試查找原始 ID 的容器並修改其 ID
if (!container) {
var originalAdDiv = 'div-gpt-ad-169028' + (adKey === 'right_1' ? '2046839' : '2073535') + '-0-' + r.aid;
container = document.getElementById(originalAdDiv);
if (container) {
container.id = adDiv; // 修改為新的唯一 ID
console.log('🔄 已修改容器 ID:', originalAdDiv, '→', adDiv);
}
}
console.log('🔍 尋找容器:', adDiv, container ? '找到' : '未找到');
if (container && adConfig.campaignResource) {
// 🔧 預設容器最小高度
container.style.minHeight = '250px';
container.style.contain = 'layout';
var campaign = Array.isArray(adConfig.campaignResource)
? adConfig.campaignResource[0]
: adConfig.campaignResource;
var adUnit = campaign?.datas?.adunitKey;
var adSize = campaign?.datas?.dimensions;
// 🔀 添加 atl 後綴(right 瀑布流專用單元)
if (adUnit && !adUnit.includes('_atl') && !adUnit.includes('_passback')) {
adUnit = adUnit + '_atl' + atlIndex;
console.log('🔀 使用瀑布流專用單元:', adUnit, '(文章', waterfallArticleIndex + 1, '→ atl' + atlIndex + ')');
}
console.log('📊 廣告詳情:', {adKey: adKey, adUnit: adUnit, adSize: adSize, adDiv: adDiv});
if (adUnit && adSize) {
googletag.cmd.push(function() {
// 🔥 瀑布流廣告必須使用新的 slot 和 _atl 後綴的 adUnit
var existingSlots = googletag.pubads().getSlots();
var existingSlot = null;
for (var i = 0; i < existingSlots.length; i++) {
if (existingSlots[i].getSlotElementId() === adDiv) {
existingSlot = existingSlots[i];
break;
}
}
if (existingSlot) {
// 🔥 已存在的 slot,檢查是否使用正確的 atl adUnit
var existingAdUnit = existingSlot.getAdUnitPath();
if (existingAdUnit.includes('_atl')) {
console.log('♻️ 廣告位已存在且使用正確 atl 單元,直接複用:', adKey, adDiv, existingAdUnit);
waterfallSlots.push({slot: existingSlot, container: container, adDiv: adDiv, adKey: adKey});
} else {
console.warn('⚠️ 舊 slot 沒有 atl 後綴:', adKey, existingAdUnit);
waterfallSlots.push({slot: existingSlot, container: container, adDiv: adDiv, adKey: adKey});
}
} else {
console.log('📌 定義新的瀑布流廣告 (含 _atl 後綴):', adKey, adDiv, adUnit);
var slot = googletag.defineSlot(adUnit, adSize, adDiv)
.addService(googletag.pubads());
googletag.display(adDiv);
waterfallSlots.push({slot: slot, container: container, adDiv: adDiv, adKey: adKey});
console.log('✅ 廣告位已定義 (atl' + atlIndex + '):', adDiv, adUnit);
}
});
}
}
});
console.log('📦 總共定義了', waterfallSlots.length, '個瀑布流廣告位');
// 🚀 使用 IntersectionObserver 只在廣告進入視窗時才刷新 (防止 CLS)
if (waterfallSlots.length > 0 && 'IntersectionObserver' in window) {
waterfallSlots.forEach(function(item) {
var hasRefreshed = false; // 防止重複刷新
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting && entry.intersectionRatio > 0.01 && !hasRefreshed) {
hasRefreshed = true;
console.log('👁️ 瀑布流廣告進入視窗,開始載入:', item.adDiv, '(' + item.adKey + ')');
googletag.cmd.push(function() {
googletag.pubads().refresh([item.slot]);
console.log('🔄 已刷新廣告:', item.adDiv);
});
observer.disconnect(); // 只刷新一次
}
});
}, {
rootMargin: '200px', // 提前 200px 開始載入
threshold: 0.01
});
observer.observe(item.container);
console.log('👀 開始監控廣告容器:', item.adDiv);
// 🔍 延遲檢查容器是否已經在視窗內(不依賴容器尺寸,因為有 minHeight)
console.log('⏰ 設置 500ms 延遲檢查:', item.adDiv);
setTimeout(function() {
console.log('⏰ 延遲檢查開始執行:', item.adDiv);
if (hasRefreshed) {
console.log('⏭️ 廣告已刷新,跳過檢查:', item.adDiv);
return;
}
var rect = item.container.getBoundingClientRect();
var windowHeight = window.innerHeight || document.documentElement.clientHeight;
// 使用滾動位置計算(更可靠)
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
var containerTop = scrollTop + rect.top;
var viewportBottom = scrollTop + windowHeight;
var isVisible = containerTop < viewportBottom + 200;
console.log('🔍 檢查廣告容器位置:', item.adDiv, {
scrollTop: Math.round(scrollTop),
containerTop: Math.round(containerTop),
viewportBottom: Math.round(viewportBottom),
isVisible: isVisible
});
if (isVisible) {
hasRefreshed = true;
console.log('⚡ 廣告容器在視窗內,立即刷新:', item.adDiv);
googletag.cmd.push(function() {
googletag.pubads().refresh([item.slot]);
console.log('🔄 已刷新廣告:', item.adDiv);
});
observer.disconnect();
} else {
console.log('👇 容器不在視窗內,等待滾動觸發:', item.adDiv);
}
}, 500); // 延遲 500ms 確保文章內容完全載入
});
console.log('✅ 已設置', waterfallSlots.length, '個瀑布流廣告的 LazyLoad 監控');
} else if (waterfallSlots.length > 0) {
// 降級方案:瀏覽器不支援 IntersectionObserver 時才立即刷新
console.warn('⚠️ 瀏覽器不支援 IntersectionObserver,使用降級方案');
setTimeout(function() {
console.log('🔄 降級方案:立即刷新', waterfallSlots.length, '個廣告');
googletag.cmd.push(function() {
googletag.pubads().refresh(waterfallSlots.map(function(s) { return s.slot; }));
});
}, 1000);
} else {
console.error('❌ 沒有可用的廣告位');
}
}, 500);
// 立即為新載入的廣告容器設置背景圖監控
var newAdContainers = waterfallContainer.querySelectorAll('.adContent, .adTop1');
newAdContainers.forEach(function(container) {
if (typeof setupAdMonitoring === 'function') {
setupAdMonitoring(container);
}
});
// 檢查標籤是否完整載入
var tagsWrap = waterfallContainer.querySelector('.tagsWrap');
if (tagsWrap) {
console.log('標籤區域已找到,標籤數量:', tagsWrap.children.length);
console.log('標籤內容:', tagsWrap.innerHTML.substring(0, 200));
} else {
console.warn('未找到標籤區域 (.tagsWrap)');
}
// 檢查完整性
var shareWrap = waterfallContainer.querySelector('.shareWrap');
if (shareWrap) {
console.log('分享區域已找到');
} else {
console.warn('未找到分享區域 (.shareWrap)');
}
// 檢查實際渲染高度
setTimeout(function() {
var finalHeight = waterfallContainer.offsetHeight;
console.log('瀑布流容器最終高度:', finalHeight, 'px');
if (finalHeight < actualHeight * 0.8) {
console.warn('容器高度可能不足,可能發生重疊!');
}
// 再次檢查標籤是否渲染完成
var finalTagsWrap = waterfallContainer.querySelector('.tagsWrap');
if (finalTagsWrap) {
console.log('最終檢查:標籤區域存在,標籤數量:', finalTagsWrap.children.length);
} else {
console.error('最終檢查:標籤區域遺失!');
}
}, 100);
// 添加淡入動畫並確保標籤可見性
setTimeout(function() {
waterfallContainer.style.opacity = '1';
waterfallContainer.style.transform = 'translateY(0)';
// 強制重繪標籤區域
var tagsWrap = waterfallContainer.querySelector('.tagsWrap');
if (tagsWrap) {
tagsWrap.style.display = 'flex';
tagsWrap.style.flexWrap = 'wrap';
tagsWrap.style.visibility = 'visible';
tagsWrap.offsetHeight; // 強制重繪
console.log('標籤區域樣式已強制更新');
}
console.log('瀑布流文章淡入動畫完成:', r.aid);
}, 50);
// 記錄新載入的文章資訊
var newArticleElement = waterfallContainer.querySelector('.articleWrap');
if (newArticleElement) {
// 修正:瀑布流文章URL不應包含POST參數,保持乾淨的URL
var originalParams = ""; // 原始頁面的參數(同上,必須跳脫)
var articleUrl = originalParams == '' ?
host+'/article/'+r.aid :
host+'/article/'+r.aid+'?'+originalParams;
console.log('瀑布流文章URL:', articleUrl, '(不包含POST參數)');
articleElements.push({
id: r.aid,
title: r.article_title,
element: newArticleElement,
url: articleUrl,
page: r.page,
isMain: false // 🔥 標記這不是主文章
});
console.log('📌 瀑布流文章已添加:', {
id: r.aid,
title: r.article_title,
url: articleUrl,
totalArticles: articleElements.length
});
}
// 加入已載入列表
loadedArticleIds.push(r.aid);
page = r.page;
extend_ad_slot_status.push(r.ad_slot_status);
isLoading = false;
// 瀑布流文章廣告初始化 - 參考 article_v3.php 的 show_bottomad 機制
if (r.ad_slot_status) {
initWaterfallAds(r.aid, r.ad_slot_status, waterfallContainer);
// 🔧 觸發廣告背景圖檢測
setTimeout(function() {
// 檢查容器內的所有廣告元素並設定背景圖監控
const waterfallAdContainers = waterfallContainer.querySelectorAll('.adContent, .adTop1');
console.log('🌊 瀑布流中發現廣告容器:', waterfallAdContainers.length);
// 🔍 詳細檢查每個廣告容器
waterfallAdContainers.forEach(function(adContainer, index) {
// 🔥 HTML 屬性在 innerHTML 插入後會自動轉換成小寫
var adKey = adContainer.getAttribute('ad-key');
var adInternal = adContainer.getAttribute('ad-internal');
console.log('🔍 廣告容器', index + 1, ':', {
id: adContainer.id,
className: adContainer.className,
adKey: adKey,
adInternal: adInternal,
hasMonitoring: adContainer.hasAttribute('data-ad-monitoring'),
hasFinalStatus: adContainer.hasAttribute('data-ad-final-status'),
isEmpty: adContainer.children.length === 0,
hasAdKeyAttr: adContainer.hasAttribute('ad-key'),
allAttributes: Array.from(adContainer.attributes).map(a => a.name).join(', ')
});
// 🔥 瀑布流廣告容器不需要再次設定監控,因為 checkAdContent() 已經處理過
// setupAdMonitoring 會在主頁面載入時設定,瀑布流只需要檢查一次即可
// ❌ 不要呼叫 setupAdMonitoring,避免重複設定導致無限循環
// 🔥 只檢查一次,且只在還沒有 final status 時
if (typeof checkIfAdLoaded === 'function' && !adContainer.hasAttribute('data-ad-final-status')) {
console.log('🔄 檢查瀑布流廣告載入狀態(單次):', adContainer.id || adContainer.className);
setTimeout(() => {
if (!adContainer.hasAttribute('data-ad-final-status')) {
checkIfAdLoaded(adContainer, null);
}
}, 1500); // 只檢查一次,給廣告足夠時間載入
}
});
// 發送自定義事件通知主檢測系統
const waterfallEvent = new CustomEvent('waterfallAdsLoaded', {
detail: {
articleId: r.aid,
container: waterfallContainer,
adContainers: waterfallAdContainers.length
}
});
document.dispatchEvent(waterfallEvent);
}, 500);
// 廣告初始化後延遲重算容器高度,並標記文章載入完成
setTimeout(function() {
recalculateWaterfallContainerHeight(r.aid);
// 標記此文章已完全載入,允許載入下一篇
markWaterfallArticleLoaded(r.aid);
}, 2000); // 給廣告更多時間載入
} else {
// 沒有廣告時也要標記載入完成
setTimeout(function() {
markWaterfallArticleLoaded(r.aid);
}, 500);
}
// 備用:如果有全域廣告載入函數則調用
if(typeof juksy_autoload_ad === "function"){
juksy_autoload_ad();
}
// 🔥 修正:不在 AJAX 載入時立即更換 URL 和觸發 GA
// 原因:使用者此時還在看上一篇文章,尚未滾動到新文章
// URL 更新和 GA virtual_pageview 改由 detectCurrentVisibleArticle() 在使用者實際滾到時觸發
// updateCurrentArticleUrl(r.aid, r.article_title); // 移至滾動偵測
console.log('📝 瀑布流文章已載入(延遲 GA):', r.aid, '等待使用者滾動到時才發送 virtual_pageview');
// 更新當前文章ID為新載入的文章
aid = r.aid;
console.log('瀑布流載入文章:', r.aid, '標題:', r.article_title, 'URL已更新');
// 顯示動畫
setTimeout(function() {
console.log('觸發瀑布流容器顯示動畫');
waterfallContainer.style.opacity = '1';
waterfallContainer.style.transform = 'translateY(0)';
waterfallContainer.style.height = actualHeight + 'px'; // 使用實際測量的高度
waterfallContainer.style.overflow = 'visible'; // 允許內容顯示
console.log('瀑布流容器樣式已更新: opacity=', waterfallContainer.style.opacity, 'height=', waterfallContainer.style.height);
}, 100);
});
} else {
console.error('瀑布流載入失敗,API 回應:', r);
console.log('失敗的請求參數:', {
aid: aid,
page: loadedArticleIds.length,
loaded_ids: loadedArticleIds.join(','),
cat_id: waterfallConfig.categoryId,
r18: waterfallConfig.isAdult,
當前已載入文章數: loadedArticleIds.length,
最大頁數限制: waterfallConfig.maxPages
});
hideAllWaterfallLoadingIndicators();
isLoading = false;
}
} else {
console.error('瀑布流請求失敗 HTTP:', xhr.status, xhr.statusText);
console.log('回應內容:', xhr.responseText);
hideAllWaterfallLoadingIndicators();
isLoading = false;
}
} else {
isLoading = false;
isScrollLocked = false;
}
};
// 優化參數 - 直接傳送必要的分類資訊,確保頁面參數正確
// 計算下一頁:已載入文章數量 + 1 (因為第一篇是 page 0)
var nextPage = loadedArticleIds.length;
var postParams = 'aid='+aid+'&page='+nextPage+'&loaded_ids='+loadedArticleIds.join(',')+'&cat_id='+waterfallConfig.categoryId+'&r18='+waterfallConfig.isAdult;
console.log('瀑布流請求參數:', {
aid: aid,
page: nextPage,
loaded_ids: loadedArticleIds.join(','),
cat_id: waterfallConfig.categoryId,
r18: waterfallConfig.isAdult
});
xhr.send(postParams);
},
getIdleArticleEl: function(){
var xhr = new XMLHttpRequest();
xhr.open('POST', '/getarticle/idleArticles', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var r = JSON.parse(xhr.responseText);
if (r.status) {
var idleWrap = document.querySelector('ul.js-idleArticleWrap');
idleWrap.innerHTML = r.outputEl;
}
}
};
xhr.send('');
}
};
// URL 更新函數
function updateCurrentArticleUrl(articleId, articleTitle) {
var targetArticle = articleElements.find(article => article.id == articleId);
if (targetArticle && currentArticleId != articleId) {
isUpdatingUrl = true;
currentArticleId = articleId;
// 🔥 同步更新頁面標題(使用 processWaterfallTitle 統一格式,避免 GA4 出現重複標題)
var newTitle = processWaterfallTitle(articleTitle || targetArticle.title);
document.title = newTitle;
window.history.replaceState(
{
additionalInformation: 'waterfall_article',
article_id: articleId,
waterfall_navigation: true
},
newTitle,
targetArticle.url
);
console.log('URL已更新為文章:', articleId, targetArticle.url, '標題:', newTitle);
setTimeout(function() {
isUpdatingUrl = false;
}, 100);
}
}
// 偵測當前可視文章
function detectCurrentVisibleArticle() {
if (isUpdatingUrl || articleElements.length <= 1) return;
var windowHeight = window.innerHeight;
var viewportCenter = windowHeight / 2;
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
var maxVisibleArea = 0;
var mostVisibleArticle = null;
// 🔥 找到主文章
var mainArticle = articleElements.find(function(a) { return a.isMain === true; });
// 🔥 特殊處理:檢查主文章是否在視窗中可見
if (mainArticle && mainArticle.element) {
var mainRect = mainArticle.element.getBoundingClientRect();
// 🔥🔥🔥 關鍵修正:如果主文章的頂部在視窗內(包含負值但不超過文章高度的一半)
// 這表示用戶正在看主文章的上半部,應該顯示主文章的 URL
var mainArticleTopVisible = mainRect.top > -mainRect.height * 0.5;
var mainArticleInView = mainRect.top < windowHeight && mainRect.bottom > 0;
// 如果滾動位置在頁面頂部 OR 主文章的標題區域在視窗內
var isNearTop = scrollTop < 500;
var isMainArticleTitleVisible = mainRect.top > -200 && mainRect.top < windowHeight * 0.6;
if ((isNearTop || isMainArticleTitleVisible) && mainArticleInView) {
if (mainArticle.id != currentArticleId) {
console.log('📍 主文章可見,切換回主文章:', mainArticle.id, {
scrollTop: scrollTop,
mainRectTop: Math.round(mainRect.top),
isNearTop: isNearTop,
isTitleVisible: isMainArticleTitleVisible
});
updateCurrentArticleUrl(mainArticle.id, mainArticle.title);
}
return; // 🔥 重要:直接返回,不再繼續檢查其他文章
}
}
articleElements.forEach(function(article) {
if (!article.element || !article.element.getBoundingClientRect) return;
// 🔥 跳過主文章(已經在上面處理過了)
if (article.isMain) return;
var rect = article.element.getBoundingClientRect();
var elementTop = Math.max(0, rect.top);
var elementBottom = Math.min(windowHeight, rect.bottom);
var visibleArea = Math.max(0, elementBottom - elementTop);
// 🔥 檢查文章頂部是否在視窗上半部(用於判斷「正在閱讀」的文章)
var isTopInUpperHalf = rect.top < viewportCenter && rect.top > -rect.height;
// 檢查文章是否在視窗中心附近
var distanceFromCenter = Math.abs((elementTop + elementBottom) / 2 - viewportCenter);
var centerWeight = Math.max(0, windowHeight - distanceFromCenter) / windowHeight;
var weightedVisibleArea = visibleArea * centerWeight;
// 🔧 調整判斷條件:只要可見區域 > 30% 就考慮
if (weightedVisibleArea > maxVisibleArea && visibleArea > windowHeight * 0.3) {
maxVisibleArea = weightedVisibleArea;
mostVisibleArticle = article;
}
});
// 🔧 只有瀑布流文章符合條件時才更新
if (mostVisibleArticle &&
mostVisibleArticle.id != currentArticleId &&
maxVisibleArea > windowHeight * 0.35) {
console.log('🔄 更新 URL 到瀑布流文章:', mostVisibleArticle.id, '可見區域:', Math.round(maxVisibleArea));
updateCurrentArticleUrl(mostVisibleArticle.id, mostVisibleArticle.title);
// 發送 GA page_view 事件 (瀑布流導航)
if (waterfallConfig.gaTracking.enabled && waterfallConfig.gaTracking.sendVirtualPageview) {
if (typeof gtag === 'function') {
gtag('event', 'page_view', {
'page_title': processWaterfallTitle(mostVisibleArticle.title),
'page_location': window.location.href,
'page_path': window.location.pathname,
'article_id': mostVisibleArticle.id,
'content_group': '影劇',
'content_type': 'article',
'waterfall_index': mostVisibleArticle.page
});
console.log('滾動導航 GA page_view 已發送:', mostVisibleArticle.id);
}
}
}
}
// 滾動檢測和瀑布流載入
var scrollTimeout;
var footerEmergencyProtection = false;
var lastUrlUpdateTime = 0; // 防止 URL 更新過於頻繁
window.addEventListener('scroll', function() {
// 使用節流避免過度觸發
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(function() {
// 🔧 URL 更新防抖,降低到 500ms 讓切換更靈敏
var now = Date.now();
if (now - lastUrlUpdateTime > 500) {
detectCurrentVisibleArticle();
lastUrlUpdateTime = now;
}
// 緊急 footer 防護
if (!footerEmergencyProtection && isLoading) {
var footer = document.querySelector('footer.foot');
if (footer) {
var footerRect = footer.getBoundingClientRect();
var windowHeight = window.innerHeight;
// 如果 footer 進入視窗範圍且正在載入中
if (footerRect.top < windowHeight && footerRect.top > 0) {
footerEmergencyProtection = true;
console.log('啟動緊急 footer 防護!');
// 立即在 footer 前插入緊急緩衝空間
var emergencyBuffer = document.createElement('div');
emergencyBuffer.className = 'emergency-footer-buffer';
// 獲取當前 theme 背景色
var isDarkTheme = document.body.classList.contains('dark-theme');
var bgGradient = isDarkTheme
? 'linear-gradient(180deg, var(--secondary-7-color) 0%, var(--secondary-5-color) 50%, var(--secondary-4-color) 100%)'
: 'linear-gradient(180deg, var(--secondary-7-color) 0%, var(--secondary-5-color) 50%, var(--secondary-4-color) 100%)';
var textColor = isDarkTheme ? 'var(--secondary-3-color)' : 'var(--secondary-3-color)';
emergencyBuffer.style.cssText = `
height: 400px;
background: ${bgGradient};
display: flex;
align-items: center;
justify-content: center;
color: ${textColor};
font-size: 16px;
font-weight: 500;
border-top: 3px solid var(--primary-1-color);
animation: emergencySlideIn 0.3s ease-out;
`;
emergencyBuffer.innerHTML = `
`;
footer.parentNode.insertBefore(emergencyBuffer, footer);
// 5秒後移除緊急緩衝(如果還存在)
setTimeout(function() {
if (emergencyBuffer.parentNode) {
emergencyBuffer.style.opacity = '0';
setTimeout(function() {
if (emergencyBuffer.parentNode) {
emergencyBuffer.parentNode.removeChild(emergencyBuffer);
}
}, 300);
}
footerEmergencyProtection = false;
}, 5000);
}
}
}
}, 50);
// 瀑布流載入檢測
if (waterfallConfig.enabled && !isLoading) {
var scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
var windowHeight = window.innerHeight;
var documentHeight = document.documentElement.scrollHeight;
var scrollPercentage = (scrollPosition + windowHeight) / documentHeight;
var scrollThreshold = 1 - (waterfallConfig.triggerDistance / documentHeight);
// 檢查載入條件
if (scrollPercentage >= scrollThreshold) {
// 檢查載入限制 - 使用一致的計數方式
var maxLoadedArticles = waterfallConfig.maxArticles + 1; // +1 for original article
var currentPageNumber = loadedArticleIds.length; // 目前要載入的頁數
console.log('瀑布流載入檢查:', {
已載入文章數: loadedArticleIds.length,
最大文章數: maxLoadedArticles,
目前頁數: currentPageNumber,
最大頁數: waterfallConfig.maxPages,
滾動百分比: Math.round(scrollPercentage * 100) + '%'
});
if (loadedArticleIds.length >= maxLoadedArticles) {
console.log('已達瀑布流文章數量上限:', maxLoadedArticles);
return;
} else if (currentPageNumber >= waterfallConfig.maxPages) {
console.log('已達瀑布流頁數上限:', waterfallConfig.maxPages, '目前頁數:', currentPageNumber);
return;
}
// 檢查是否有文章正在載入中,防止重疊
if (!canLoadNextWaterfallArticle()) {
console.log('前一篇文章尚未載入完成,延後觸發瀑布流');
return;
}
isLoading = true;
console.log('觸發瀑布流載入,頁面:', currentPageNumber, '已載入文章:', loadedArticleIds.length);
// 顯示載入指示器
showLastWaterfallLoadingIndicator();
// 延遲載入以改善使用體驗
setTimeout(function() {
getData.getArticle();
}, waterfallConfig.loadingDelay);
}
}
});
// 初始化檢查
console.log('瀑布流 JavaScript 載入完成');
// 檢查 DOM 是否準備就緒
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
console.log('瀑布流:DOMContentLoaded 事件觸發');
var relatedWrapCheck = document.querySelector('div.relatedWrap');
if (relatedWrapCheck) {
console.log('瀑布流:relatedWrap 元素檢查通過');
} else {
console.error('瀑布流:找不到 relatedWrap 元素');
}
});
} else {
console.log('瀑布流:DOM 已經載入完成');
var relatedWrapCheck = document.querySelector('div.relatedWrap');
if (relatedWrapCheck) {
console.log('瀑布流:relatedWrap 元素檢查通過');
} else {
console.error('瀑布流:找不到 relatedWrap 元素');
}
}
// 瀑布流文章載入狀態管理
var waterfallArticleStates = {
loading: new Set(), // 正在載入的文章ID
loaded: new Set() // 已完全載入的文章ID
};
// 標記文章載入完成
function markWaterfallArticleLoaded(articleId) {
waterfallArticleStates.loading.delete(articleId);
waterfallArticleStates.loaded.add(articleId);
console.log('瀑布流文章載入完成:', articleId, '已載入文章數:', waterfallArticleStates.loaded.size);
// 🔥 瀑布流文章載入後,檢查是否需要重新顯示 sticky/cover 廣告
if (typeof waterfallConfig !== 'undefined' && waterfallConfig.extraAds) {
// Cover 廣告
if (waterfallConfig.extraAds.cover) {
console.log('🎯 [瀑布流] Cover 廣告已開啟,重新呼叫 cover_ads()');
setTimeout(function() {
if (typeof window.cover_ads === 'function') {
console.log('✅ [瀑布流] 正在呼叫 window.cover_ads()');
window.cover_ads();
} else {
console.warn('⚠️ [瀑布流] window.cover_ads 函數不存在');
}
}, 300);
}
// Sticky 廣告
if (waterfallConfig.extraAds.sticky) {
console.log('🎯 [瀑布流] Sticky 廣告已開啟,重新呼叫 sticky_ads()');
// 重新呼叫 sticky_ads() 函數來載入廣告
setTimeout(function() {
if (typeof window.sticky_ads === 'function') {
console.log('✅ [瀑布流] 正在呼叫 window.sticky_ads()');
window.sticky_ads();
} else {
console.warn('⚠️ [瀑布流] window.sticky_ads 函數不存在');
}
}, 500);
}
}
// 觸發自定義事件通知其他組件
var event = new CustomEvent('waterfallArticleLoaded', {
detail: { articleId: articleId, totalLoaded: waterfallArticleStates.loaded.size }
});
document.dispatchEvent(event);
}
// 檢查是否可以載入下一篇文章
function canLoadNextWaterfallArticle() {
// 如果有文章正在載入,就不能載入下一篇
if (waterfallArticleStates.loading.size > 0) {
console.log('有文章正在載入中,暫緩載入下一篇,載入中:', Array.from(waterfallArticleStates.loading));
return false;
}
return true;
}
var waterfallAdCounters = {};
function getWaterfallAdDivId(adElement, adKey, articleId, adDevice) {
var existingId = adElement.getAttribute('data-ad-div-id');
if (existingId) {
return existingId;
}
var counterKey = adKey + '_' + articleId + '_' + adDevice;
if (!waterfallAdCounters[counterKey]) {
waterfallAdCounters[counterKey] = 1;
} else {
waterfallAdCounters[counterKey] += 1;
}
var generatedId = adKey + '-' + articleId + '-' + adDevice + '-' + waterfallAdCounters[counterKey] + '-' + Math.floor(Math.random() * 1000);
adElement.setAttribute('data-ad-div-id', generatedId);
return generatedId;
}
function initWaterfallAds(articleId, adSlotStatus, container) {
if (!adSlotStatus || typeof googletag === 'undefined') {
console.warn('瀑布流廣告初始化失敗: adSlotStatus 或 googletag 未定義', articleId);
return;
}
console.log('🎯 開始初始化瀑布流廣告:', articleId, 'container:', container);
console.log('📋 接收到的 adSlotStatus keys:', Object.keys(adSlotStatus));
// 🔥 計算 atl 索引 (1-4):根據此文章是瀑布流第幾篇
var waterfallArticleIndex = loadedArticleIds.indexOf(articleId);
if (waterfallArticleIndex < 0) waterfallArticleIndex = loadedArticleIds.length;
var atlIndex = Math.min(waterfallArticleIndex + 1, 4); // 1-4 循環
console.log('🔢 ATL 索引計算:', 'articleId:', articleId, 'waterfallIndex:', waterfallArticleIndex, '→ atl' + atlIndex);
// 🔥 檢查 adSlotStatus 是否包含 content 廣告配置
var hasContentAds = adSlotStatus.content_1_1 || adSlotStatus.content_1_2;
if (!hasContentAds) {
// ⚠️ 過去這裡會直接 fallback 到 window.ad_slot_status(主文章),
// 結果整個瀑布流文章的 campaign / adunit 都被替換成主文章的設定,
// 造成 PM 後台針對每篇文章設定的活動名稱在前台失效(瀑布流永遠顯示主文章 campaign)。
// 改為只記錄警告、保留該瀑布流文章原本回傳的 ad_slot_status,
// 即使部分欄位空白也比錯誤標示主文章 campaign 好。
console.warn('⚠️ 瀑布流文章缺少 content 廣告配置,保留原始 ad_slot_status,不再 fallback 到主文章設定');
}
// 查找容器內的所有廣告元素(包含 ad-key 和 ad-internal)
var adElements = container.querySelectorAll('[ad-key], [ad-internal]');
console.log('找到', adElements.length, '個廣告元素');
// 🔥 檢測當前裝置類型(只載入對應裝置的廣告,避免 slot ID 衝突)
var currentDevice = window.innerWidth <= 768 ? 'mobile' : 'desktop';
console.log('🖥️ 當前裝置類型:', currentDevice);
// 🎯 檢測頁面上是否已有 LiTV ppnio 影音廣告(避免重複 sticky 衝突)
var hasExistingLiTV = document.querySelector('.ppn-container, [data-campaign*="LiTV"], [id^="ppn-"]');
if (hasExistingLiTV) {
console.log('⚠️ [initWaterfallAds] 頁面上已存在 LiTV ppnio 廣告,瀑布流 LiTV bottom 將改用 GAM');
}
// 🔥 修正瀑布流文章中的 adshow/adnotshow(針對該文章容器)
if (typeof window.fixAdShowClasses === 'function') {
var articleContainer = document.querySelector('[data-article-id="' + articleId + '"]');
if (articleContainer) {
window.fixAdShowClasses(articleContainer);
}
}
adElements.forEach(function(adElement, index) {
if (adElement.getAttribute('data-waterfall-initialized') === '1') {
console.log('廣告已初始化,跳過:', adElement.getAttribute('ad-key') || adElement.getAttribute('ad-internal'));
return;
}
var isInternalAd = adElement.hasAttribute('ad-internal');
var adKey = adElement.getAttribute('ad-key') || adElement.getAttribute('ad-internal');
// 🔥 如果頁面已有 LiTV 廣告,瀑布流的 LiTV bottom 廣告改用 GAM
if (hasExistingLiTV && (adKey === 'bottom_1' || adKey === 'bottom_2')) {
// 🔧 修正:data-campaign 可能在 adElement 本身或其父層 .adBottom 上
var adCampaign = adElement.getAttribute('data-campaign') || '';
var parentAdBottom = adElement.closest('.adBottom');
if (!adCampaign && parentAdBottom) {
adCampaign = parentAdBottom.getAttribute('data-campaign') || '';
}
// 🔧 也檢查內部是否有 LiTV iframe 或 ppn 元素
var hasLiTVContent = adElement.querySelector('[id^="ppn-"], .ppn-container, iframe[src*="litv"], iframe[src*="ad-assets"]');
if (adCampaign.includes('LiTV') || adCampaign.includes('Videoplayer') || hasLiTVContent) {
console.log('🔄 [initWaterfallAds] 瀑布流 LiTV bottom 改用 GAM:', adKey, '(campaign:', adCampaign, ', hasContent:', !!hasLiTVContent + ')');
// 移除 LiTV campaign 標記(從 adElement 和 parentAdBottom)
adElement.removeAttribute('data-campaign');
if (parentAdBottom) {
parentAdBottom.removeAttribute('data-campaign');
}
// 🔧 清除 LiTV 內部廣告內容(包括 iframe)
var litvElements = adElement.querySelectorAll('[id^="ppn-"], .ppn-container, script[src*="litv"], iframe[src*="litv"], iframe[src*="ad-assets"]');
litvElements.forEach(function(el) {
console.log('🗑️ 移除 LiTV 元素:', el.tagName, el.id || el.src || '');
el.remove();
});
// 🔧 也清空整個 adElement 內容(確保沒有殘留的 LiTV 內容)
adElement.innerHTML = '';
// 確保有 GAM 容器
var deviceSuffix = adElement.classList.contains('mobile') ? '-m' : '-d';
var gamDivId = 'div-gpt-ad-1690282150377-' + (adKey === 'bottom_1' ? '0' : '1') + '-' + articleId + deviceSuffix;
// 🔧 直接建立新的 GAM 容器(因為已清空 innerHTML)
var gamContainer = document.createElement('div');
gamContainer.id = gamDivId;
gamContainer.style.cssText = 'min-width: 200px; min-height: 200px;';
adElement.appendChild(gamContainer);
console.log('🆕 建立 GAM 容器:', gamDivId);
// 🔥 優先從 adSlotStatus(該瀑布流文章自己的)讀取 bottom ad 配置
var bottomAdKey = adKey === 'bottom_1' ? 'bottom_1' : 'bottom_2';
var bottomAdConfig = adSlotStatus && adSlotStatus[bottomAdKey] && adSlotStatus[bottomAdKey][adDevice];
var gamAdUnit = null;
var gamAdSize = null; // 🔥 不預設寫死,優先從後台讀取
var gamAdSizeSource = ''; // 記錄 size 來源
// 🔥 情況1:從 adSlotStatus 讀取實際配置
if (bottomAdConfig && bottomAdConfig.campaignResource) {
if (Array.isArray(bottomAdConfig.campaignResource) && bottomAdConfig.campaignResource.length > 0) {
var validResources = bottomAdConfig.campaignResource.filter(function(r) {
return r && r.datas && r.datas.adunitKey;
});
if (validResources.length > 0) {
var selectedResource = validResources[0];
gamAdUnit = selectedResource.datas.adunitKey;
// 🔥 優先從後台讀取 dimensions
if (selectedResource.datas.dimensions && Array.isArray(selectedResource.datas.dimensions)) {
gamAdSize = selectedResource.datas.dimensions;
gamAdSizeSource = '後台配置';
}
console.log('✅ [initWaterfallAds] 使用後台 bottom ad 配置:', {
bottomAdKey: bottomAdKey,
adDevice: adDevice,
gamAdUnit: gamAdUnit,
gamAdSize: gamAdSize,
sizeSource: gamAdSizeSource
});
}
}
}
// 🔥 情況2:後台沒有配置,使用預設的分類專屬單元
if (!gamAdUnit) {
console.warn('⚠️ [initWaterfallAds] 後台 bottom ad 配置缺失,使用預設配置', {
bottomAdKey: bottomAdKey,
adDevice: adDevice,
hasAdSlotStatus: !!window.ad_slot_status,
hasBottomConfig: !!bottomAdConfig
});
// 使用分類專屬的 GAM bottom 廣告單元
// 格式: /21697024903,65167615/juksy_{category}_content_bottom_ad1_atl{1-4}
var gamCategory = waterfallConfig.categoryNameGam || 'star';
gamAdUnit = '/21697024903,65167615/juksy_' + gamCategory + '_content_bottom_ad1_atl' + atlIndex;
console.log('📝 [initWaterfallAds] 使用預設 GAM 分類單元:', gamAdUnit, '(分類:', gamCategory, ', atl' + atlIndex + ')');
}
// 🔥 如果還是沒有 size,使用通用預設值(最後手段)
if (!gamAdSize) {
gamAdSize = [[336, 280], [300, 250], [250, 250], [200, 200], [1, 1]]; // 通用 responsive sizes
gamAdSizeSource = '預設值(後台無配置)';
console.warn('⚠️ [initWaterfallAds] 後台未提供 dimensions,使用預設 size:', gamAdSize);
}
console.log('🎯 [initWaterfallAds] 最終決定使用:', {
gamAdUnit: gamAdUnit,
gamAdSize: gamAdSize,
sizeSource: gamAdSizeSource
});
if (typeof googletag !== 'undefined') {
googletag.cmd.push(function() {
var existingSlot = googletag.pubads().getSlots().find(function(s) {
return s.getSlotElementId() === gamContainer.id;
});
if (!existingSlot) {
var slot = googletag.defineSlot(gamAdUnit, gamAdSize, gamContainer.id)
.addService(googletag.pubads());
googletag.display(gamContainer.id);
googletag.pubads().refresh([slot]);
console.log('✅ [initWaterfallAds] GAM 廣告已載入:', gamContainer.id, gamAdUnit);
}
});
}
adElement.setAttribute('data-waterfall-initialized', '1');
return;
}
}
// 🔥 從 CSS class 判斷裝置類型(瀑布流廣告容器沒有 data-device 屬性)
var adDevice = adElement.classList.contains('desktop') ? 'desktop' :
adElement.classList.contains('mobile') ? 'mobile' : null;
if (!adKey) {
console.error('❌ 瀑布流廣告缺少 ad-key 或 ad-internal 屬性:', adElement);
return;
}
// 🔥 檢查額外廣告開關(cover/sticky)
if (typeof waterfallConfig !== 'undefined' && waterfallConfig.extraAds) {
if ((adKey === 'cover' || adKey.includes('takeover')) && !waterfallConfig.extraAds.cover) {
console.log('⏭️ Cover 廣告已關閉,跳過:', adKey);
adElement.setAttribute('data-waterfall-initialized', '1');
return;
}
if (adKey === 'sticky' && !waterfallConfig.extraAds.sticky) {
console.log('⏭️ Sticky 廣告已關閉,跳過:', adKey);
adElement.setAttribute('data-waterfall-initialized', '1');
return;
}
}
// 🔥🔥🔥 關鍵:只處理當前裝置類型的廣告,避免 desktop/mobile 使用相同 slot ID 導致衝突
if (adDevice && adDevice !== currentDevice) {
console.log('⏭️ 跳過非當前裝置的廣告:', adKey, 'device:', adDevice, '(當前:', currentDevice + ')');
// 🔥 確保非當前設備的廣告設為 adnotshow,不讓它顯示
adElement.classList.remove('adshow');
adElement.classList.add('adnotshow');
adElement.setAttribute('data-waterfall-initialized', '1'); // 標記為已處理,避免重複處理
return;
}
console.log('處理廣告 #' + index + ':', adKey, 'device:', adDevice, 'internal:', isInternalAd);
// 如果是內部廣告 (ad-internal),檢查容器內是否已有廣告內容
if (isInternalAd) {
var existingContent = adElement.querySelector('iframe, .ad-iframe, [data-type="iframe"]');
if (existingContent) {
// 🔥 檢查這是否是當前設備的廣告
// 如果不是,清空內容避免浪費資源(即使容器被 CSS 隱藏,iframe 還是會載入)
if (adDevice && adDevice !== currentDevice) {
console.log('🗑️ 清空非當前設備的內部廣告:', adKey, 'device:', adDevice, '(當前:', currentDevice + ')');
adElement.innerHTML = '';
adElement.setAttribute('data-waterfall-initialized', '1');
return;
}
// 內部廣告已經由後端渲染,標記為已初始化並跳過
adElement.setAttribute('data-waterfall-initialized', '1');
console.log('✅ 瀑布流:內部廣告已存在,跳過:', adKey, articleId);
// 🔥 不修改 class,保持後端設定的 adshow/adnotshow
// 讓 CSS 媒體查詢自然控制顯示/隱藏
// 仍然觸發背景圖檢查
if (typeof setupAdMonitoring === 'function') {
setupAdMonitoring(adElement);
}
return;
}
}
// 檢查裝置類別,但不強制設定樣式
if (!adDevice) {
if (adElement.classList.contains('desktop')) {
adDevice = 'desktop';
} else if (adElement.classList.contains('mobile')) {
adDevice = 'mobile';
} else {
adDevice = window.innerWidth > 768 ? 'desktop' : 'mobile';
}
}
adElement.setAttribute('data-waterfall-device', adDevice);
var adDiv = adElement.querySelector('[id^="div-gpt-ad"]');
// 如果沒有 div-gpt-ad,嘗試查找帶有正確裝置後綴的 div
if (!adDiv) {
// 🔥 優先查找帶有裝置後綴的 div(-d 或 -m)
var deviceSuffix = adDevice === 'desktop' ? '-d' : '-m';
adDiv = adElement.querySelector('div[id$="' + deviceSuffix + '"]');
// 如果沒找到,再嘗試找任何帶 ID 的 div(向後相容)
if (!adDiv) {
adDiv = adElement.querySelector('div[id]');
}
}
if (!adDiv) {
// 🔥 生成帶裝置後綴的 ID
var deviceSuffix = adDevice === 'desktop' ? '-d' : '-m';
var generatedId = getWaterfallAdDivId(adElement, adKey, articleId, adDevice) + deviceSuffix;
adDiv = document.createElement('div');
adDiv.id = generatedId;
adDiv.style.minWidth = '300px';
adDiv.style.minHeight = '250px';
adDiv.setAttribute('data-generated', '1');
adElement.appendChild(adDiv);
console.log('🆕 生成瀑布流廣告容器:', generatedId);
} else {
adElement.setAttribute('data-ad-div-id', adDiv.id);
}
if (!adDiv) {
// 即使沒有 div 容器,對於 content 廣告也要觸發背景圖檢查
if (adKey && adKey.indexOf('content_') === 0) {
if (typeof setupAdMonitoring === 'function') {
setupAdMonitoring(adElement);
}
}
return;
}
var adDivId = adDiv.id;
// 對於所有廣告容器,都觸發背景圖檢查
if (typeof setupAdMonitoring === 'function') {
setupAdMonitoring(adElement);
}
// 取得對應的廣告設定
var adConfig = adSlotStatus[adKey] && adSlotStatus[adKey][adDevice];
console.log('🔍 檢查廣告配置:', {
adKey: adKey,
adDevice: adDevice,
hasAdSlotStatus: !!adSlotStatus,
hasAdKeyInStatus: !!(adSlotStatus && adSlotStatus[adKey]),
adConfig: adConfig
});
if (!adConfig) {
console.log('瀑布流廣告缺少配置:', adKey, adDevice, articleId);
return;
}
adElement.setAttribute('data-waterfall-initialized', '1');
// 🔥 從 adConfig 取得真實 campaign 名稱(過去寫死成「一般-生活-主文章」導致瀑布流文章誤標)
try {
var _campaignRes = adConfig && adConfig.campaignResource;
var _campaign = Array.isArray(_campaignRes) ? _campaignRes[0] : _campaignRes;
var _campaignName = (_campaign && (_campaign.name || (_campaign.datas && _campaign.datas.name))) || '';
if (_campaignName) {
adElement.setAttribute('data-campaign', _campaignName);
}
} catch (e) {
console.warn('取得瀑布流 campaign 名稱失敗:', e);
}
// 🔥 只有當裝置類型匹配時才顯示廣告,否則保持 adnotshow
if (adDevice === currentDevice) {
console.log('🎯 準備顯示廣告:', adKey, '當前 class:', adElement.className);
adElement.classList.remove('adnotshow');
adElement.classList.add('adshow');
// 廣告顯示邏輯 - 移除不必要的延遲
console.log('📢 呼叫 showWaterfallAd:', adKey, adDivId);
showWaterfallAd(adConfig, adDivId, adKey, articleId, adElement, adDevice);
} else {
console.log('⏭️ 跳過非當前裝置的廣告:', adKey, 'device:', adDevice, '(當前:', currentDevice + ')');
// 保持 adnotshow,不呼叫 showWaterfallAd
}
});
// 定期檢查廣告狀態,確保空廣告顯示背景
setTimeout(function() {
adElements.forEach(function(adElement) {
if (typeof checkIfAdLoaded === 'function') {
checkIfAdLoaded(adElement);
}
});
console.log('🔍 瀑布流廣告狀態檢查完成(1秒後):', articleId);
}, 1000);
setTimeout(function() {
adElements.forEach(function(adElement) {
if (typeof checkIfAdLoaded === 'function') {
checkIfAdLoaded(adElement);
}
});
console.log('🔍 瀑布流廣告狀態檢查完成(3秒後):', articleId);
}, 3000);
console.log('✅ 瀑布流廣告初始化完成:', articleId, '處理了', adElements.length, '個廣告');
}
function showWaterfallAd(ad, adDivId, adKey, articleId, adElement, adDevice) {
console.log('🚀 showWaterfallAd 收到參數:', {
ad: ad,
adDivId: adDivId,
adKey: adKey,
articleId: articleId,
adDevice: adDevice,
hasAdElement: !!adElement
});
if (!ad) {
console.error('❌ 廣告配置為空:', adKey);
return;
}
if (!adKey) {
console.error('❌ adKey 為空,無法處理廣告');
return;
}
console.log('🚀 showWaterfallAd 開始:', {
adKey: adKey,
adDivId: adDivId,
articleId: articleId,
adDevice: adDevice,
adElement: adElement
});
// 🆕 計算瀑布流文章索引,用於 atl 單元輪替
// 🔥 修正:使用 loadedArticleIds 陣列來計算索引(更可靠)
var waterfallArticleIndex = 0;
try {
var articleIdStr = String(articleId);
// 在 loadedArticleIds 中尋找該文章的位置
for (var i = 0; i < loadedArticleIds.length; i++) {
if (String(loadedArticleIds[i]) === articleIdStr) {
waterfallArticleIndex = i;
break;
}
}
// 如果找不到,使用已載入文章數量作為索引
// 🔧 修正:但要限制在合理範圍內(最多 maxArticles + 1)
if (waterfallArticleIndex === 0 && loadedArticleIds[0] !== articleIdStr) {
waterfallArticleIndex = Math.min(loadedArticleIds.length, 5); // 最多 5(1主文章+4瀑布流)
}
// 📊 詳細診斷日誌:幫助追蹤 atl 分配問題
var expectedAtl = waterfallArticleIndex > 0 ? ((waterfallArticleIndex - 1) % 4) + 1 : 0;
console.log('📍 [ATL診斷] 文章索引計算:', {
articleId: articleId,
waterfallArticleIndex: waterfallArticleIndex,
expectedAtl: expectedAtl > 0 ? 'atl' + expectedAtl : '無(主文章)',
loadedArticleIdsLength: loadedArticleIds.length,
adKey: adKey
});
} catch (e) {
console.warn('⚠️ 無法計算文章索引,使用預設值 0:', e);
}
try {
var adAdunit, adAdsize, adCampaign, adIframeSrc = '';
// � 針對 bottom ad 添加詳細診斷
if (adKey === 'bottom_1' || adKey === 'bottom_2') {
console.log('🔍 [Bottom Ad 診斷]', {
adKey: adKey,
adDevice: adDevice,
articleId: articleId,
hasAd: !!ad,
hasCampaignResource: !!(ad && ad.campaignResource),
campaignResourceType: ad && ad.campaignResource ?
(Array.isArray(ad.campaignResource) ? 'Array[' + ad.campaignResource.length + ']' : 'Object') :
'null',
fullAdConfig: ad
});
}
// 🔧 檢查廣告資源配置是否存在
if (!ad.campaignResource) {
console.warn('⚠️ 廣告配置缺少 campaignResource:', adKey, {
adKey: adKey,
adDevice: adDevice,
receivedAd: ad
});
// 標記為空廣告
if (adElement) {
adElement.setAttribute('data-ad-final-status', 'empty');
adElement.style.minHeight = '0';
adElement.style.display = 'none';
}
return;
}
// 處理廣告資源配置
if (Array.isArray(ad.campaignResource)) {
// 優先選擇有 adunitKey 的資源
var validResources = ad.campaignResource.filter(function(resource) {
return resource && resource.datas && resource.datas.adunitKey;
});
if (validResources.length > 0) {
var adRand = Math.floor(Math.random() * validResources.length);
var selectedResource = validResources[adRand];
adAdunit = selectedResource.datas.adunitKey;
adAdsize = selectedResource.datas.dimensions;
adCampaign = selectedResource.name;
adIframeSrc = selectedResource.datas?.iframeSrc || '';
// 🆕 為瀑布流廣告添加 _atl1~4 後綴(主文章不加,瀑布流才加)
// waterfallArticleIndex: 0=主文章, 1=瀑布流第1篇, 2=瀑布流第2篇...
var originalAdunit = adAdunit;
if (waterfallArticleIndex > 0 && adAdunit && !adAdunit.includes('_atl') && !adAdunit.includes('_passback')) {
var atlIndex = ((waterfallArticleIndex - 1) % 4) + 1; // 1,2,3,4,1,2,3,4...
adAdunit = adAdunit + '_atl' + atlIndex;
console.log('🔀 [ATL添加] 瀑布流廣告:', {
adKey: adKey,
originalAdunit: originalAdunit,
newAdunit: adAdunit,
waterfallArticleIndex: waterfallArticleIndex,
atlIndex: atlIndex,
articleId: articleId
});
} else {
console.log('⏭️ [ATL跳過]', {
adKey: adKey,
adAdunit: adAdunit,
waterfallArticleIndex: waterfallArticleIndex,
reason: waterfallArticleIndex === 0 ? '主文章' :
adAdunit.includes('_atl') ? '已有atl' :
adAdunit.includes('_passback') ? 'passback' : '其他'
});
}
console.log('隨機廣告選擇(有 GPT):', adRand, '/', validResources.length, '(總共', ad.campaignResource.length, '個配置)');
} else {
// 如果沒有有效的 GPT 配置,嘗試使用 iframe
var iframeResources = ad.campaignResource.filter(function(resource) {
return resource && resource.datas && resource.datas.iframeSrc;
});
if (iframeResources.length > 0) {
var adRand = Math.floor(Math.random() * iframeResources.length);
var selectedResource = iframeResources[adRand];
adIframeSrc = selectedResource.datas.iframeSrc;
adCampaign = selectedResource.name;
console.log('使用 iframe 備援:', adRand, '/', iframeResources.length);
} else {
console.warn('⚠️ 沒有可用的廣告資源:', adKey);
return;
}
}
} else if (ad.campaignResource) {
if (!ad.campaignResource.datas) {
console.warn('⚠️ 廣告資源缺少 datas:', adKey);
return;
}
adAdunit = ad.campaignResource.datas.adunitKey;
adAdsize = ad.campaignResource.datas.dimensions;
adCampaign = ad.campaignResource.name;
adIframeSrc = ad.campaignResource.datas?.iframeSrc || '';
// 🆕 為瀑布流廣告添加 _atl1~4 後綴(主文章不加,瀑布流才加)
// waterfallArticleIndex: 0=主文章, 1=瀑布流第1篇, 2=瀑布流第2篇...
var originalAdunit = adAdunit;
if (waterfallArticleIndex > 0 && adAdunit && !adAdunit.includes('_atl') && !adAdunit.includes('_passback')) {
var atlIndex = ((waterfallArticleIndex - 1) % 4) + 1; // 1,2,3,4,1,2,3,4...
adAdunit = adAdunit + '_atl' + atlIndex;
console.log('🔀 [ATL添加-單一] 瀑布流廣告:', {
adKey: adKey,
originalAdunit: originalAdunit,
newAdunit: adAdunit,
waterfallArticleIndex: waterfallArticleIndex,
atlIndex: atlIndex,
articleId: articleId
});
}
}
console.log('📦 廣告配置:', {
adKey: adKey,
adunit: adAdunit,
adsize: adAdsize,
campaign: adCampaign,
iframeSrc: adIframeSrc,
hasAdunit: !!adAdunit,
hasAdsize: !!adAdsize,
adsizeIsArray: Array.isArray(adAdsize),
adsizeLength: adAdsize ? adAdsize.length : 0
});
// 🔥 針對 bottom ad 檢查配置完整性
if (adKey === 'bottom_1' || adKey === 'bottom_2') {
console.log('🎯 [Bottom Ad 配置確認]', {
adKey: adKey,
adAdunit: adAdunit,
adAdsize: adAdsize,
hasValidConfig: !!(adAdunit && adAdsize),
willUseGAM: !!(adAdunit && adAdsize && typeof googletag !== 'undefined'),
willUseIframe: !!(adIframeSrc && !adAdunit)
});
}
// 設定廣告活動資訊
if (adElement) {
if (adCampaign) {
adElement.setAttribute('data-campaign', adCampaign);
}
adElement.setAttribute('data-waterfall-device', adDevice || '');
adElement.setAttribute('data-ad-final-status', 'loading');
}
// 🔥 為瀑布流廣告生成唯一的 div ID(主文章不加,瀑布流才加 atl 索引)
var originalDivId = adDivId;
// 計算 atlIndex(1~4 循環):瀑布流第1篇=atl1, 第2篇=atl2...
// 🔧 修正:確保 atlIndex 只會是 1-4
var atlIndex = 0;
if (waterfallArticleIndex > 0) {
atlIndex = ((waterfallArticleIndex - 1) % 4) + 1;
}
// 只有瀑布流文章才需要加 atl 後綴
// 🔧 修正:先移除任何錯誤的 atl 後綴,再添加正確的
if (waterfallArticleIndex > 0) {
// 移除可能存在的錯誤 atl 後綴(如 atl34)
adDivId = adDivId.replace(/-atl\d+$/, '');
// 添加正確的 atl 後綴
adDivId = adDivId + '-atl' + atlIndex;
// 同步修改 DOM 元素的 ID
var adDivElement = document.getElementById(originalDivId);
if (adDivElement) {
adDivElement.id = adDivId;
console.log('🔄 已修改廣告容器 ID:', originalDivId, '→', adDivId, '(atl' + atlIndex + ')');
}
}
// 初始化 Google Publisher Tag
if (adAdunit && adAdsize && typeof googletag !== 'undefined') {
console.log('📢 使用 Google Ad Manager:', {
divId: adDivId,
adUnit: adAdunit,
atlIndex: atlIndex > 0 ? 'atl' + atlIndex : '主文章',
waterfallIndex: waterfallArticleIndex,
articleId: articleId
});
googletag.cmd.push(function() {
// 檢查是否已經定義過這個廣告位
var existingSlots = googletag.pubads().getSlots();
var existingSlot = existingSlots.find(function(slot) {
return slot.getSlotElementId() === adDivId;
});
if (existingSlot) {
// 🔥 檢查現有 slot 是否使用正確的 _atl adUnit
var existingAdUnit = existingSlot.getAdUnitPath();
if (existingAdUnit.includes('_atl')) {
console.log('✅ 廣告位已存在且使用正確 atl 單元,重新刷新:', adDivId, existingAdUnit);
if (adElement) {
adElement.setAttribute('data-ad-final-status', 'loaded');
}
googletag.display(adDivId);
googletag.pubads().refresh([existingSlot]);
console.log('🔄 已刷新現有廣告位 (atl):', adDivId);
} else {
console.warn('⚠️ 現有 slot 沒有 _atl 後綴,可能會導致填充率問題:', adDivId, existingAdUnit);
// 仍然刷新,但記錄警告
googletag.display(adDivId);
googletag.pubads().refresh([existingSlot]);
}
return;
}
console.log('🆕 定義新廣告位 (含 _atl 後綴):', adDivId, adAdunit, adAdsize);
// 定義廣告位
var slot = googletag.defineSlot(adAdunit, adAdsize, adDivId);
if (slot) {
slot.addService(googletag.pubads());
// 設定目標定位
googletag.pubads().setTargeting("category", "影劇");
googletag.pubads().setTargeting("juksy_category", "影劇");
googletag.pubads().setTargeting("juksy_subcategory", "影劇");
googletag.pubads().setTargeting("article_id", articleId);
console.log('✅ 瀑布流廣告位已定義 (atl' + atlIndex + '):', adDivId, adAdunit);
// 🔥 在 enableSingleRequest 模式下,display() 只會渲染容器
// 必須呼叫 refresh() 才會實際載入廣告
googletag.display(adDivId);
console.log('📺 瀑布流廣告容器已渲染:', adDivId);
// 🔥🔥🔥 關鍵:呼叫 refresh() 實際載入廣告
googletag.pubads().refresh([slot]);
console.log('🚀 瀑布流廣告已觸發載入 (refresh):', adDivId, adAdunit, adAdsize);
// 🔥 延遲檢查:如果 slotRenderEnded 沒有觸發,手動標記
// 但要確保只在廣告有實際內容時才標記為 loaded
setTimeout(function() {
if (adElement && !adElement.hasAttribute('data-ad-final-status')) {
var adDiv = document.getElementById(adDivId);
var iframe = adDiv && adDiv.querySelector('iframe');
// 檢查 iframe 是否有實際內容(非空廣告)
// 空廣告的 iframe 通常很小或沒有可見內容
if (iframe) {
var iframeHeight = iframe.offsetHeight || iframe.clientHeight;
var hasContent = iframeHeight > 50; // 有效廣告通常高度 > 50px
if (hasContent) {
adElement.classList.add('ad-loaded');
adElement.setAttribute('data-ad-final-status', 'loaded');
console.log('🔧 手動標記廣告已載入(備援):', adDivId, 'height:', iframeHeight);
} else {
// 空廣告,確保顯示灰底背景
adElement.classList.remove('ad-loaded');
adElement.setAttribute('data-ad-final-status', 'empty');
console.log('🔧 手動標記空廣告(備援):', adDivId, 'height:', iframeHeight);
}
} else {
// 沒有 iframe,標記為空
adElement.classList.remove('ad-loaded');
adElement.setAttribute('data-ad-final-status', 'empty');
console.log('🔧 沒有 iframe,標記為空廣告:', adDivId);
}
}
}, 3000); // 延長到 3 秒確保 slotRenderEnded 有時間觸發
// ⚠️ 不要立即標記為 loaded,等待 slotRenderEnded 事件來判斷是否為空廣告
// Google 廣告會在 slotRenderEnded 事件中設定正確的狀態 (loaded/empty)
console.log('⏳ 等待 Google 廣告渲染完成事件...', adDivId);
// 廣告載入後重新計算容器高度
setTimeout(function() {
recalculateWaterfallContainerHeight(articleId);
console.log('📐 重新計算容器高度:', articleId);
}, 1000); // 等待廣告完全載入
} else {
console.error('❌ 廣告位定義失敗:', adDivId);
}
});
} else if (adIframeSrc && !adAdunit) {
// 只在沒有 adAdunit 時才使用 iframe 備援
console.log('🖼️ 使用 iframe 備援廣告(無 GPT 配置):', adDivId, adIframeSrc);
var fallbackFrame = document.getElementById(adDivId + '-iframe') || document.createElement('iframe');
fallbackFrame.id = adDivId + '-iframe';
fallbackFrame.src = adIframeSrc;
fallbackFrame.width = '100%';
fallbackFrame.style.border = '0';
fallbackFrame.style.minHeight = '250px';
fallbackFrame.setAttribute('scrolling', 'no');
fallbackFrame.setAttribute('title', 'advertisement');
var mountTarget = document.getElementById(adDivId) || adElement;
if (mountTarget && !mountTarget.contains(fallbackFrame)) {
mountTarget.innerHTML = '';
mountTarget.appendChild(fallbackFrame);
if (adElement) {
adElement.setAttribute('data-ad-final-status', 'loaded');
adElement.classList.add('ad-loaded');
}
}
console.log('✅ 瀑布流廣告使用 iframe 備援:', adDivId, adIframeSrc);
} else {
// 🔥 沒有 adAdunit 也沒有 iframeSrc:嘗試使用預設配置(針對 bottom ad)
if (adKey === 'bottom_1' || adKey === 'bottom_2') {
console.error('❌ [Bottom Ad] 後台配置缺失且無 iframe 備援:', adKey, {
adAdunit: adAdunit,
adAdsize: adAdsize,
adIframeSrc: adIframeSrc,
建議: '檢查後台 bottom_1/bottom_2 配置是否正確'
});
// 🔧 最後手段:使用預設的 GAM 分類單元
if (typeof waterfallConfig !== 'undefined' && waterfallConfig.categoryNameGam) {
var gamCategory = waterfallConfig.categoryNameGam;
var fallbackAtlIndex = waterfallArticleIndex > 0 ? ((waterfallArticleIndex - 1) % 4) + 1 : 1;
var fallbackAdUnit = '/21697024903,65167615/juksy_' + gamCategory + '_content_bottom_ad1_atl' + fallbackAtlIndex;
var fallbackAdSize = [[336, 280], [300, 250], [250, 250], [200, 200], [1, 1]];
console.warn('⚠️ [Bottom Ad Fallback] 使用緊急預設配置:', {
fallbackAdUnit: fallbackAdUnit,
fallbackAdSize: fallbackAdSize,
gamCategory: gamCategory
});
// 使用預設配置呼叫 GAM
if (typeof googletag !== 'undefined') {
googletag.cmd.push(function() {
var slot = googletag.defineSlot(fallbackAdUnit, fallbackAdSize, adDivId);
if (slot) {
slot.addService(googletag.pubads());
googletag.display(adDivId);
googletag.pubads().refresh([slot]);
console.log('🚨 [Bottom Ad Fallback] 已載入緊急預設廣告:', adDivId, fallbackAdUnit);
}
});
}
return;
}
}
console.error('❌ 廣告資訊不完整,無法顯示:', adDivId, { adAdunit, adAdsize });
if (adElement) {
adElement.setAttribute('data-ad-final-status', 'error');
}
}
} catch (error) {
console.error('❌ 瀑布流廣告初始化錯誤:', error, adKey);
if (adElement) {
adElement.setAttribute('data-ad-final-status', 'error');
}
}
}
// 重新計算瀑布流容器高度 - 廣告載入後調整
function recalculateWaterfallContainerHeight(articleId) {
// 找到對應的瀑布流容器
var waterfallContainer = document.querySelector('[data-article-id="' + articleId + '"]');
if (!waterfallContainer) {
console.log('找不到瀑布流容器,無法重新計算高度');
return;
}
// 等待DOM更新完成後測量實際高度
requestAnimationFrame(function() {
var actualHeight = waterfallContainer.scrollHeight;
var currentHeight = waterfallContainer.offsetHeight;
console.log('瀑布流容器高度重算:', articleId, '當前:', currentHeight, '實際:', actualHeight);
// 如果實際高度比當前高度大,更新容器高度
if (actualHeight > currentHeight) {
waterfallContainer.style.height = actualHeight + 'px';
waterfallContainer.style.minHeight = actualHeight + 'px';
console.log('瀑布流容器高度已更新:', articleId, actualHeight + 'px');
// 強制重繪確保footer被正確推下
waterfallContainer.offsetHeight;
// 通知其他元素容器高度已變更
var event = new CustomEvent('waterfallHeightChanged', {
detail: { articleId: articleId, newHeight: actualHeight }
});
document.dispatchEvent(event);
} else {
console.log('瀑布流容器高度無需調整:', articleId);
}
});
}
// 在 virtual_pageview 事件中修改標題處理邏輯
function processWaterfallTitle(title) {
// 確保標題格式統一
if (title.endsWith(' - JUKSY 街星')) {
return title; // 已經有正確格式
} else if (title.endsWith('JUKSY 街星')) {
return title.replace(/JUKSY 街星$/, ' - JUKSY 街星'); // 補上 " - "
} else {
return title + ' - JUKSY 街星'; // 完全沒有後綴
}
}
// 🔥 瀑布流廣告 SSP Passback 機制
function setupWaterfallAdPassback() {
var isMobile = window.innerWidth <= 768;
if (!isMobile) return; // 只處理手機版
console.log('🔧 [瀑布流 Passback] 開始設定 SSP passback 機制');
// 找出所有瀑布流廣告容器
var waterfallAdContainers = document.querySelectorAll('.adContent.mobile[data-waterfall-initialized="1"]');
console.log('🔧 [瀑布流 Passback] 找到', waterfallAdContainers.length, '個已初始化的瀑布流廣告');
waterfallAdContainers.forEach(function(adContainer, index) {
var gamContainer = adContainer.querySelector('[id^="juksy_m_"]');
if (!gamContainer) return;
var gamContainerId = gamContainer.id;
var adKey = adContainer.getAttribute('ad-internal') || adContainer.getAttribute('ad-key');
// 🔥 檢查額外廣告開關(如果是 cover/sticky 且開關關閉,跳過 passback)
if (typeof waterfallConfig !== 'undefined' && waterfallConfig.extraAds) {
if ((adKey === 'cover' || adKey.includes('takeover')) && !waterfallConfig.extraAds.cover) {
console.log('⏭️ [瀑布流 Passback] Cover 廣告已關閉,跳過 passback:', adKey);
return;
}
if (adKey === 'sticky' && !waterfallConfig.extraAds.sticky) {
console.log('⏭️ [瀑布流 Passback] Sticky 廣告已關閉,跳過 passback:', adKey);
return;
}
}
// 檢查是否已設定過 passback
if (adContainer.getAttribute('data-passback-initialized') === '1') {
console.log('⏭️ [瀑布流 Passback] 已設定過,跳過:', adKey);
return;
}
adContainer.setAttribute('data-passback-initialized', '1');
console.log('🎯 [瀑布流 Passback #' + index + '] 設定廣告:', adKey, 'container:', gamContainerId);
// 5 秒後檢查 SSP 是否填充
setTimeout(function() {
var hasContent = gamContainer.children.length > 0 &&
gamContainer.querySelector('iframe, div[id*="google_ads"]');
if (!hasContent) {
console.warn('⚠️ [瀑布流 Passback] SSP 未填充:', adKey, '- 嘗試主動呼叫 GAM');
// 🔥 主動定義並顯示 GAM 廣告
googletag.cmd.push(function() {
var adUnit = '/21697024903,65167615/' + gamContainerId;
var adSizes = [[300, 250], [336, 280], [320, 100], [1, 1]];
// 檢查是否已定義
var existingSlots = googletag.pubads().getSlots();
var alreadyDefined = existingSlots.some(function(s) {
return s.getSlotElementId() === gamContainerId;
});
if (!alreadyDefined) {
googletag.defineSlot(adUnit, adSizes, gamContainerId)
.addService(googletag.pubads());
if (typeof ad_category !== 'undefined') {
googletag.pubads().setTargeting("category", ad_category);
}
if (typeof juksy_category !== 'undefined') {
googletag.pubads().setTargeting("juksy_category", juksy_category);
}
if (typeof juksy_subcategory !== 'undefined') {
googletag.pubads().setTargeting("juksy_subcategory", juksy_subcategory);
}
console.log('✅ [瀑布流 Passback] GAM 廣告已定義:', adUnit);
}
setTimeout(function() {
googletag.display(gamContainerId);
console.log('✅ [瀑布流 Passback] GAM 廣告已呼叫 display:', gamContainerId);
}, 100);
});
} else {
console.log('✅ [瀑布流 Passback] 廣告已載入:', adKey);
}
}, 5000);
});
}
// 在瀑布流文章載入後執行 passback 設定
document.addEventListener('waterfallArticleLoaded', function(e) {
setTimeout(setupWaterfallAdPassback, 1000); // 等待廣告初始化完成
});
// 頁面載入時也執行一次(處理首次載入的文章)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(setupWaterfallAdPassback, 2000);
});
} else {
setTimeout(setupWaterfallAdPassback, 2000);
}