// 弹幕功能封装
(function() {
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)');
}
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);
});
}
// 获取 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();
}
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)) {
// 尝试重新获取容器 (可能被 PJAX 替换但 ID 没变?)
// 这里简单处理:如果失效则停止
// console.warn('[Barrage] 容器失效,停止');
this.destroy();
return;
}
if (!this.isPaused && this.data.length > 0) {
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';
let html = '';
if (avatar) {
html += ``;
}
html += `