diff --git a/themes/Ying/assets/js/barrage.js b/themes/Ying/assets/js/barrage.js index 34b45566..f7d789b3 100644 --- a/themes/Ying/assets/js/barrage.js +++ b/themes/Ying/assets/js/barrage.js @@ -1,51 +1,121 @@ // 弹幕功能封装 (function() { - // 异步初始化 Artalk 并获取评论数据 - function initArtalkAndGetComments() { - return new Promise((resolve, reject) => { - // 检查 Artalk 是否存在,增加轮询等待机制 - if (typeof Artalk === 'undefined') { - let attempts = 0; - const maxAttempts = 50; // 最多等待 5 秒 - const interval = setInterval(() => { - attempts++; - if (typeof Artalk !== 'undefined') { - clearInterval(interval); - Artalk.use((ctx) => { - ctx.on('list-loaded', () => { - console.log('评论列表加载完毕'); - const commentsData = ctx.getComments(); - if (commentsData) { - resolve(commentsData); - } else { - reject('获取评论数据失败'); - } - }); - }); - } else if (attempts >= maxAttempts) { - clearInterval(interval); - console.warn('Artalk 加载超时'); - reject('Artalk 未定义 (加载超时)'); - } - }, 100); - return; + let barrageInstance = null; // 单例实例引用 + + // 获取 Artalk 评论数据 + function getArtalkComments() { + return new Promise((resolve) => { + let isResolved = false; + + 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. 立即检查 + if (window.artalk && typeof window.artalk.getComments === 'function') { + tryResolve(window.artalk.getComments(), 'immediate'); + } else { + console.log('[Barrage] Artalk 实例未就绪 (Immediate)'); } - Artalk.use((ctx) => { - ctx.on('list-loaded', () => { - console.log('评论列表加载完毕'); - const commentsData = ctx.getComments(); - if (commentsData) { - resolve(commentsData); - } else { - reject('获取评论数据失败'); - } + if (isResolved) return; + + // 2. 监听加载完成事件 (推荐) + if (window.artalk && typeof window.artalk.on === 'function') { + window.artalk.on('list-loaded', (data) => { + console.log('[Barrage] list-loaded 事件触发'); + // 优先使用事件传递的数据(如果有),否则调用 getComments + const comments = (Array.isArray(data) && data.length > 0) ? data : window.artalk.getComments(); + tryResolve(comments, 'event'); }); - }); + } + + // 3. 轮询 (后备) + let pollCount = 0; + const interval = setInterval(() => { + if (isResolved) { + clearInterval(interval); + return; + } + pollCount++; + if (window.artalk && typeof window.artalk.getComments === 'function') { + // 仅在有数据时才 resolve,避免轮询到空数据提前结束 + const data = window.artalk.getComments(); + if (Array.isArray(data) && data.length > 0) { + tryResolve(data, `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); }); } - // 弹幕类 (基于 min-barrage 逻辑) + // 获取 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' @@ -62,238 +132,261 @@ this.timer = null; this.isPaused = false; this.isDestroyed = false; + + // 追踪当前屏幕上显示的弹幕 ID + this.activeIds = new Set(); } - // 开始发送 start(data = []) { - console.log(`[Barrage] 启动弹幕,共 ${data.length} 条`); - this.data = 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.clear(); - this.isDestroyed = false; this.loop(); } - // 清空 - clear() { - this.isDestroyed = true; - if (this.timer) clearTimeout(this.timer); - // 清除容器内所有弹幕元素 - if (this.container) { - const items = this.container.querySelectorAll('.barrage-item'); - items.forEach(item => item.remove()); + stop() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; } - this.index = 0; + this.activeIds.clear(); + } + + clearDOM() { + if (this.container) { + this.container.innerHTML = ''; + } + } + + destroy() { + this.isDestroyed = true; + this.stop(); + this.clearDOM(); } - // 循环发射 loop() { if (this.isDestroyed) return; - // 如果容器不在文档中(PJAX切换后),停止播放 + // 容器检测 if (!document.body.contains(this.container)) { - console.warn('[Barrage] 容器失效,停止播放'); + // 尝试重新获取容器 (可能被 PJAX 替换但 ID 没变?) + // 这里简单处理:如果失效则停止 + // console.warn('[Barrage] 容器失效,停止'); + this.destroy(); return; } - // 发射逻辑 - if (!this.isPaused) { - const itemData = this.data[this.index]; - this.shoot(itemData); - this.index = (this.index + 1) % this.data.length; + if (!this.isPaused && this.data.length > 0) { + this.tryShoot(); } - // 随机间隔 1s - 3s - const interval = 1000 + Math.random() * 2000; + // 随机间隔 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'; - // 构造内部 HTML 结构:头像 + 气泡 let html = ''; - - // 1. 头像 if (avatar) { html += ``; } - - // 2. 气泡 (包裹文本) html += `
${text}
`; - item.innerHTML = html; - // 点击跳转 (通用处理) - const jumpHandler = (e) => { - e.stopPropagation(); - e.preventDefault(); - console.log(`[Barrage] 点击跳转: ${url}`); - - // 检查目标评论是否在当前页面 - 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; - } - }; - if (url && url !== '#') { - item.addEventListener('click', jumpHandler); item.style.cursor = 'pointer'; + item.addEventListener('click', (e) => { + 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; + } + }); } - // 随机轨道 (Top 5% - 95%) - 避免太靠边 - const tracks = 15; - const trackHeight = 100 / tracks; + // 样式与动画 + const tracks = 10; const trackIndex = Math.floor(Math.random() * tracks); - const top = trackIndex * trackHeight + (Math.random() * 2); + const top = 5 + (trackIndex * (90 / tracks)); item.style.top = `${top}%`; - - // 随机速度 (18s - 25s) - 更慢一点,更自然 + const duration = 18 + Math.random() * 7; - item.style.animation = `barrage-slide-right ${duration}s linear forwards`; - - // 鼠标悬停暂停该弹幕 + item.addEventListener('mouseenter', () => { item.style.animationPlayState = 'paused'; - item.style.zIndex = '99999'; + item.style.zIndex = '9999'; }); item.addEventListener('mouseleave', () => { item.style.animationPlayState = 'running'; - item.style.zIndex = '10'; + item.style.zIndex = ''; }); - // 动画结束移除 item.addEventListener('animationend', () => { item.remove(); + this.activeIds.delete(id); }); this.container.appendChild(item); } } - // 挂载到 window,使用 BarrageEngine 避免冲突 - window.BarrageEngine = BarrageEngine; - - // 从 Artalk 评论数据生成弹幕 - async function createBarragerFromArtalk(commentsData) { - const barragerContainer = document.querySelector('.barrager'); - if (!barragerContainer) { - console.warn('[Barrage] 未找到弹幕容器 .barrager'); - return; - } - - // 1. 数据预处理 - const barrageList = commentsData.map(comment => { - // 清理 HTML 标签 - const contentText = comment.content.replace(/<[^>]+>/g, "").substring(0, 50); - - // 获取头像逻辑优化: 优先从 DOM 获取 Artalk 已渲染的头像,其次使用 API 数据 - let avatarUrl = ''; - - // 尝试从 DOM 获取 (最准确,因为是 Artalk 自己渲染的) - const commentEl = document.getElementById(`atk-comment-${comment.id}`); - if (commentEl) { - const imgEl = commentEl.querySelector('.atk-avatar img'); - if (imgEl && imgEl.src) { - avatarUrl = imgEl.src; - } - } - - // 如果 DOM 没取到,尝试从数据获取 - if (!avatarUrl) { - if (comment.avatar) { - avatarUrl = comment.avatar; - } else if (comment.ua && comment.ua.avatar) { - avatarUrl = comment.ua.avatar; - } else { - avatarUrl = 'https://cravatar.cn/avatar/default?d=mp'; - } - } - - return { - text: contentText, - avatar: avatarUrl, - url: `#atk-comment-${comment.id}`, // 跳转锚点 - id: comment.id - }; - }).filter(item => item.text.trim() !== ""); - - if (barrageList.length === 0) { - console.log('[Barrage] 没有有效的评论弹幕数据'); - return; - } - - // 2. 初始化 Barrage 实例 - // 挂载到 window 以便调试或清理 - if (window.myBarrage) { - window.myBarrage.clear(); - } - - // 使用 window.BarrageEngine - if (window.BarrageEngine) { - window.myBarrage = new window.BarrageEngine({ - container: barragerContainer - }); - window.myBarrage.start(barrageList); - } else { - console.error('[Barrage] window.BarrageEngine 未定义'); - } - } - - // 判断是否为移动端 + // 辅助:判断移动端 function isMobile() { - if (window.innerWidth < 768) { - return true; - } - return window.navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i); + 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] 移动端已禁用弹幕'); + 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 commentsData = await initArtalkAndGetComments(); - if (commentsData) { - await createBarragerFromArtalk(commentsData); + const config = window.barrageConfig || {}; + const promises = [getArtalkComments()]; + + // 如果开启了 Hitokoto,则并发获取 + if (config.enableHitokoto && config.hitokotoUrl) { + promises.push(fetchHitokoto(config.hitokotoUrl)); } - } catch (error) { - console.error('加载弹幕时发生错误:', error); + + // 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: '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); } } - - // 暴露给全局,供 mypjax.js 调用 + + // 暴露全局 window.loadBarrager = loadBarrager; - // 初始化监听 - document.addEventListener('DOMContentLoaded', function() { - loadBarrager(); // 页面首次加载时启动弹幕 + // 初始化 + document.addEventListener('DOMContentLoaded', () => { + setTimeout(loadBarrager, 500); }); - // 移除内部的 pjax:complete 监听,统一由 mypjax.js 控制调用时机 - // document.addEventListener('pjax:complete', function() { - // loadBarrager(); - // }); - })(); diff --git a/themes/Ying/layouts/partials/barrage.html b/themes/Ying/layouts/partials/barrage.html index 81018448..17c02fc4 100644 --- a/themes/Ying/layouts/partials/barrage.html +++ b/themes/Ying/layouts/partials/barrage.html @@ -1,4 +1,9 @@ {{ if .Site.Params.barrage.enable }} - +
-{{ end }} +{{ end }} \ No newline at end of file