// 弹幕功能封装 (function() { let barrageInstance = null; // 单例实例引用 // 获取 Artalk 评论数据 function getArtalkComments() { return new Promise((resolve) => { let isResolved = false; let interval = null; // 安全获取评论列表的辅助函数 const safeGetComments = () => { if (!window.artalk) return null; if (typeof window.artalk.getComments === 'function') return window.artalk.getComments(); if (typeof window.artalk.getCommentList === 'function') return window.artalk.getCommentList(); // 尝试访问内部数据 (Artalk v2 hack) if (window.artalk.ctx && window.artalk.ctx.get('list') && typeof window.artalk.ctx.get('list').getComments === 'function') { return window.artalk.ctx.get('list').getComments(); } return null; }; const tryResolve = (data, source) => { if (isResolved) return; if (Array.isArray(data)) { // 如果是事件触发(说明已加载完成),或者是轮询且有数据,则认为成功 if (source === 'event' || data.length > 0) { console.log(`[Barrage] 数据获取成功 (${source}): ${data.length} 条`); isResolved = true; if (interval) clearInterval(interval); resolve(data); } } }; // 检查 Artalk 实例并尝试获取数据 const checkArtalk = () => { if (isResolved) return; // 如果当前页面没有评论区,不要等待 Artalk 初始化,直接返回空 // 这样可以避免在无评论页(如首页)长时间等待超时 const hasCommentSection = document.getElementById('artalk') || document.getElementById('Comments') || document.querySelector('.atk-comment-wrap'); if (!hasCommentSection) { // 延迟一点点返回,确保逻辑统一 setTimeout(() => { tryResolve([], 'no-comment-section'); }, 100); return; } if (window.artalk) { // 1. 尝试绑定事件 (只绑定一次) if (!window.artalk._barrageListenerAttached && typeof window.artalk.on === 'function') { window.artalk.on('list-loaded', (data) => { tryResolve(data, 'event'); }); window.artalk._barrageListenerAttached = true; console.log('[Barrage] Artalk 事件监听已绑定'); } // 2. 尝试直接获取数据 const data = safeGetComments(); if (data) { tryResolve(data, 'poll'); } } }; // 立即检查 checkArtalk(); // 轮询检查 (直到获取数据或超时) // 增加轮询频率,以便更快响应 Artalk 初始化 interval = setInterval(checkArtalk, 200); // 超时处理 (8秒) setTimeout(() => { if (!isResolved) { if (interval) clearInterval(interval); console.warn('[Barrage] 获取数据超时'); // 最后的尝试:DOM 兜底 (适用于静态页面或 API 失败但 DOM 已渲染的情况) const domComments = document.querySelectorAll('.atk-comment-wrap'); if (domComments.length > 0) { console.log(`[Barrage] 从 DOM 挽救数据: ${domComments.length} 条`); const fallbackData = Array.from(domComments).map(el => { const idStr = el.id || ''; const id = idStr.replace('atk-comment-', ''); const contentEl = el.querySelector('.atk-content'); const avatarEl = el.querySelector('.atk-avatar img'); if (!id || !contentEl) return null; return { id: id, content: contentEl.innerText || contentEl.textContent, avatar: avatarEl ? avatarEl.src : '', ua: null }; }).filter(Boolean); if (fallbackData.length > 0) { resolve(fallbackData); return; } } resolve([]); } }, 8000); }); } // 获取 Hitokoto 一言 async function fetchHitokoto(url) { try { const res = await fetch(url); if (!res.ok) throw new Error('Network response was not ok'); const data = await res.json(); return { id: `hitokoto-${data.id}`, content: data.hitokoto, from: data.from }; } catch (e) { console.warn('[Barrage] Hitokoto fetch failed:', e); return null; } } class BarrageEngine { constructor(options = {}) { this.container = typeof options.container === 'string' ? document.querySelector(options.container) : options.container; if (!this.container) { console.warn('[Barrage] 错误: 找不到弹幕容器'); return; } this.data = []; this.index = 0; this.timer = null; this.isPaused = false; this.isDestroyed = false; // 追踪当前屏幕上显示的弹幕 ID this.activeIds = new Set(); // 最大同屏数量限制,防止卡顿 this.maxActiveItems = 30; // 绑定事件委托 this.initEventDelegation(); } initEventDelegation() { // 鼠标进入容器暂停 this.container.addEventListener('mouseover', (e) => { const item = e.target.closest('.barrage-item'); if (item) { item.style.animationPlayState = 'paused'; item.style.zIndex = '9999'; } }); // 鼠标离开恢复 this.container.addEventListener('mouseout', (e) => { const item = e.target.closest('.barrage-item'); if (item) { item.style.animationPlayState = 'running'; item.style.zIndex = ''; } }); // 点击事件 this.container.addEventListener('click', (e) => { const item = e.target.closest('.barrage-item'); if (item) { const url = item.dataset.url; if (url && url !== '#') { e.stopPropagation(); e.preventDefault(); const targetId = url.split('#')[1]; const targetEl = document.getElementById(targetId); if (targetEl) { targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); targetEl.classList.add('atk-flash'); setTimeout(() => targetEl.classList.remove('atk-flash'), 2000); } else { window.location.href = url; } } } }); } start(data = []) { this.stop(); this.clearDOM(); this.isDestroyed = false; this.updateData(data); } updateData(data = []) { // 数据预处理:去重 & ID 规范化 const uniqueData = []; const seenIds = new Set(); data.forEach(item => { const id = String(item.id); if (!seenIds.has(id)) { seenIds.add(id); uniqueData.push({ ...item, id: id }); } }); this.data = uniqueData; // 如果数据更新时列表变短了,重置索引以防越界 if (this.index >= this.data.length) { this.index = 0; } console.log(`[Barrage] 数据更新: ${this.data.length} 条`); if (this.data.length > 0 && !this.timer && !this.isPaused) { this.loop(); } } hide() { if (this.container) { this.container.style.opacity = '0'; this.container.style.pointerEvents = 'none'; } this.isPaused = true; } show() { if (this.container) { this.container.style.opacity = '1'; this.container.style.pointerEvents = 'auto'; } this.isPaused = false; if (!this.timer) this.loop(); } stop() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } // 注意:不清除 activeIds,允许已发射的弹幕飞完 } clearDOM() { if (this.container) { this.container.innerHTML = ''; } this.activeIds.clear(); } destroy() { this.isDestroyed = true; this.stop(); this.clearDOM(); } loop() { if (this.isDestroyed || this.isPaused) return; // 容器检测 if (!document.body.contains(this.container)) { this.destroy(); return; } if (this.data.length > 0) { // 只有当前屏幕数量未满时才发射 if (this.activeIds.size < this.maxActiveItems) { this.tryShoot(); } } // 随机间隔 2s - 4s const interval = 2000 + Math.random() * 2000; this.timer = setTimeout(() => this.loop(), interval); } tryShoot() { if (this.data.length === 0) return; let attempts = 0; const maxAttempts = this.data.length; while (attempts < maxAttempts) { const item = this.data[this.index]; this.index = (this.index + 1) % this.data.length; if (!this.activeIds.has(item.id)) { this.shoot(item); return; } attempts++; } } shoot(itemData) { const { text, avatar, url, id } = itemData; this.activeIds.add(id); const item = document.createElement('div'); item.className = 'barrage-item'; // 存储 URL 到 dataset 以供事件委托使用 if (url && url !== '#') { item.dataset.url = url; item.style.cursor = 'pointer'; } let html = ''; // 优化:只有当有头像时才渲染 img 标签 // 移除 loading="lazy" 以修复弹幕头像加载问题 if (avatar) { html += ``; } html += `
${text}
`; item.innerHTML = html; // 样式与动画 const tracks = 10; const trackIndex = Math.floor(Math.random() * tracks); const top = 5 + (trackIndex * (90 / tracks)); item.style.top = `${top}%`; const duration = 18 + Math.random() * 7; item.style.animation = `barrage-slide-right ${duration}s linear forwards`; // 仅绑定必要的生命周期事件 item.addEventListener('animationend', () => { item.remove(); this.activeIds.delete(id); }); this.container.appendChild(item); } } // 辅助:判断移动端 function isMobile() { return window.innerWidth < 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } // 主加载函数 async function loadBarrager() { // 移动端禁用 if (isMobile()) { console.log('[Barrage] 移动端禁用'); if (barrageInstance) barrageInstance.destroy(); return; } // 检测是否存在评论区 const hasCommentSection = document.getElementById('artalk') || document.getElementById('Comments') || document.querySelector('.atk-comment-wrap'); // 即使没有评论区,也不要隐藏弹幕,允许 Hitokoto 或上一页的数据继续播放 // if (!hasCommentSection) { // // console.log('[Barrage] 当前页面无评论区,隐藏弹幕'); // if (barrageInstance) { // barrageInstance.hide(); // } // return; // } // 1. 获取容器,如果没有则动态创建 let container = document.querySelector('.barrager'); if (!container) { console.warn('[Barrage] 未找到 .barrager 容器,尝试动态创建'); container = document.createElement('div'); container.className = 'barrager'; document.body.appendChild(container); } // 确保显示(无论是否有评论区,都允许弹幕存在,可能显示的是上一页的数据或 Hitokoto) if (barrageInstance) { barrageInstance.show(); } try { const config = window.barrageConfig || {}; const promises = [getArtalkComments()]; // 如果开启了 Hitokoto,则并发获取 if (config.enableHitokoto && config.hitokotoUrl) { promises.push(fetchHitokoto(config.hitokotoUrl)); } // 3. 获取数据 (支持事件监听 + 轮询 + Hitokoto) const results = await Promise.allSettled(promises); const barrageList = []; // 处理 Artalk 评论 if (results[0].status === 'fulfilled' && Array.isArray(results[0].value)) { const comments = results[0].value; const processedComments = comments.map(c => { const text = c.content.replace(/<[^>]+>/g, "").substring(0, 60); if (!text.trim()) return null; let avatar = ''; // 1. 优先尝试使用 email_encrypted 构造 Gravatar (适用于跨页面无 DOM 的情况) if (c.email_encrypted) { avatar = `https://cravatar.cn/avatar/${c.email_encrypted}?d=mp&s=240`; } else if (c.email_hash) { avatar = `https://cravatar.cn/avatar/${c.email_hash}?d=mp&s=240`; } // 2. 如果没有 hash,尝试从 API 对象直接获取 if (!avatar && c.avatar) { avatar = c.avatar; } // 3. 尝试从 UA 获取 if (!avatar && c.ua && c.ua.avatar) { avatar = c.ua.avatar; } // 4. 兜底默认头像 if (!avatar) { avatar = 'https://cravatar.cn/avatar/default?d=mp'; } return { id: c.id, text: text, avatar: avatar, url: `#atk-comment-${c.id}` }; }).filter(Boolean); barrageList.push(...processedComments); } // 处理 Hitokoto if (results[1] && results[1].status === 'fulfilled' && results[1].value) { const h = results[1].value; barrageList.push({ id: h.id, text: `${h.content}`, avatar: config.hitokotoAvatar || 'https://cravatar.cn/avatar/default?d=mp', url: '#' }); } if (barrageList.length === 0) { console.log('[Barrage] 无弹幕数据'); // 即使无数据,也不要销毁实例,保持空转即可 return; } // 更新实例或创建新实例 if (barrageInstance) { barrageInstance.updateData(barrageList); } else { console.log(`[Barrage] 初始化: ${barrageList.length} 条`); barrageInstance = new BarrageEngine({ container }); barrageInstance.start(barrageList); } } catch (e) { console.warn('[Barrage] 加载异常:', e); } } // 暴露全局 window.loadBarrager = loadBarrager; // 初始化 document.addEventListener('DOMContentLoaded', () => { setTimeout(loadBarrager, 500); }); // PJAX 适配 document.addEventListener('pjax:complete', () => { setTimeout(loadBarrager, 500); }); })();