// 弹幕功能封装 (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; // 优先获取 data-src (Artalk 懒加载),否则获取 src const avatarSrc = avatarEl ? (avatarEl.getAttribute('data-src') || avatarEl.src) : ''; return { id: id, content: contentEl.innerText || contentEl.textContent, avatar: avatarSrc, 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.pool = []; this.initPool(); // 绑定事件委托 this.initEventDelegation(); } initPool() { // 初始化对象池,预创建 DOM const fragment = document.createDocumentFragment(); for (let i = 0; i < this.maxActiveItems; i++) { const item = document.createElement('div'); item.className = 'barrage-item'; item.style.display = 'none'; // 初始隐藏 item.style.willChange = 'transform'; // 提示浏览器优化 // 预构建结构 const avatar = document.createElement('img'); avatar.className = 'barrage-avatar'; avatar.draggable = false; const bubble = document.createElement('div'); bubble.className = 'barrage-bubble'; const textSpan = document.createElement('span'); textSpan.className = 'barrage-text'; bubble.appendChild(textSpan); item.appendChild(avatar); item.appendChild(bubble); // 绑定动画结束事件(只需绑定一次) item.addEventListener('animationend', () => { this.recycle(item); }); this.pool.push({ el: item, avatarEl: avatar, textEl: textSpan, busy: false, id: null }); fragment.appendChild(item); } this.container.appendChild(fragment); } recycle(itemEl) { const poolItem = this.pool.find(p => p.el === itemEl); if (poolItem) { poolItem.busy = false; poolItem.el.style.display = 'none'; if (poolItem.id) { this.activeIds.delete(poolItem.id); poolItem.id = null; } } } 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(); // 不再清除 DOM,而是重置池子 this.resetPool(); this.isDestroyed = false; this.updateData(data); } resetPool() { this.pool.forEach(item => { item.busy = false; item.el.style.display = 'none'; item.id = null; if (item.animation) { item.animation.cancel(); item.animation = null; } }); this.activeIds.clear(); } 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; } } clearDOM() { // 兼容旧接口:实际上就是 resetPool this.resetPool(); } destroy() { this.isDestroyed = true; this.stop(); this.resetPool(); // 彻底销毁时才移除 DOM if (this.container) { this.container.innerHTML = ''; } } 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; // 查找空闲的池对象 const poolItem = this.pool.find(p => !p.busy); if (!poolItem) return; // 没有空闲对象,放弃本次发射 poolItem.busy = true; poolItem.id = id; this.activeIds.add(id); const { el, avatarEl, textEl } = poolItem; // 更新数据 if (url && url !== '#') { el.dataset.url = url; el.style.cursor = 'pointer'; } else { el.removeAttribute('data-url'); el.style.cursor = 'default'; } // 只有当有头像时才显示 img if (avatar) { avatarEl.src = avatar; avatarEl.style.display = 'block'; // 错误处理 avatarEl.onerror = function() { this.src = 'https://cravatar.cn/avatar/default?d=mp'; }; } else { avatarEl.style.display = 'none'; } textEl.innerText = text; // 计算轨道 const tracks = 10; const trackIndex = Math.floor(Math.random() * tracks); const top = 5 + (trackIndex * (90 / tracks)); el.style.top = `${top}%`; // 显示 el.style.display = 'inline-flex'; // 使用 Web Animations API (WAAPI) 替代 CSS Animation // 优势:无需触发 offsetHeight 重绘,控制更精准 const duration = (18 + Math.random() * 7) * 1000; // ms const keyframes = [ { transform: 'translateX(-100%)', opacity: 0, offset: 0 }, { opacity: 1, offset: 0.05 }, { opacity: 1, offset: 0.95 }, { transform: 'translateX(100vw)', opacity: 0, offset: 1 } ]; const animation = el.animate(keyframes, { duration: duration, easing: 'linear', fill: 'forwards' }); animation.onfinish = () => { this.recycle(el); }; poolItem.animation = animation; } } // 辅助:判断移动端 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); }); })();