300 lines
11 KiB
JavaScript
300 lines
11 KiB
JavaScript
// 弹幕功能封装
|
|
(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;
|
|
}
|
|
|
|
Artalk.use((ctx) => {
|
|
ctx.on('list-loaded', () => {
|
|
console.log('评论列表加载完毕');
|
|
const commentsData = ctx.getComments();
|
|
if (commentsData) {
|
|
resolve(commentsData);
|
|
} else {
|
|
reject('获取评论数据失败');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// 弹幕类 (基于 min-barrage 逻辑)
|
|
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;
|
|
}
|
|
|
|
// 开始发送
|
|
start(data = []) {
|
|
console.log(`[Barrage] 启动弹幕,共 ${data.length} 条`);
|
|
this.data = data;
|
|
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());
|
|
}
|
|
this.index = 0;
|
|
}
|
|
|
|
// 循环发射
|
|
loop() {
|
|
if (this.isDestroyed) return;
|
|
|
|
// 如果容器不在文档中(PJAX切换后),停止播放
|
|
if (!document.body.contains(this.container)) {
|
|
console.warn('[Barrage] 容器失效,停止播放');
|
|
return;
|
|
}
|
|
|
|
// 发射逻辑
|
|
if (!this.isPaused) {
|
|
const itemData = this.data[this.index];
|
|
this.shoot(itemData);
|
|
this.index = (this.index + 1) % this.data.length;
|
|
}
|
|
|
|
// 随机间隔 1s - 3s
|
|
const interval = 1000 + Math.random() * 2000;
|
|
this.timer = setTimeout(() => this.loop(), interval);
|
|
}
|
|
|
|
// 发射单条
|
|
shoot(itemData) {
|
|
const { text, avatar, url, id } = itemData;
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'barrage-item';
|
|
|
|
// 构造内部 HTML 结构:头像 + 气泡
|
|
let html = '';
|
|
|
|
// 1. 头像
|
|
if (avatar) {
|
|
html += `<img src="${avatar}" class="barrage-avatar" draggable="false">`;
|
|
}
|
|
|
|
// 2. 气泡 (包裹文本)
|
|
html += `<div class="barrage-bubble"><span class="barrage-text">${text}</span></div>`;
|
|
|
|
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';
|
|
}
|
|
|
|
// 随机轨道 (Top 5% - 95%) - 避免太靠边
|
|
const tracks = 15;
|
|
const trackHeight = 100 / tracks;
|
|
const trackIndex = Math.floor(Math.random() * tracks);
|
|
const top = trackIndex * trackHeight + (Math.random() * 2);
|
|
|
|
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.addEventListener('mouseleave', () => {
|
|
item.style.animationPlayState = 'running';
|
|
item.style.zIndex = '10';
|
|
});
|
|
|
|
// 动画结束移除
|
|
item.addEventListener('animationend', () => {
|
|
item.remove();
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
// 加载弹幕主函数
|
|
async function loadBarrager() {
|
|
// 移动端不加载弹幕
|
|
if (isMobile()) {
|
|
console.log('[Barrage] 移动端已禁用弹幕');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const commentsData = await initArtalkAndGetComments();
|
|
if (commentsData) {
|
|
await createBarragerFromArtalk(commentsData);
|
|
}
|
|
} catch (error) {
|
|
console.error('加载弹幕时发生错误:', error);
|
|
}
|
|
}
|
|
|
|
// 暴露给全局,供 mypjax.js 调用
|
|
window.loadBarrager = loadBarrager;
|
|
|
|
// 初始化监听
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadBarrager(); // 页面首次加载时启动弹幕
|
|
});
|
|
|
|
// 移除内部的 pjax:complete 监听,统一由 mypjax.js 控制调用时机
|
|
// document.addEventListener('pjax:complete', function() {
|
|
// loadBarrager();
|
|
// });
|
|
|
|
})();
|