From dd642782b49b524bc9929b2ef0ec052bc08dbd23 Mon Sep 17 00:00:00 2001 From: Vaica <94612053+zqlit@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:09:57 +0800 Subject: [PATCH] =?UTF-8?q?fixed=20=E5=BC=B9=E5=B9=95=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hugo.toml | 1 + themes/Ying/assets/css/main.css | 167 ++++++++----- themes/Ying/assets/js/barrage.js | 285 ++++++++++++++++++++++ themes/Ying/assets/js/main.js | 88 +------ themes/Ying/layouts/partials/barrage.html | 4 + themes/Ying/layouts/partials/footer.html | 3 +- themes/Ying/layouts/partials/head.html | 2 +- themes/Ying/layouts/partials/header.html | 2 + 8 files changed, 417 insertions(+), 135 deletions(-) create mode 100644 themes/Ying/assets/js/barrage.js create mode 100644 themes/Ying/layouts/partials/barrage.html diff --git a/hugo.toml b/hugo.toml index c9e337fc..c88c7116 100644 --- a/hugo.toml +++ b/hugo.toml @@ -126,6 +126,7 @@ hasCJKLanguage = true # 弹幕配置 (Barrage Configuration) # ---------------------------------------------------------------------------- [params.barrage] + enable = true enableHitokoto = true hitokotoUrl = "https://v1.hitokoto.cn?c=a&c=b&c=c&c=d&c=i" diff --git a/themes/Ying/assets/css/main.css b/themes/Ying/assets/css/main.css index d00eff82..4fea277f 100644 --- a/themes/Ying/assets/css/main.css +++ b/themes/Ying/assets/css/main.css @@ -2192,69 +2192,126 @@ input[type=submit] { } /* 弹幕css */ -.barrage { - display: -webkit-box; - display: -webkit-flex; - display: -moz-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -webkit-flex-direction: row; - -moz-box-orient: horizontal; - -moz-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - cursor: pointer; - -webkit-flex-wrap: nowrap; - -ms-flex-wrap: nowrap; - flex-wrap: nowrap; - -webkit-box-align: center; - -webkit-align-items: center; - -moz-box-align: center; - -ms-flex-align: center; - align-items: center; - word-break: keep-all; - white-space: nowrap; - -webkit-border-radius: 20px; - border-radius: 20px; - padding: 0 20px; - padding-left: 15px; - opacity: 0.8; - will-change: transform; +.barrager { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; /* 容器不阻挡点击 */ + z-index: 1; /* 在 main (z-index: 10) 背后,但在 body 背景之上 */ + overflow: hidden; +} + +/* 弹幕条目 (左到右) */ +.barrage-item { position: absolute; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + display: inline-flex; + align-items: flex-start; /* 顶部对齐 */ + white-space: nowrap; + will-change: transform; + pointer-events: auto; /* 弹幕本身可点击 */ user-select: none; - pointer-events: auto; - text-shadow: 0ch; - z-index: 11; + transform: translateX(-100%); /* 初始在左侧外 */ + z-index: 10; + /* 初始透明度,配合动画实现淡入淡出 */ + opacity: 0.9; + /* 缩小整体尺寸 */ + transform-origin: left top; } -.barrage .header-avatar { - -webkit-border-radius: 50%; +/* 头像样式 */ +.barrage-avatar { + width: 28px; /* 缩小头像 */ + height: 28px; border-radius: 50%; - position: relative; - right: 7px; - display: flex; - align-items: center; - justify-content: center; - width: 26px !important; - height: 26px !important; - min-width: 26px !important; -} - -.barrage .header-avatar img { - width: 26px !important; - height: 26px !important; + margin-right: 8px; object-fit: cover; - border-radius: 50%; + border: 1px solid rgba(0,0,0,0.05); + box-shadow: 0 1px 2px rgba(0,0,0,0.1); + flex-shrink: 0; + background: #fff; } -.pause { - -webkit-animation-play-state: paused; - animation-play-state: paused; +/* 聊天气泡样式 (仿QQ/微信风格) */ +.barrage-bubble { + background: #ffffff; /* QQ/微信 白底 */ + color: #1f1f1f; /* 深黑文字 */ + padding: 6px 10px; /* 缩小内边距 */ + border-radius: 6px; /* 稍微减小圆角 */ + font-size: 13px; /* 缩小字体 */ + line-height: 1.4; + position: relative; + box-shadow: 0 1px 5px rgba(0,0,0,0.08); + max-width: 260px; /* 减小最大宽度 */ + white-space: normal; + word-break: break-all; +} + +/* 气泡小尾巴 */ +.barrage-bubble::before { + content: ''; + position: absolute; + top: 10px; /* 调整位置 */ + left: -5px; /* 调整大小 */ + width: 0; + height: 0; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #ffffff; +} + +/* 昼夜切换适配 */ +[data-theme="dark"] .barrage-avatar { + border-color: #444; + background: #222; +} + +[data-theme="dark"] .barrage-bubble { + background: #262626; /* 深色模式深灰底 */ + color: #e5e5e5; + border: 1px solid rgba(255,255,255,0.05); + box-shadow: none; +} + +[data-theme="dark"] .barrage-bubble::before { + border-right-color: #262626; +} + +/* 悬停效果 */ +.barrage-item:hover .barrage-bubble { + background: #f0f0f0; /* 悬停微暗 */ + z-index: 100; +} + +.barrage-item:hover .barrage-bubble::before { + border-right-color: #f0f0f0; +} + +[data-theme="dark"] .barrage-item:hover .barrage-bubble { + background: #333; +} + +[data-theme="dark"] .barrage-item:hover .barrage-bubble::before { + border-right-color: #333; +} + +/* 左到右动画的关键帧 */ +@keyframes barrage-slide-right { + 0% { + transform: translateX(-100%); /* 左侧屏幕外 */ + opacity: 0; + } + 5% { + opacity: 1; /* 快速淡入 */ + } + 95% { + opacity: 1; + } + 100% { + transform: translateX(100vw); /* 右侧屏幕外 */ + opacity: 0; /* 淡出 */ + } } /* artalk样式重写css */ diff --git a/themes/Ying/assets/js/barrage.js b/themes/Ying/assets/js/barrage.js new file mode 100644 index 00000000..609786c6 --- /dev/null +++ b/themes/Ying/assets/js/barrage.js @@ -0,0 +1,285 @@ +// 弹幕功能封装 +(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; + } + + // 开始发送 + start(data = []) { + console.log(`[Barrage] 启动弹幕,共 ${data.length} 条`); + this.data = data; + if (this.data.length === 0) return; + + this.clear(); + this.loop(); + } + + // 清空 + clear() { + 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() { + // 如果容器不在文档中(PJAX切换后),尝试重新获取容器 + // (注意:this.container 是 DOM 引用,如果页面重绘,旧引用可能失效) + if (!document.body.contains(this.container)) { + console.warn('[Barrage] 容器失效,尝试重新获取...'); + const newContainer = document.querySelector('.barrager'); + if (newContainer) { + this.container = newContainer; + console.log('[Barrage] 容器重新获取成功,继续播放'); + } else { + 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 += ``; + } + + // 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'; + } + + // 随机轨道 (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 未定义'); + } + } + + // 加载弹幕主函数 + async function loadBarrager() { + try { + const commentsData = await initArtalkAndGetComments(); + if (commentsData) { + await createBarragerFromArtalk(commentsData); + } + } catch (error) { + console.error('加载弹幕时发生错误:', error); + } + } + + // 初始化监听 + document.addEventListener('DOMContentLoaded', function() { + loadBarrager(); // 页面首次加载时启动弹幕 + }); + + document.addEventListener('pjax:complete', function() { + // PJAX 跳转后重新加载弹幕 + loadBarrager(); + }); + +})(); diff --git a/themes/Ying/assets/js/main.js b/themes/Ying/assets/js/main.js index 8854b0ee..8ec94df0 100644 --- a/themes/Ying/assets/js/main.js +++ b/themes/Ying/assets/js/main.js @@ -79,81 +79,9 @@ if (typeof lazyLoadInit === 'function') { console.warn('lazyLoadInit is not defined'); } -async function loadBarrager() { - try { - const commentsData = await initArtalkAndGetComments(); - if (commentsData) { - await createBarragerFromArtalk(commentsData); - } - } catch (error) { - console.error('加载弹幕时发生错误:', error); - } -} -// 异步初始化 Artalk 并获取评论数据 -function initArtalkAndGetComments() { - return new Promise((resolve, reject) => { - Artalk.use((ctx) => { - ctx.on('list-loaded', () => { - console.log('评论列表加载完毕'); - const commentsData = ctx.getComments(); - if (commentsData) { - resolve(commentsData); - // console.log(commentsData); - } else { - reject('获取评论数据失败'); - } - }); - }); - }); -} +// 异步初始化 Artalk 并获取评论数据 (Moved to barrage.js) -// 从 Artalk 评论数据生成弹幕 -async function createBarragerFromArtalk(commentsData) { - // 确保弹幕容器存在 - const barragerContainer = document.querySelector('.barrager'); - if (!barragerContainer) { - console.warn('未找到弹幕容器 .barrager'); - return; - } - - // 1. 过滤和处理评论数据 (只保留昵称和内容) - const barrageList = commentsData.map(comment => { - // 简单清理 HTML 标签,保留纯文本 (可选) - const contentText = comment.content.replace(/<[^>]+>/g, "").substring(0, 50); // 限制长度 - return { - info: contentText, // 弹幕内容 - href: '#', // 链接 (可选,点击弹幕跳转) - speed: 10 + Math.random() * 5, // 随机速度 - color: '#fff', // 字体颜色 (白色) - old_ie_color: '#000000', // 旧版 IE 颜色 - }; - }).filter(item => item.info.trim() !== ""); // 过滤空评论 - - // 如果没有评论,则不显示弹幕 - if (barrageList.length === 0) { - return; - } - - // 2. 初始化 jQuery.barrager (如果尚未初始化) - // 注意:这里假设 jquery.barrager.js 已经加载并挂载到 jQuery 对象上 - // 由于 barrager 插件通常是直接操作 DOM,我们可能需要循环添加 - - // 简单的循环发送弹幕示例 (每隔一段时间发送一条) - let index = 0; - const sendBarrage = () => { - if (index >= barrageList.length) { - index = 0; // 循环播放 - } - const item = barrageList[index]; - // 使用 jQuery 选择器调用 barrager - $('body').barrager(item); - index++; - }; - - // 每 2 秒发送一条弹幕 - setInterval(sendBarrage, 2000); -} async function fetchHitokoto() { @@ -225,11 +153,11 @@ function initCodeCopy() { }); } -// 页面加载完成后初始化 -document.addEventListener('DOMContentLoaded', function() { - initCodeCopy(); - initDescriptionHitokoto(); -}); +// 页面加载完成后初始化 (Merged into main DOMContentLoaded listener) +// document.addEventListener('DOMContentLoaded', function() { +// initCodeCopy(); +// initDescriptionHitokoto(); +// }); // 初始化个人描述一言切换 function initDescriptionHitokoto() { @@ -367,11 +295,15 @@ function initFloatingTools() { document.addEventListener('pjax:complete', function() { updateFloatingToolsVisibility(); hideSelectionPopup(); + // PJAX 跳转后重新加载弹幕 (Moved to barrage.js) }); document.addEventListener('DOMContentLoaded', function() { initFloatingTools(); initSelectionPopup(); + initCodeCopy(); + initDescriptionHitokoto(); + // 页面首次加载时启动弹幕 (Moved to barrage.js) }); // Selection Popup Logic diff --git a/themes/Ying/layouts/partials/barrage.html b/themes/Ying/layouts/partials/barrage.html new file mode 100644 index 00000000..81018448 --- /dev/null +++ b/themes/Ying/layouts/partials/barrage.html @@ -0,0 +1,4 @@ +{{ if .Site.Params.barrage.enable }} + +
+{{ end }} diff --git a/themes/Ying/layouts/partials/footer.html b/themes/Ying/layouts/partials/footer.html index 19efc51b..67ad8db0 100644 --- a/themes/Ying/layouts/partials/footer.html +++ b/themes/Ying/layouts/partials/footer.html @@ -30,6 +30,7 @@ {{ $easylazyload := resources.Get "js/EasyLazyload.min.js" }} {{ $viewimage := resources.Get "js/view-image.min.js" }} {{ $artalk := resources.Get "js/Artalk.js" }} +{{ $barrage := resources.Get "js/barrage.js" }} {{ $main := resources.Get "js/main.js" }} {{ $mypjax := resources.Get "js/mypjax.js" }} {{ $nprogress := resources.Get "js/nprogress.js" }} @@ -37,7 +38,7 @@ {{ $echarts := resources.Get "js/echarts.min.js" }} {{ $tiaozhuan := resources.Get "js/tiaozhuan.js" }} -{{ $scripts := slice $iconfont $lightgallery $uikit $easylazyload $viewimage $artalk $main $mypjax $nprogress $message $echarts $tiaozhuan | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint }} +{{ $scripts := slice $iconfont $lightgallery $uikit $easylazyload $viewimage $artalk $barrage $main $mypjax $nprogress $message $echarts $tiaozhuan | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint }} diff --git a/themes/Ying/layouts/partials/head.html b/themes/Ying/layouts/partials/head.html index b6ec7cf8..7ca26b9b 100644 --- a/themes/Ying/layouts/partials/head.html +++ b/themes/Ying/layouts/partials/head.html @@ -206,7 +206,7 @@ } - + diff --git a/themes/Ying/layouts/partials/header.html b/themes/Ying/layouts/partials/header.html index ea2b3fa5..b5e81215 100644 --- a/themes/Ying/layouts/partials/header.html +++ b/themes/Ying/layouts/partials/header.html @@ -1,3 +1,5 @@ + +{{ partial "barrage.html" . }}