fixed 重写弹幕逻辑

This commit is contained in:
Vaica committed 2026-01-29 21:05:27 +08:00
1 parent 7de8d0e253
commit 5b7569fe40
2 files changed
+298 -200

No files matched your search

+291 -198
View File
@@ -1,51 +1,121 @@
// 弹幕功能封装 // 弹幕功能封装
(function() { (function() {
// 异步初始化 Artalk 并获取评论数据 let barrageInstance = null; // 单例实例引用
function initArtalkAndGetComments() {
return new Promise((resolve, reject) => { // 获取 Artalk 评论数据
// 检查 Artalk 是否存在,增加轮询等待机制 function getArtalkComments() {
if (typeof Artalk === 'undefined') { return new Promise((resolve) => {
let attempts = 0; let isResolved = false;
const maxAttempts = 50; // 最多等待 5 秒
const interval = setInterval(() => { const tryResolve = (data, source) => {
attempts++; if (isResolved) return;
if (typeof Artalk !== 'undefined') {
clearInterval(interval); // 调试日志
Artalk.use((ctx) => { if (data && Array.isArray(data)) {
ctx.on('list-loaded', () => { console.log(`[Barrage] 数据检查 (${source}): ${data.length} 条`);
console.log('评论列表加载完毕'); } else {
const commentsData = ctx.getComments(); console.log(`[Barrage] 数据检查 (${source}): 无效数据`, data);
if (commentsData) { }
resolve(commentsData);
} else { if (Array.isArray(data) && data.length > 0) {
reject('获取评论数据失败'); isResolved = true;
} resolve(data);
}); }
}); };
} else if (attempts >= maxAttempts) {
clearInterval(interval); // 1. 立即检查
console.warn('Artalk 加载超时'); if (window.artalk && typeof window.artalk.getComments === 'function') {
reject('Artalk 未定义 (加载超时)'); tryResolve(window.artalk.getComments(), 'immediate');
} } else {
}, 100); console.log('[Barrage] Artalk 实例未就绪 (Immediate)');
return;
} }
Artalk.use((ctx) => { if (isResolved) return;
ctx.on('list-loaded', () => {
console.log('评论列表加载完毕'); // 2. 监听加载完成事件 (推荐)
const commentsData = ctx.getComments(); if (window.artalk && typeof window.artalk.on === 'function') {
if (commentsData) { window.artalk.on('list-loaded', (data) => {
resolve(commentsData); console.log('[Barrage] list-loaded 事件触发');
} else { // 优先使用事件传递的数据(如果有),否则调用 getComments
reject('获取评论数据失败'); 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 { class BarrageEngine {
constructor(options = {}) { constructor(options = {}) {
this.container = typeof options.container === 'string' this.container = typeof options.container === 'string'
@@ -62,238 +132,261 @@
this.timer = null; this.timer = null;
this.isPaused = false; this.isPaused = false;
this.isDestroyed = false; this.isDestroyed = false;
// 追踪当前屏幕上显示的弹幕 ID
this.activeIds = new Set();
} }
// 开始发送
start(data = []) { start(data = []) {
console.log(`[Barrage] 启动弹幕,共 ${data.length} 条`); this.stop();
this.data = data; 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; if (this.data.length === 0) return;
this.clear();
this.isDestroyed = false;
this.loop(); this.loop();
} }
// 清空 stop() {
clear() { if (this.timer) {
this.isDestroyed = true; clearTimeout(this.timer);
if (this.timer) clearTimeout(this.timer); this.timer = null;
// 清除容器内所有弹幕元素
if (this.container) {
const items = this.container.querySelectorAll('.barrage-item');
items.forEach(item => item.remove());
} }
this.index = 0; this.activeIds.clear();
}
clearDOM() {
if (this.container) {
this.container.innerHTML = '';
}
}
destroy() {
this.isDestroyed = true;
this.stop();
this.clearDOM();
} }
// 循环发射
loop() { loop() {
if (this.isDestroyed) return; if (this.isDestroyed) return;
// 如果容器不在文档中(PJAX切换后),停止播放 // 容器检测
if (!document.body.contains(this.container)) { if (!document.body.contains(this.container)) {
console.warn('[Barrage] 容器失效,停止播放'); // 尝试重新获取容器 (可能被 PJAX 替换但 ID 没变?)
// 这里简单处理:如果失效则停止
// console.warn('[Barrage] 容器失效,停止');
this.destroy();
return; return;
} }
// 发射逻辑 if (!this.isPaused && this.data.length > 0) {
if (!this.isPaused) { this.tryShoot();
const itemData = this.data[this.index];
this.shoot(itemData);
this.index = (this.index + 1) % this.data.length;
} }
// 随机间隔 1s - 3s // 随机间隔 2s - 4s
const interval = 1000 + Math.random() * 2000; const interval = 2000 + Math.random() * 2000;
this.timer = setTimeout(() => this.loop(), interval); 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) { shoot(itemData) {
const { text, avatar, url, id } = itemData; const { text, avatar, url, id } = itemData;
this.activeIds.add(id);
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'barrage-item'; item.className = 'barrage-item';
// 构造内部 HTML 结构:头像 + 气泡
let html = ''; let html = '';
// 1. 头像
if (avatar) { if (avatar) {
html += `<img src="${avatar}" class="barrage-avatar" draggable="false">`; html += `<img src="${avatar}" class="barrage-avatar" draggable="false">`;
} }
// 2. 气泡 (包裹文本)
html += `<div class="barrage-bubble"><span class="barrage-text">${text}</span></div>`; html += `<div class="barrage-bubble"><span class="barrage-text">${text}</span></div>`;
item.innerHTML = html; 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 !== '#') { if (url && url !== '#') {
item.addEventListener('click', jumpHandler);
item.style.cursor = 'pointer'; 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 tracks = 10;
const trackHeight = 100 / tracks;
const trackIndex = Math.floor(Math.random() * tracks); const trackIndex = Math.floor(Math.random() * tracks);
const top = trackIndex * trackHeight + (Math.random() * 2); const top = 5 + (trackIndex * (90 / tracks));
item.style.top = `${top}%`; item.style.top = `${top}%`;
// 随机速度 (18s - 25s) - 更慢一点,更自然
const duration = 18 + Math.random() * 7; const duration = 18 + Math.random() * 7;
item.style.animation = `barrage-slide-right ${duration}s linear forwards`; item.style.animation = `barrage-slide-right ${duration}s linear forwards`;
// 鼠标悬停暂停该弹幕
item.addEventListener('mouseenter', () => { item.addEventListener('mouseenter', () => {
item.style.animationPlayState = 'paused'; item.style.animationPlayState = 'paused';
item.style.zIndex = '99999'; item.style.zIndex = '9999';
}); });
item.addEventListener('mouseleave', () => { item.addEventListener('mouseleave', () => {
item.style.animationPlayState = 'running'; item.style.animationPlayState = 'running';
item.style.zIndex = '10'; item.style.zIndex = '';
}); });
// 动画结束移除
item.addEventListener('animationend', () => { item.addEventListener('animationend', () => {
item.remove(); item.remove();
this.activeIds.delete(id);
}); });
this.container.appendChild(item); 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() { function isMobile() {
if (window.innerWidth < 768) { return window.innerWidth < 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
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);
} }
// 加载弹幕主函数 // 主加载函数
async function loadBarrager() { async function loadBarrager() {
// 移动端不加载弹幕 // 移动端禁用
if (isMobile()) { if (isMobile()) {
console.log('[Barrage] 移动端已禁用弹幕'); console.log('[Barrage] 移动端禁用');
if (barrageInstance) barrageInstance.destroy();
return; 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 { try {
const commentsData = await initArtalkAndGetComments(); const config = window.barrageConfig || {};
if (commentsData) { const promises = [getArtalkComments()];
await createBarragerFromArtalk(commentsData);
// 如果开启了 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; window.loadBarrager = loadBarrager;
// 初始化监听 // 初始化
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', () => {
loadBarrager(); // 页面首次加载时启动弹幕 setTimeout(loadBarrager, 500);
}); });
// 移除内部的 pjax:complete 监听,统一由 mypjax.js 控制调用时机
// document.addEventListener('pjax:complete', function() {
// loadBarrager();
// });
})(); })();
+7 -2
View File
@@ -1,4 +1,9 @@
{{ if .Site.Params.barrage.enable }} {{ if .Site.Params.barrage.enable }}
<!-- 弹幕容器 (body 标签下) --> <script>
window.barrageConfig = {
enableHitokoto: {{ .Site.Params.barrage.enableHitokoto | default false }},
hitokotoUrl: "{{ .Site.Params.barrage.hitokotoUrl | default "https://v1.hitokoto.cn" }}"
};
</script>
<div class="barrager"></div> <div class="barrager"></div>
{{ end }} {{ end }}