// 弹幕功能封装 (function() { let barrageInstance = null; // 单例实例引用 // 获取 Artalk 评论数据 function getArtalkComments() { return new Promise((resolve) => { let isResolved = false; // 安全获取评论列表的辅助函数 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 (data && Array.isArray(data)) { console.log(`[Barrage] 数据检查 (${source}): ${data.length} 条`); } else { console.log(`[Barrage] 数据检查 (${source}): 无效数据`, data); } if (Array.isArray(data) && data.length > 0) { isResolved = true; resolve(data); } }; // 1. 立即检查 const immediateData = safeGetComments(); if (immediateData) { tryResolve(immediateData, 'immediate'); } else { console.log('[Barrage] Artalk 实例未就绪 (Immediate)'); } if (isResolved) return; // 2. 监听加载完成事件 (推荐) if (window.artalk && typeof window.artalk.on === 'function') { window.artalk.on('list-loaded', (data) => { console.log('[Barrage] list-loaded 事件触发'); // 优先使用事件传递的数据(如果有),否则尝试主动获取 const comments = (Array.isArray(data) && data.length > 0) ? data : (safeGetComments() || []); tryResolve(comments, 'event'); }); } // 3. 轮询 (后备) let pollCount = 0; const interval = setInterval(() => { if (isResolved) { clearInterval(interval); return; } pollCount++; const pollData = safeGetComments(); if (pollData && Array.isArray(pollData) && pollData.length > 0) { tryResolve(pollData, `poll-${pollCount}`); } }, 500); // 4. 超时处理 (5秒) setTimeout(() => { if (!isResolved) { clearInterval(interval); console.warn('[Barrage] 等待评论数据超时 (5s)'); // 尝试从 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 || ''; // atk-comment-123 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 : '', // 模拟 Artalk 数据结构 ua: null }; }).filter(Boolean); if (fallbackData.length > 0) { isResolved = true; resolve(fallbackData); return; } } resolve([]); // 真的没有数据 } }, 5000); }); } // 获取 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; // 数据预处理:去重 & 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; console.log(`[Barrage] 启动: 原始 ${data.length}, 去重后 ${this.data.length}`); if (this.data.length === 0) return; this.loop(); } stop() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } this.activeIds.clear(); } clearDOM() { if (this.container) { this.container.innerHTML = ''; } } destroy() { this.isDestroyed = true; this.stop(); this.clearDOM(); // 移除事件监听需要保存引用,这里因为容器可能被销毁,且是单例,暂不处理复杂的解绑 } loop() { if (this.isDestroyed) return; // 容器检测 if (!document.body.contains(this.container)) { this.destroy(); return; } if (!this.isPaused && 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() { 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 标签 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; } // 1. 获取容器,如果没有则动态创建 let container = document.querySelector('.barrager'); if (!container) { console.warn('[Barrage] 未找到 .barrager 容器,尝试动态创建'); container = document.createElement('div'); container.className = 'barrager'; document.body.appendChild(container); } // 2. 清理旧实例 if (barrageInstance) { barrageInstance.destroy(); barrageInstance = null; } 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 = ''; const domComment = document.getElementById(`atk-comment-${c.id}`); if (domComment) { const img = domComment.querySelector('.atk-avatar img'); if (img && img.src) avatar = img.src; } if (!avatar) { avatar = c.avatar || (c.ua && c.ua.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.destroy(); } 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); }); })();