// Global Artalk Initialization Function (Supports PJAX) // ── 持久化 artalkInitCount (跨 PJAX 切换保留) ── // PJAX 切换时 artalk.js 会被重新加载,普通变量会重置。 // 使用 sessionStorage 持久化,让缓存机制在 PJAX 切换时继续生效。 if (typeof window.artalkInitCount === 'undefined') { try { var stored = sessionStorage.getItem('artalk_init_count'); window.artalkInitCount = stored ? parseInt(stored, 10) : 0; } catch(e) { window.artalkInitCount = 0; } } var saveInitCount = function() { try { sessionStorage.setItem('artalk_init_count', String(window.artalkInitCount)); } catch(e) {} }; // ── 增强评论框字段 (PJAX 适配版本) ── // 在 Artalk 完全加载前就开始监控,确保油猴脚本能识别到字段 window.__artalkEnhancer = window.__artalkEnhancer || { observer: null, running: false, enhance: function() { var self = this; var count = 0; // 昵称输入框 var nickInputs = document.querySelectorAll('input[name="nick"]:not([data-autofill-done])'); nickInputs.forEach(function(input) { input.setAttribute('autocomplete', 'name'); input.setAttribute('aria-label', '昵称 author nick name user'); input.classList.add('comment-author', 'user-nick', 'user-name', 'user-displayname', '昵称'); input.setAttribute('data-autofill-done', '1'); count++; }); // 邮箱输入框 var emailInputs = document.querySelectorAll('input[name="email"]:not([data-autofill-done])'); emailInputs.forEach(function(input) { input.setAttribute('autocomplete', 'email'); input.setAttribute('aria-label', '邮箱 email mail'); input.classList.add('comment-email', 'user-email', 'user-mail', '邮箱'); input.setAttribute('data-autofill-done', '1'); count++; }); // 网址输入框 var linkInputs = document.querySelectorAll('input[name="link"]:not([data-autofill-done])'); linkInputs.forEach(function(input) { input.setAttribute('autocomplete', 'url'); input.setAttribute('aria-label', '网址 website url link homepage site web'); input.classList.add('comment-url', 'user-link', 'user-website', 'user-site', 'user-web', '网址'); input.setAttribute('data-autofill-done', '1'); count++; }); // 内容输入框(避免被误识别为昵称) var contentInputs = document.querySelectorAll('textarea[name="content"]:not([data-autofill-done])'); contentInputs.forEach(function(input) { input.setAttribute('autocomplete', 'off'); input.setAttribute('aria-label', '评论内容 content comment message'); input.setAttribute('data-autofill-done', '1'); count++; }); if (count > 0 && window.console && console.log) { console.log('[ArtalkEnhancer] 增强了 ' + count + ' 个评论输入框'); } return count; }, // 监控 DOM 变化(PJAX 切换、动态加载) observe: function() { var self = this; if (typeof MutationObserver === 'undefined') return; if (this.observer) return; this.observer = new MutationObserver(function(mutations) { var shouldEnhance = false; for (var i = 0; i < mutations.length; i++) { var added = mutations[i].addedNodes; for (var j = 0; j < added.length; j++) { var node = added[j]; if (node.nodeType === 1) { // 检查是否是 input 或 textarea 元素 if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') { shouldEnhance = true; break; } // 检查子节点 if (node.querySelectorAll) { var inputs = node.querySelectorAll('input, textarea'); if (inputs.length > 0) { shouldEnhance = true; break; } } } } if (shouldEnhance) break; } if (shouldEnhance) { // 稍微延迟,确保 DOM 完全插入 setTimeout(function() { self.enhance(); }, 10); } }); this.observer.observe(document.body, { childList: true, subtree: true }); }, // 启动全局监控 start: function() { if (this.running) return; this.running = true; if (window.console && console.log) { console.log('[ArtalkEnhancer] 启动评论框字段增强'); } // 立即尝试增强一次 this.enhance(); // 启动 DOM 监控 this.observe(); // 兜底:定时检查(Artalk 加载慢时) var checkCount = 0; var maxChecks = 20; // 最多检查 20 次(10 秒) var interval = setInterval(function() { checkCount++; var enhanced = window.__artalkEnhancer.enhance(); if (enhanced > 0 || checkCount >= maxChecks) { clearInterval(interval); } }, 500); } }; // 立即启动(不等待 DOMContentLoaded,尽早开始监控) if (typeof window.__artalkEnhancer !== 'undefined') { window.__artalkEnhancer.start(); } // 监听 PJAX 切换:重置并重新增强 document.addEventListener('pjax:send', function() { if (window.__artalkEnhancer && window.console && console.log) { console.log('[ArtalkEnhancer] PJAX 切换,准备重新增强'); } }); document.addEventListener('pjax:success', function() { if (window.__artalkEnhancer) { // 立即尝试 window.__artalkEnhancer.enhance(); // 延迟再试(Artalk 重新初始化需要时间) setTimeout(function() { window.__artalkEnhancer.enhance(); }, 200); setTimeout(function() { window.__artalkEnhancer.enhance(); }, 500); setTimeout(function() { window.__artalkEnhancer.enhance(); }, 1000); } }); document.addEventListener('pjax:complete', function() { if (window.__artalkEnhancer) { setTimeout(function() { window.__artalkEnhancer.enhance(); }, 300); setTimeout(function() { window.__artalkEnhancer.enhance(); }, 800); } }); window.loadScriptOnce = window.loadScriptOnce || function(src, globalName, callback) { if (globalName && window[globalName]) { callback(); return; } var existing = document.querySelector('script[data-dynamic-src="' + src + '"]'); if (existing) { existing.addEventListener('load', callback, { once: true }); return; } var script = document.createElement('script'); script.src = src; script.defer = true; script.dataset.dynamicSrc = src; script.onload = callback; document.head.appendChild(script); }; window.initArtalk = function() { if (!document.querySelector("#Comments")) return; // Check if configuration exists if (!window.artalkConfig) { console.warn('window.artalkConfig is not defined. Skipping Artalk init.'); return; } if (typeof Artalk === 'undefined') { var artalkUrl = window.optionalAssetUrls && window.optionalAssetUrls.artalk; if (!artalkUrl) return; window.loadScriptOnce(artalkUrl, 'Artalk', window.initArtalk); return; } try { // Logic for caching Artalk configuration to speed up rendering // 1. Manual Refresh (F5): Request from server and save to localStorage // 2. PJAX: Load from localStorage if available // Initialize initialization count to track PJAX usage // (从 sessionStorage 加载,由文件顶部的逻辑处理) if (typeof window.artalkInitCount === 'undefined') { window.artalkInitCount = 0; } window.artalkInitCount++; saveInitCount(); // 持久化到 sessionStorage var config = Object.assign({}, window.artalkConfig); // Clone config var server = config.server.replace(/\/$/, ''); var confKey = 'artalk_conf_' + btoa(server); var cachedConf = null; // Try to read cache try { var raw = localStorage.getItem(confKey); if (raw) cachedConf = JSON.parse(raw); } catch(e) { console.warn('Failed to parse Artalk cache:', e); } // Decision: Use Cache or Fetch? // Use cache only if: // 1. Not the first load (PJAX) // 2. Cache exists if (window.artalkInitCount > 1 && cachedConf) { // Merge cache into config // CAUTION: cachedConf contains page-specific data (like pageKey) from the previous page. // We must ensure current window.artalkConfig overwrites it. try { // Defensive copy to avoid mutating cached object var mergedConfig = Object.assign({}, config); Object.assign(mergedConfig, cachedConf); Object.assign(mergedConfig, window.artalkConfig); // Re-apply current page config mergedConfig.useBackendConf = false; // Attempt to init with cache window.artalk = Artalk.init(mergedConfig); } catch (initErr) { console.warn('Artalk init with cache failed, falling back to default:', initErr); localStorage.removeItem(confKey); // Clear bad cache window.artalk = Artalk.init(config); // Fallback to normal init } } else { // First load or no cache: Allow Artalk to fetch (useBackendConf defaults to true) window.artalk = Artalk.init(config); } var artalk = window.artalk; // Local reference // Sync Theme Mode Immediately var currentTheme = document.documentElement.getAttribute('data-theme'); if (typeof artalk.setDarkMode === 'function') { artalk.setDarkMode(currentTheme === 'dark'); } // ── 增强评论框字段属性 (兼容油猴自动填充脚本) ── // 为 Artalk 输入框添加额外属性,确保第三方脚本(如博客网站留言评论信息自动填充) // 能够通过 type/id/name/placeholder/class/aria-label 准确匹配 var enhanceEditorInputs = function() { var editor = document.querySelector('.atk-editor'); if (!editor) return; // 昵称输入框 var nickInput = editor.querySelector('input[name="nick"]'); if (nickInput && !nickInput.getAttribute('data-autofill-done')) { nickInput.setAttribute('autocomplete', 'name'); nickInput.setAttribute('aria-label', '昵称 author nick name'); nickInput.classList.add('comment-author', 'user-nick'); nickInput.setAttribute('data-autofill-done', '1'); } // 邮箱输入框 var emailInput = editor.querySelector('input[name="email"]'); if (emailInput && !emailInput.getAttribute('data-autofill-done')) { emailInput.setAttribute('autocomplete', 'email'); emailInput.setAttribute('aria-label', '邮箱 email mail'); emailInput.classList.add('comment-email', 'user-email'); emailInput.setAttribute('data-autofill-done', '1'); } // 网址输入框 var linkInput = editor.querySelector('input[name="link"]'); if (linkInput && !linkInput.getAttribute('data-autofill-done')) { linkInput.setAttribute('autocomplete', 'url'); linkInput.setAttribute('aria-label', '网址 website url link homepage site web'); linkInput.classList.add('comment-url', 'user-link', 'user-website'); linkInput.setAttribute('data-autofill-done', '1'); } // 内容输入框(避免被误识别为昵称) var contentInput = editor.querySelector('textarea[name="content"]'); if (contentInput && !contentInput.getAttribute('data-autofill-done')) { contentInput.setAttribute('autocomplete', 'off'); contentInput.setAttribute('aria-label', '评论内容 content comment'); contentInput.setAttribute('data-autofill-done', '1'); } }; // 立即执行一次 enhanceEditorInputs(); // 监听评论框加载完成 artalk.on('mounted', function() { setTimeout(enhanceEditorInputs, 100); }); // 监听评论列表加载(PJAX 切换后) artalk.on('list-loaded', function() { setTimeout(enhanceEditorInputs, 200); }); // 兜底:定时检查(防止某些情况下事件未触发) var enhanceInterval = setInterval(function() { enhanceEditorInputs(); // 找到所有字段都已增强后停止 var editor = document.querySelector('.atk-editor'); if (editor) { var inputs = editor.querySelectorAll('input[name="nick"], input[name="email"], input[name="link"], textarea[name="content"]'); var allDone = true; inputs.forEach(function(input) { if (!input.getAttribute('data-autofill-done')) allDone = false; }); if (allDone && inputs.length > 0) { clearInterval(enhanceInterval); } } }, 1000); // 30秒后停止轮询 setTimeout(function() { clearInterval(enhanceInterval); }, 30000); // Aggressive Config Saving Strategy var saveConf = function() { try { // Attempt to find the full configuration object // artalk.conf is the standard property in v2+ // artalk.ctx.conf is for internal context access var remoteConf = artalk.conf || (artalk.ctx && artalk.ctx.conf); if (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) { console.error('Artalk saveConf error:', e); } }; // Hook into multiple events to ensure we capture the config after it's loaded // 'conf-loaded' is the ideal event if supported artalk.on('conf-loaded', saveConf); // 'mounted' is when the widget is ready artalk.on('mounted', saveConf); // 'list-loaded' means comments (and likely config) are fetched artalk.on('list-loaded', saveConf); // Fallback: Poll check after a few seconds (for slow networks) setTimeout(saveConf, 2000); setTimeout(saveConf, 5000); // ── Badge system: 友链 / 友圈 / 评论等级 ── var badgesInjected = false; var normalizeHost = function(h) { return (h || '').replace(/^www\./, ''); }; var buildMap = function(data) { var hosts = new Map(); var names = new Map(); if (data && data.hosts) { data.hosts.forEach(function(item) { if (item.host) hosts.set(item.host, item.title || item.host); }); } if (data && data.names) { data.names.forEach(function(n) { if (n.lower) names.set(n.lower, n.title); }); } return { hosts: hosts, names: names }; }; var linkMap = null; var feedMap = null; // Comment rank table — Song dynasty 九品十八阶 // Color scale: warm → cool gradient for visual hierarchy var RANK_TABLE = [ { min: 2000, title: '正一品·太师', short: '太师', color: '#c5881b', bg: 'rgba(197,136,27,0.16)' }, { min: 1400, title: '从一品·太尉', short: '太尉', color: '#cc942a', bg: 'rgba(204,148,42,0.13)' }, { min: 1050, title: '正二品·参知政事', short: '参知政事', color: '#d46d33', bg: 'rgba(212,109,51,0.16)' }, { min: 800, title: '从二品·节度使', short: '节度使', color: '#d87940', bg: 'rgba(216,121,64,0.13)' }, { min: 600, title: '正三品·御史中丞', short: '御史中丞', color: '#cf4a58', bg: 'rgba(207,74,88,0.16)' }, { min: 480, title: '从三品·秘书监', short: '秘书监', color: '#d45d68', bg: 'rgba(212,93,104,0.13)' }, { min: 380, title: '正四品·谏议大夫', short: '谏议大夫', color: '#b05385', bg: 'rgba(176,83,133,0.16)' }, { min: 300, title: '从四品·侍读学士', short: '侍读学士', color: '#b8638f', bg: 'rgba(184,99,143,0.13)' }, { min: 240, title: '正五品·给事中', short: '给事中', color: '#9570d6', bg: 'rgba(149,112,214,0.16)' }, { min: 190, title: '从五品·知州', short: '知州', color: '#a07bda', bg: 'rgba(160,123,218,0.13)' }, { min: 150, title: '正六品·侍御史', short: '侍御史', color: '#5f7ace', bg: 'rgba(95,122,206,0.16)' }, { min: 115, title: '从六品·通判', short: '通判', color: '#6b85d0', bg: 'rgba(107,133,208,0.13)' }, { min: 85, title: '正七品·知县', short: '知县', color: '#2a9a84', bg: 'rgba(42,154,132,0.16)' }, { min: 60, title: '从七品·殿中侍御史', short: '殿中侍御史', color: '#34a390', bg: 'rgba(52,163,144,0.13)' }, { min: 40, title: '正八品·大理评事', short: '大理评事', color: '#628530', bg: 'rgba(98,133,48,0.16)' }, { min: 25, title: '从八品·录事参军', short: '录事参军', color: '#6e913d', bg: 'rgba(110,145,61,0.13)' }, { min: 15, title: '正九品·主簿', short: '主簿', color: '#8a7050', bg: 'rgba(138,112,80,0.16)' }, { min: 5, title: '从九品·司户参军', short: '司户参军', color: '#917a5c', bg: 'rgba(145,122,92,0.13)' }, { min: 0, title: '庶民', short: '庶民', color: '#8a8e98', bg: 'rgba(138,142,152,0.10)' } ]; var getRank = function(count) { for (var i = 0; i < RANK_TABLE.length; i++) { if (count >= RANK_TABLE[i].min) return RANK_TABLE[i]; } return RANK_TABLE[RANK_TABLE.length - 1]; }; // localStorage-based cross-page user comment counter with page dedup var USER_COUNT_KEY = 'atk_user_counts_v2'; var loadUserCounts = function() { try { var raw = localStorage.getItem(USER_COUNT_KEY); return raw ? JSON.parse(raw) : { _pages: {} }; } catch(e) { return { _pages: {} }; } }; var saveUserCounts = function(counts) { try { localStorage.setItem(USER_COUNT_KEY, JSON.stringify(counts)); } catch(e) {} }; var getPageKey = function() { return window.artalkConfig ? (window.artalkConfig.pageKey || window.location.pathname) : window.location.pathname; }; var processBadges = function() { if (!linkMap && !feedMap) return; var data = loadUserCounts(); var pageKey = getPageKey(); var pages = data._pages || {}; var isNewPage = !pages[pageKey]; // Count per-user comments on current page var pageCounts = {}; document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) { var nickEl = wrap.querySelector('.atk-nick'); if (!nickEl) return; var name = (nickEl.textContent || '').trim(); if (name) pageCounts[name] = (pageCounts[name] || 0) + 1; }); // Accumulate: only add counts if this page hasn't been processed before var userCounts = {}; for (var name in pageCounts) { var prev = data[name] || 0; if (isNewPage) { data[name] = prev + pageCounts[name]; } userCounts[name] = data[name]; } if (isNewPage) { pages[pageKey] = true; data._pages = pages; } saveUserCounts(data); document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) { var nickEl = wrap.querySelector('.atk-nick'); var avatarEl = wrap.querySelector('.atk-avatar'); if (!nickEl) return; var nickText = (nickEl.textContent || '').trim(); var nickLower = nickText.toLowerCase(); var nickLink = nickEl.querySelector('a[href]'); var commentHost = null; if (nickLink) { try { commentHost = normalizeHost(new URL(nickLink.getAttribute('href')).hostname); } catch(e) {} } // 1. 友链 badge (green gradient) — from /api/links?all=1 if (linkMap && !nickEl.querySelector('.atk-link-badge')) { var linkMatch = null; if (commentHost && linkMap.hosts.has(commentHost)) { linkMatch = linkMap.hosts.get(commentHost); } else if (linkMap.names.has(nickLower)) { linkMatch = linkMap.names.get(nickLower); } if (linkMatch) { var lb = document.createElement('span'); lb.className = 'atk-link-badge'; lb.textContent = '友链'; lb.title = '友链: ' + linkMatch; nickEl.appendChild(lb); } } // 2. 友圈 badge (gold gradient) — from /api/feeds if (feedMap && !nickEl.querySelector('.atk-feed-badge')) { var feedMatch = null; if (commentHost && feedMap.hosts.has(commentHost)) { feedMatch = feedMap.hosts.get(commentHost); } else if (feedMap.names.has(nickLower)) { feedMatch = feedMap.names.get(nickLower); } if (feedMatch) { var fb = document.createElement('span'); fb.className = 'atk-feed-badge'; fb.textContent = '友圈'; fb.title = '友圈: ' + feedMatch; nickEl.appendChild(fb); } } // 3. Comment rank badge — inline next to name if (!nickEl.querySelector('.atk-rank-badge')) { var count = userCounts[nickText] || 0; var rank = getRank(count); var rb = document.createElement('span'); rb.className = 'atk-rank-badge'; rb.textContent = rank.short; rb.title = rank.title + ' (' + count + '条评论)'; rb.style.color = rank.color; rb.style.backgroundColor = rank.bg; rb.style.borderColor = rank.bg.replace(/[\d.]+\)$/, function(m) { var v = parseFloat(m) + 0.06; return Math.min(v, 0.26).toFixed(2) + ')'; }); var idx = RANK_TABLE.indexOf(rank); if (idx > 0) { var next = RANK_TABLE[idx - 1]; var diff = next.min - count; if (diff > 0) { var progress = Math.round((count - rank.min) / (next.min - rank.min) * 100); rb.title += ' — 距' + next.short + '还差' + diff + '条 (' + progress + '%)'; } } nickEl.appendChild(rb); } }); }; var injectAllBadges = function() { if (badgesInjected) return; badgesInjected = true; if (window.friendLinks) linkMap = buildMap(window.friendLinks); if (window.friendFeeds) feedMap = buildMap(window.friendFeeds); if (linkMap || feedMap) { processBadges(); } }; artalk.on('list-loaded', function() { // Badge injection badgesInjected = false; setTimeout(injectAllBadges, 300); // Add staggered animation to the comment list var list = document.querySelector('.atk-list'); if (list) { list.setAttribute('uk-scrollspy', 'target: > .atk-comment-wrap; cls: uk-animation-slide-bottom-small; delay: 50'); } // Apply granular animations to inner elements document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) { var avatar = wrap.querySelector('.atk-avatar'); if (avatar) avatar.setAttribute('uk-scrollspy', 'cls: uk-animation-scale-up; delay: 200'); var main = wrap.querySelector('.atk-main'); if (main) main.setAttribute('uk-scrollspy', 'cls: uk-animation-slide-right-small; delay: 300'); var nick = wrap.querySelector('.atk-nick'); if (nick) nick.setAttribute('uk-scrollspy', 'cls: uk-animation-slide-top-small; delay: 350'); }); // Trigger lazy load update if (window.lazyLoadAgain) { window.lazyLoadAgain(); } }); } catch (e) { console.error('Artalk init failed:', e); } };