From 0714b8eb48499d0656557f4771d8a6651fa5bc00 Mon Sep 17 00:00:00 2001 From: Vaica <94612053+zqlit@users.noreply.github.com> Date: Sat, 31 Jan 2026 22:52:19 +0800 Subject: [PATCH] =?UTF-8?q?fixed=20=E5=BC=B9=E5=B9=95=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hugo.toml | 6 + themes/Ying/assets/css/main.css | 123 ++++++----- themes/Ying/assets/js/barrage.js | 195 +++++++++++------- themes/Ying/assets/js/main.js | 1 + themes/Ying/assets/js/modules/artalk.js | 33 ++- .../Ying/assets/js/modules/floating-tools.js | 5 + .../Ying/assets/js/modules/infinite-scroll.js | 132 ++++++++++++ .../assets/js/modules/paragraph-comments.js | 62 +++++- themes/Ying/assets/js/modules/search.js | 8 + themes/Ying/assets/js/mypjax.js | 9 +- themes/Ying/layouts/partials/footer.html | 11 +- 11 files changed, 446 insertions(+), 139 deletions(-) create mode 100644 themes/Ying/assets/js/modules/infinite-scroll.js diff --git a/hugo.toml b/hugo.toml index e4fb3cc3..db63b4a2 100644 --- a/hugo.toml +++ b/hugo.toml @@ -140,6 +140,12 @@ hasCJKLanguage = true # 一言弹幕头像,留空则默认使用博客头像 hitokotoAvatar = "" + # ---------------------------------------------------------------------------- + # 无限加载配置 (Infinite Scroll Configuration) + # ---------------------------------------------------------------------------- + [params.infiniteScroll] + enable = false + # ---------------------------------------------------------------------------- # AI 评论配置 (AI Comment Configuration) # ---------------------------------------------------------------------------- diff --git a/themes/Ying/assets/css/main.css b/themes/Ying/assets/css/main.css index fed7a591..24a6ac6d 100644 --- a/themes/Ying/assets/css/main.css +++ b/themes/Ying/assets/css/main.css @@ -747,71 +747,73 @@ ul { display: flex; align-items: center; width: 100%; - border: 1px solid rgba(0, 0, 0, 0.08); /* Very subtle border for minimalism */ - border-radius: 99px; /* Fully rounded pill */ - padding: 2px 15px; - background: transparent; /* No background by default */ - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); /* Minimalist bottom border only */ + border-radius: 0; + padding: 2px 0; + background: transparent; + transition: all 0.3s ease; } .search-input-group:hover { - border-color: rgba(0, 0, 0, 0.15); - background: rgba(0, 0, 0, 0.01); + border-bottom-color: rgba(0, 0, 0, 0.3); + background: transparent; } .search-input-group:focus-within { - background: #fff; + background: transparent; border-color: transparent; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08); /* Soft diffuse shadow */ - transform: translateY(-1px); + border-bottom-color: var(--theme-color, #4a90e2); + box-shadow: none; + transform: none; } [data-theme="dark"] .search-input-group { - border-color: rgba(255, 255, 255, 0.1); + border-bottom-color: rgba(255, 255, 255, 0.1); background: transparent; } [data-theme="dark"] .search-input-group:hover { - border-color: rgba(255, 255, 255, 0.2); - background: rgba(255, 255, 255, 0.02); + border-bottom-color: rgba(255, 255, 255, 0.3); + background: transparent; } [data-theme="dark"] .search-input-group:focus-within { - background: #1f2933; + background: transparent; border-color: transparent; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); + border-bottom-color: var(--theme-color, #4a90e2); + box-shadow: none; } .search-input-group input { width: 100%; border: none; background: transparent; - padding: 10px 10px 10px 30px; + padding: 5px 30px 5px 0; /* Left align text, space for close icon on right */ font-size: 0.95rem; color: var(--text-title-color); outline: none; } +/* Search Icon visible */ .search-icon { - position: absolute; - left: 15px; - font-size: 1.1rem; color: #9ca3af; - pointer-events: none; - transition: color 0.3s; + font-size: 1.1rem; + margin-right: 6px; + transition: color 0.3s ease; } .search-input-group:focus-within .search-icon { - color: #4b5563; + color: var(--text-title-color); } [data-theme="dark"] .search-input-group:focus-within .search-icon { - color: #d1d5db; + color: var(--text-title-dark-color); } +/* Close icon on the right */ .close-icon { position: absolute; - right: 15px; + right: 0; font-size: 1.1rem; color: #9ca3af; cursor: pointer; @@ -826,7 +828,7 @@ ul { } .close-icon:hover { - color: #ef4444; + color: var(--theme-color, #4a90e2); } .search-dropdown { @@ -834,19 +836,18 @@ ul { top: 100%; left: 0; right: 0; - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-radius: 12px; - box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - margin-top: 12px; + background: rgba(255, 255, 255, 0.98); /* Less transparent for readability */ + /* backdrop-filter: blur(12px); Removed for cleaner look */ + border-radius: 4px; /* Sharper corners */ + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); /* Softer shadow */ + border: 1px solid rgba(0, 0, 0, 0.05); + margin-top: 5px; overflow: hidden; z-index: 50; opacity: 0; - transform: translateY(10px); + transform: translateY(5px); visibility: hidden; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.2s ease; } .search-dropdown.show { @@ -856,58 +857,61 @@ ul { } [data-theme="dark"] .search-dropdown { - background: rgba(31, 41, 51, 0.9); + background: #1f2933; border: 1px solid rgba(255, 255, 255, 0.05); - box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.5); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); } .search-result-item { - padding: 10px 15px; - border-bottom: 1px solid rgba(0,0,0,0.03); + padding: 8px 10px; + border-bottom: none; /* Clean list */ display: flex; flex-direction: column; + transition: background-color 0.2s; } .search-result-meta { display: flex; - justify-content: space-between; - font-size: 0.8rem; + justify-content: flex-start; /* Left align */ + gap: 10px; + font-size: 0.75rem; color: #999; - margin-top: 4px; + margin-top: 2px; } .search-result-summary-text { - max-width: 70%; + max-width: 80%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .no-result { - text-align: center; + text-align: left; color: #999; - padding: 15px; + padding: 10px; + font-size: 0.9rem; cursor: default; } [data-theme="dark"] .search-result-item { - border-bottom: 1px solid rgba(255,255,255,0.05); + border-bottom: none; } .search-result-item:hover { - background: rgba(0,0,0,0.02); + background: rgba(0,0,0,0.03); } [data-theme="dark"] .search-result-item:hover { - background: rgba(255,255,255,0.02); + background: rgba(255,255,255,0.05); } .search-result-title { display: block; - font-weight: 600; - font-size: 0.95rem; + font-weight: normal; /* Lighter weight */ + font-size: 0.9rem; color: var(--text-title-color); - margin-bottom: 4px; + margin-bottom: 0; } [data-theme="dark"] .search-result-title { @@ -3112,6 +3116,9 @@ input[type=submit] { flex-direction: column; gap: 10px; align-items: center; + /* GPU Acceleration for container */ + transform: translateZ(0); + will-change: transform; } .floating-tools a { @@ -3135,6 +3142,9 @@ input[type=submit] { backdrop-filter: blur(10px) saturate(180%); -webkit-backdrop-filter: blur(10px) saturate(180%); border: 1px solid rgba(255, 255, 255, 0.4); + /* GPU Acceleration for individual items */ + transform: translate3d(0, 0, 0); + will-change: transform, opacity; } .floating-tools a:hover { @@ -3165,10 +3175,12 @@ input[type=submit] { margin: 0; padding: 0; overflow: hidden; - transform: scale(0.8) translateY(10px); + /* Use translate3d for GPU acceleration instead of scale/translateY combo which might cause repaint */ + transform: translate3d(0, 10px, 0) scale(0.8); pointer-events: none; border-width: 0; transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s, height 0.3s ease, border-width 0.3s ease; + /* Explicitly tell browser to promote this layer */ will-change: opacity, transform, height; } @@ -3178,13 +3190,13 @@ input[type=submit] { height: 40px; width: 40px; margin-top: 0; - transform: scale(1) translateY(0); + transform: translate3d(0, 0, 0) scale(1); pointer-events: auto; border-width: 1px; } #back-to-top.show:hover { - transform: scale(1) translateY(-3px); + transform: translate3d(0, -3px, 0) scale(1); } /* GenericStrong */ [data-theme="dark"] .chroma .gs { } /* GenericSubheading */ [data-theme="dark"] .chroma .gu { font-weight:bold } @@ -4195,7 +4207,7 @@ html[data-theme="light"] .atk-loading-spinner svg circle { border: none; background: transparent; flex: 1; - padding: 5px 10px; + padding: 5px 5px; /* Reduced side padding */ font-size: 14px; outline: none; color: #333; @@ -4203,7 +4215,8 @@ html[data-theme="light"] .atk-loading-spinner svg circle { .search-input-group .search-icon { color: #999; - font-size: 16px; + font-size: 18px; /* Slightly larger */ + margin-right: 4px; } .search-input-group .close-icon { diff --git a/themes/Ying/assets/js/barrage.js b/themes/Ying/assets/js/barrage.js index f6173c1b..ad7518bc 100644 --- a/themes/Ying/assets/js/barrage.js +++ b/themes/Ying/assets/js/barrage.js @@ -6,6 +6,7 @@ function getArtalkComments() { return new Promise((resolve) => { let isResolved = false; + let interval = null; // 安全获取评论列表的辅助函数 const safeGetComments = () => { @@ -22,66 +23,58 @@ 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); + if (Array.isArray(data)) { + // 如果是事件触发(说明已加载完成),或者是轮询且有数据,则认为成功 + if (source === 'event' || data.length > 0) { + console.log(`[Barrage] 数据获取成功 (${source}): ${data.length} 条`); + isResolved = true; + if (interval) clearInterval(interval); + resolve(data); + } } }; - // 1. 立即检查 - const immediateData = safeGetComments(); - if (immediateData) { - tryResolve(immediateData, 'immediate'); - } else { - console.log('[Barrage] Artalk 实例未就绪 (Immediate)'); - } + // 检查 Artalk 实例并尝试获取数据 + const checkArtalk = () => { + if (isResolved) return; - if (isResolved) return; + if (window.artalk) { + // 1. 尝试绑定事件 (只绑定一次) + if (!window.artalk._barrageListenerAttached && typeof window.artalk.on === 'function') { + window.artalk.on('list-loaded', (data) => { + tryResolve(data, 'event'); + }); + window.artalk._barrageListenerAttached = true; + console.log('[Barrage] Artalk 事件监听已绑定'); + } - // 2. 监听加载完成事件 (推荐) - if (window.artalk && typeof window.artalk.on === 'function') { - window.artalk.on('list-loaded', (data) => { - console.log('[Barrage] list-loaded 事件触发'); - // 优先使用事件传递的数据(如果有),否则尝试主动获取 - const comments = (Array.isArray(data) && data.length > 0) ? data : (safeGetComments() || []); - tryResolve(comments, 'event'); - }); - } - - // 3. 轮询 (后备) - let pollCount = 0; - const interval = setInterval(() => { - if (isResolved) { - clearInterval(interval); - return; + // 2. 尝试直接获取数据 + const data = safeGetComments(); + if (data) { + tryResolve(data, 'poll'); + } } - pollCount++; - - const pollData = safeGetComments(); - if (pollData && Array.isArray(pollData) && pollData.length > 0) { - tryResolve(pollData, `poll-${pollCount}`); - } - }, 500); + }; - // 4. 超时处理 (5秒) + // 立即检查 + checkArtalk(); + + // 轮询检查 (直到获取数据或超时) + // 增加轮询频率,以便更快响应 Artalk 初始化 + interval = setInterval(checkArtalk, 200); + + // 超时处理 (8秒) setTimeout(() => { if (!isResolved) { - clearInterval(interval); - console.warn('[Barrage] 等待评论数据超时 (5s)'); + if (interval) clearInterval(interval); + console.warn('[Barrage] 获取数据超时'); - // 尝试从 DOM 获取作为最后兜底 + // 最后的尝试:DOM 兜底 (适用于静态页面或 API 失败但 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 idStr = el.id || ''; const id = idStr.replace('atk-comment-', ''); const contentEl = el.querySelector('.atk-content'); const avatarEl = el.querySelector('.atk-avatar img'); @@ -92,21 +85,19 @@ 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([]); // 真的没有数据 + resolve([]); } - }, 5000); + }, 8000); }); } @@ -200,7 +191,10 @@ this.stop(); this.clearDOM(); this.isDestroyed = false; + this.updateData(data); + } + updateData(data = []) { // 数据预处理:去重 & ID 规范化 const uniqueData = []; const seenIds = new Set(); @@ -214,11 +208,33 @@ }); this.data = uniqueData; - console.log(`[Barrage] 启动: 原始 ${data.length}, 去重后 ${this.data.length}`); + // 如果数据更新时列表变短了,重置索引以防越界 + if (this.index >= this.data.length) { + this.index = 0; + } - if (this.data.length === 0) return; + console.log(`[Barrage] 数据更新: ${this.data.length} 条`); - this.loop(); + if (this.data.length > 0 && !this.timer && !this.isPaused) { + this.loop(); + } + } + + hide() { + if (this.container) { + this.container.style.opacity = '0'; + this.container.style.pointerEvents = 'none'; + } + this.isPaused = true; + } + + show() { + if (this.container) { + this.container.style.opacity = '1'; + this.container.style.pointerEvents = 'auto'; + } + this.isPaused = false; + if (!this.timer) this.loop(); } stop() { @@ -226,24 +242,24 @@ clearTimeout(this.timer); this.timer = null; } - this.activeIds.clear(); + // 注意:不清除 activeIds,允许已发射的弹幕飞完 } clearDOM() { if (this.container) { this.container.innerHTML = ''; } + this.activeIds.clear(); } destroy() { this.isDestroyed = true; this.stop(); this.clearDOM(); - // 移除事件监听需要保存引用,这里因为容器可能被销毁,且是单例,暂不处理复杂的解绑 } loop() { - if (this.isDestroyed) return; + if (this.isDestroyed || this.isPaused) return; // 容器检测 if (!document.body.contains(this.container)) { @@ -251,7 +267,7 @@ return; } - if (!this.isPaused && this.data.length > 0) { + if (this.data.length > 0) { // 只有当前屏幕数量未满时才发射 if (this.activeIds.size < this.maxActiveItems) { this.tryShoot(); @@ -264,6 +280,8 @@ } tryShoot() { + if (this.data.length === 0) return; + let attempts = 0; const maxAttempts = this.data.length; @@ -294,8 +312,9 @@ let html = ''; // 优化:只有当有头像时才渲染 img 标签 + // 移除 loading="lazy" 以修复弹幕头像加载问题 if (avatar) { - html += ``; + html += ``; } html += `
${text}
`; item.innerHTML = html; @@ -334,6 +353,19 @@ return; } + // 检测是否存在评论区 + const hasCommentSection = document.getElementById('artalk') || + document.getElementById('Comments') || + document.querySelector('.atk-comment-wrap'); + + if (!hasCommentSection) { + // console.log('[Barrage] 当前页面无评论区,隐藏弹幕'); + if (barrageInstance) { + barrageInstance.hide(); + } + return; + } + // 1. 获取容器,如果没有则动态创建 let container = document.querySelector('.barrager'); if (!container) { @@ -343,10 +375,9 @@ document.body.appendChild(container); } - // 2. 清理旧实例 + // 确保显示(无论是否有评论区,都允许弹幕存在,可能显示的是上一页的数据或 Hitokoto) if (barrageInstance) { - barrageInstance.destroy(); - barrageInstance = null; + barrageInstance.show(); } try { @@ -371,13 +402,27 @@ 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; + + // 1. 优先尝试使用 email_encrypted 构造 Gravatar (适用于跨页面无 DOM 的情况) + if (c.email_encrypted) { + avatar = `https://cravatar.cn/avatar/${c.email_encrypted}?d=mp&s=240`; + } else if (c.email_hash) { + avatar = `https://cravatar.cn/avatar/${c.email_hash}?d=mp&s=240`; } + + // 2. 如果没有 hash,尝试从 API 对象直接获取 + if (!avatar && c.avatar) { + avatar = c.avatar; + } + + // 3. 尝试从 UA 获取 + if (!avatar && c.ua && c.ua.avatar) { + avatar = c.ua.avatar; + } + + // 4. 兜底默认头像 if (!avatar) { - avatar = c.avatar || (c.ua && c.ua.avatar) || 'https://cravatar.cn/avatar/default?d=mp'; + avatar = 'https://cravatar.cn/avatar/default?d=mp'; } return { @@ -403,17 +448,18 @@ if (barrageList.length === 0) { console.log('[Barrage] 无弹幕数据'); + // 即使无数据,也不要销毁实例,保持空转即可 return; } - // 再次检查实例 (竞态保护) + // 更新实例或创建新实例 if (barrageInstance) { - barrageInstance.destroy(); + barrageInstance.updateData(barrageList); + } else { + console.log(`[Barrage] 初始化: ${barrageList.length} 条`); + barrageInstance = new BarrageEngine({ container }); + barrageInstance.start(barrageList); } - - console.log(`[Barrage] 最终加载: ${barrageList.length} 条`); - barrageInstance = new BarrageEngine({ container }); - barrageInstance.start(barrageList); } catch (e) { console.warn('[Barrage] 加载异常:', e); @@ -427,5 +473,10 @@ document.addEventListener('DOMContentLoaded', () => { setTimeout(loadBarrager, 500); }); + + // PJAX 适配 + document.addEventListener('pjax:complete', () => { + setTimeout(loadBarrager, 500); + }); })(); diff --git a/themes/Ying/assets/js/main.js b/themes/Ying/assets/js/main.js index d04d5a8e..0e303409 100644 --- a/themes/Ying/assets/js/main.js +++ b/themes/Ying/assets/js/main.js @@ -27,4 +27,5 @@ document.addEventListener('DOMContentLoaded', function() { if (window.initCodeFold) window.initCodeFold(); if (window.initCodeCopy) window.initCodeCopy(); if (window.initParagraphComments) window.initParagraphComments(); + if (window.initInfiniteScroll && window.enableInfiniteScroll) window.initInfiniteScroll(); }); diff --git a/themes/Ying/assets/js/modules/artalk.js b/themes/Ying/assets/js/modules/artalk.js index 736bafab..3a200a0d 100644 --- a/themes/Ying/assets/js/modules/artalk.js +++ b/themes/Ying/assets/js/modules/artalk.js @@ -76,9 +76,36 @@ window.initArtalk = function() { var remoteConf = artalk.conf || (artalk.ctx && artalk.ctx.conf); if (remoteConf) { - // We only really want to cache the fields that come from the server, - // but saving the whole object is easier. We just have to be careful when merging. - localStorage.setItem(confKey, JSON.stringify(remoteConf)); + // Filter out non-serializable properties (like DOM elements, circular refs) + var safeConf = {}; + for (var key in remoteConf) { + var val = remoteConf[key]; + // Skip if value is a DOM element + if (val instanceof Element || val instanceof Node) continue; + // Skip if value is window or document + if (val === window || val === document) continue; + // Skip internal private properties starting with $ or _ + if (key.startsWith('$') || key.startsWith('_')) continue; + // Only allow simple types or simple objects + var type = typeof val; + if (type === 'string' || type === 'number' || type === 'boolean' || val === null) { + safeConf[key] = val; + } else if (type === 'object') { + // Simple array check + if (Array.isArray(val)) { + safeConf[key] = val; + } else { + // For objects, deep check might be too expensive/complex here. + // We'll try to shallow copy if it looks like a plain object + // and doesn't have circular markers like 'el' + if (val.constructor === Object && !val.el && !val.$el) { + safeConf[key] = val; + } + } + } + } + + localStorage.setItem(confKey, JSON.stringify(safeConf)); // console.log('Artalk: Config cached to', confKey); } } catch(e) { diff --git a/themes/Ying/assets/js/modules/floating-tools.js b/themes/Ying/assets/js/modules/floating-tools.js index 030b176f..49bae919 100644 --- a/themes/Ying/assets/js/modules/floating-tools.js +++ b/themes/Ying/assets/js/modules/floating-tools.js @@ -58,11 +58,16 @@ window.initFloatingTools = function() { // Global Scroll Handler (Singleton) if (!window.backToTopScrollHandler) { window.backToTopScrollHandler = function() { + // Performance Optimization: Check scroll less frequently? + // No, rAF is best. But we can add a check if the button is even in DOM/Visible? + // But checking DOM layout is expensive. + // Keep rAF but ensure 'update' is light. if (!ticking) { window.requestAnimationFrame(update); ticking = true; } }; + // Use passive listener to prevent scroll blocking window.addEventListener('scroll', window.backToTopScrollHandler, { passive: true }); } diff --git a/themes/Ying/assets/js/modules/infinite-scroll.js b/themes/Ying/assets/js/modules/infinite-scroll.js new file mode 100644 index 00000000..96fb5b57 --- /dev/null +++ b/themes/Ying/assets/js/modules/infinite-scroll.js @@ -0,0 +1,132 @@ + +window.initInfiniteScroll = function() { + // Configuration + const containerSelector = '.posts'; + const itemSelector = '.post'; + const paginationSelector = '.pagination'; + const nextLinkSelector = '.pagination .page-next .next'; + const loadingText = '加载中...'; + const noMoreText = '没有更多了'; + + const container = document.querySelector(containerSelector); + if (!container) return; // Not on a page with posts + + // Check if we already have an observer (to prevent duplicates on PJAX re-init) + if (window.infiniteScrollObserver) { + window.infiniteScrollObserver.disconnect(); + window.infiniteScrollObserver = null; + } + + // Remove existing sentinel if any + const existingSentinel = document.getElementById('infinite-scroll-sentinel'); + if (existingSentinel) existingSentinel.remove(); + + // Find next link + let nextLink = document.querySelector(nextLinkSelector); + if (!nextLink) return; // No next page + + // Hide default pagination + const pagination = document.querySelector(paginationSelector); + if (pagination) pagination.style.display = 'none'; + + // Create sentinel + const sentinel = document.createElement('div'); + sentinel.id = 'infinite-scroll-sentinel'; + sentinel.innerHTML = `
${loadingText}
`; + + // Insert after container + container.parentNode.insertBefore(sentinel, container.nextSibling); + + let isLoading = false; + + const loadNextPage = async () => { + if (isLoading) return; + + // Re-check next link from the DOM (it should be updated after previous fetch) + nextLink = document.querySelector(nextLinkSelector); + if (!nextLink) { + sentinel.innerHTML = `
${noMoreText}
`; + if (window.infiniteScrollObserver) window.infiniteScrollObserver.disconnect(); + return; + } + + isLoading = true; + const url = nextLink.href; + + try { + const response = await fetch(url); + if (!response.ok) throw new Error('Network response was not ok'); + const text = await response.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(text, 'text/html'); + + // 1. Get new posts + const newPosts = doc.querySelectorAll(`${containerSelector} ${itemSelector}`); + + // 2. Append posts + if (newPosts.length > 0) { + newPosts.forEach(post => { + // Remove UIkit scrollspy attributes to prevent interference and use manual animation + post.removeAttribute('uk-scrollspy-class'); + post.style.opacity = 0; + container.appendChild(post); + + // Trigger reflow + void post.offsetWidth; + + post.style.transition = 'opacity 0.5s ease'; + post.style.opacity = 1; + }); + } + + // 3. Update pagination (crucial for next link) + const newPagination = doc.querySelector(paginationSelector); + const oldPagination = document.querySelector(paginationSelector); + if (oldPagination && newPagination) { + oldPagination.innerHTML = newPagination.innerHTML; + oldPagination.style.display = 'none'; // Keep hidden + } else if (oldPagination && !newPagination) { + oldPagination.remove(); + sentinel.innerHTML = `
${noMoreText}
`; + if (window.infiniteScrollObserver) window.infiniteScrollObserver.disconnect(); + isLoading = false; + return; + } + + // 4. Re-init plugins for new content + if (typeof lazyLoadInit === 'function') { + lazyLoadInit({ + offsetBottom: 800, + offsetTopm: 800, + showTime: 1100 + }); + } + if (window.ViewImage) { + ViewImage.init('.post-content img'); + } + + // 5. Check if there is a next page in the NEW content (double check) + const newNextLink = document.querySelector(nextLinkSelector); + if (!newNextLink) { + sentinel.innerHTML = `
${noMoreText}
`; + if (window.infiniteScrollObserver) window.infiniteScrollObserver.disconnect(); + } + + } catch (error) { + console.error('Infinite scroll error:', error); + sentinel.innerHTML = `
加载失败,点击重试
`; + } finally { + isLoading = false; + } + }; + + window.infiniteScrollObserver = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + loadNextPage(); + } + }, { + rootMargin: '200px' + }); + + window.infiniteScrollObserver.observe(sentinel); +}; diff --git a/themes/Ying/assets/js/modules/paragraph-comments.js b/themes/Ying/assets/js/modules/paragraph-comments.js index 8f5e72fe..72cff61a 100644 --- a/themes/Ying/assets/js/modules/paragraph-comments.js +++ b/themes/Ying/assets/js/modules/paragraph-comments.js @@ -11,19 +11,21 @@ display: none; background: var(--artalk-editer-color, #333); color: #fff; - padding: 6px 12px; - border-radius: 6px; + padding: 8px 16px; + border-radius: 50px; font-size: 13px; cursor: pointer; z-index: 2147483647; transform: translate(-50%, -120%); - box-shadow: 0 4px 12px rgba(0,0,0,0.15); + box-shadow: 0 4px 20px rgba(0,0,0,0.25); transition: opacity 0.2s, transform 0.2s; pointer-events: auto; align-items: center; gap: 6px; white-space: nowrap; animation: popIn 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28); + border: 1px solid rgba(255,255,255,0.1); + font-weight: 500; } #${POPUP_ID}::after { content: ''; @@ -172,7 +174,8 @@ // Create Popup Element var popup = document.createElement('div'); popup.id = POPUP_ID; - popup.innerHTML = ' 评论'; + // 使用 Remix Icon 的引用图标 + popup.innerHTML = ' 引用评论'; document.body.appendChild(popup); // Create Modal Element @@ -494,11 +497,60 @@ popup.style.opacity = 0; } + // Custom Smooth Scroll with Easing + function smoothScrollTo(targetPosition, duration) { + var startPosition = window.scrollY; + var distance = targetPosition - startPosition; + var startTime = null; + + function animation(currentTime) { + if (startTime === null) startTime = currentTime; + var timeElapsed = currentTime - startTime; + var run = ease(timeElapsed, startPosition, distance, duration); + window.scrollTo(0, run); + if (timeElapsed < duration) requestAnimationFrame(animation); + } + + // Ease-in-out Cubic + function ease(t, b, c, d) { + t /= d / 2; + if (t < 1) return c / 2 * t * t * t + b; + t -= 2; + return c / 2 * (t * t * t + 2) + b; + } + + requestAnimationFrame(animation); + } + function handleQuoteClick() { if (!window.artalk) return; var commentsEl = document.getElementById('Comments'); - if (commentsEl) commentsEl.scrollIntoView({ behavior: 'smooth' }); + if (commentsEl) { + // Target the editor area directly if possible, or the top of comments + // Adjust offset to show the editor more clearly (e.g. 100px from top) + var targetEl = document.querySelector('.atk-main-editor') || commentsEl; + var elementPosition = targetEl.getBoundingClientRect().top + window.scrollY; + + // Align to 1/3 of the screen height (more comfortable for reading/typing) + var offsetPosition = elementPosition - (window.innerHeight / 3); + + // Use custom smooth scroll (800ms) + smoothScrollTo(offsetPosition, 800); + + // Highlight effect after scroll (Subtle Scale only, no blue border) + setTimeout(function() { + var editorEl = document.querySelector('.atk-main-editor'); + if (editorEl) { + editorEl.style.transition = 'transform 0.3s ease'; + editorEl.style.transform = 'scale(1.02)'; + + setTimeout(function() { + editorEl.style.transform = ''; + }, 600); + } + }, 800); + } var quoteContent = `> ${selectedText}\n\n`; var editor = window.artalk.editor; diff --git a/themes/Ying/assets/js/modules/search.js b/themes/Ying/assets/js/modules/search.js index 2f6e9406..dccb26b2 100644 --- a/themes/Ying/assets/js/modules/search.js +++ b/themes/Ying/assets/js/modules/search.js @@ -149,6 +149,14 @@ function initInlineSearch() { input.parentNode.replaceChild(newInput, input); input = newInput; + // Handle Search Icon Click (Focus input) + var searchIcon = searchContainer.querySelector('.search-icon'); + if (searchIcon) { + searchIcon.addEventListener('click', function() { + input.focus(); + }); + } + // Handle Close Icon Logic var closeIcon = searchContainer.querySelector('.close-icon'); if (closeIcon) { diff --git a/themes/Ying/assets/js/mypjax.js b/themes/Ying/assets/js/mypjax.js index 540ad1fa..3913d707 100644 --- a/themes/Ying/assets/js/mypjax.js +++ b/themes/Ying/assets/js/mypjax.js @@ -98,7 +98,14 @@ document.addEventListener("pjax:complete", function () { if (typeof window.initParagraphComments === 'function') { try { window.initParagraphComments(); - } catch(e) { console.error('initParagraphComments error:', e); } + } catch (e) { + console.error('initParagraphComments error:', e); + } + } + + // 初始化无限加载 + if (typeof window.initInfiniteScroll === 'function' && window.enableInfiniteScroll) { + window.initInfiniteScroll(); } // 初始化友链状态 diff --git a/themes/Ying/layouts/partials/footer.html b/themes/Ying/layouts/partials/footer.html index 3d15f65c..5a9cfcb5 100644 --- a/themes/Ying/layouts/partials/footer.html +++ b/themes/Ying/layouts/partials/footer.html @@ -67,12 +67,17 @@ {{ $search := resources.Get "js/modules/search.js" }} {{ $floatingTools := resources.Get "js/modules/floating-tools.js" }} {{ $paragraphComments := resources.Get "js/modules/paragraph-comments.js" }} +{{ $infiniteScroll := resources.Get "js/modules/infinite-scroll.js" }} {{ $artalkModule := resources.Get "js/modules/artalk.js" }} {{ $pjaxLib := resources.Get "js/pjax.js" }} -{{ $scripts := slice $iconfont $lightgallery $uikit $easylazyload $viewimage $artalk $barrage $utils $loader $search $floatingTools $paragraphComments $artalkModule $nprogress $pjaxLib $main $mypjax $message $echarts $tiaozhuan | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint }} - - +{{ $scripts := slice $iconfont $lightgallery $uikit $easylazyload $viewimage $artalk $barrage $utils $loader $search $floatingTools $paragraphComments $infiniteScroll $artalkModule $nprogress $pjaxLib $main $mypjax $message $echarts $tiaozhuan | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint }} + + + \ No newline at end of file