Files
blog/themes/Ying/assets/js/modules/artalk.js
T

775 lines
33 KiB
JavaScript
Raw Normal View History

2026-01-31 19:24:22 +08:00
// Global Artalk Initialization Function (Supports PJAX)
2026-06-25 18:49:06 +08:00
// ── 持久化 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 || {
2026-06-25 19:07:58 +08:00
observer: null,
running: false,
2026-06-25 18:49:06 +08:00
enhance: function() {
var self = this;
2026-06-25 19:07:58 +08:00
var count = 0;
2026-06-25 18:49:06 +08:00
2026-06-25 19:07:58 +08:00
// 昵称输入框
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++;
2026-06-25 18:49:06 +08:00
});
2026-06-25 19:07:58 +08:00
// 邮箱输入框
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;
2026-06-25 18:49:06 +08:00
},
// 监控 DOM 变化(PJAX 切换、动态加载)
observe: function() {
var self = this;
if (typeof MutationObserver === 'undefined') return;
2026-06-25 19:07:58 +08:00
if (this.observer) return;
2026-06-25 18:49:06 +08:00
2026-06-25 19:07:58 +08:00
this.observer = new MutationObserver(function(mutations) {
var shouldEnhance = false;
2026-06-25 18:49:06 +08:00
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) {
2026-06-25 19:07:58 +08:00
// 检查是否是 input 或 textarea 元素
if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
shouldEnhance = true;
break;
2026-06-25 18:49:06 +08:00
}
2026-06-25 19:07:58 +08:00
// 检查子节点
2026-06-25 18:49:06 +08:00
if (node.querySelectorAll) {
2026-06-25 19:07:58 +08:00
var inputs = node.querySelectorAll('input, textarea');
if (inputs.length > 0) {
shouldEnhance = true;
break;
2026-06-25 18:49:06 +08:00
}
}
}
}
2026-06-25 19:07:58 +08:00
if (shouldEnhance) break;
}
if (shouldEnhance) {
// 稍微延迟,确保 DOM 完全插入
setTimeout(function() { self.enhance(); }, 10);
2026-06-25 18:49:06 +08:00
}
});
2026-06-25 19:07:58 +08:00
this.observer.observe(document.body, { childList: true, subtree: true });
2026-06-25 18:49:06 +08:00
},
2026-06-25 19:07:58 +08:00
// 启动全局监控
2026-06-25 18:49:06 +08:00
start: function() {
2026-06-25 19:07:58 +08:00
if (this.running) return;
this.running = true;
if (window.console && console.log) {
console.log('[ArtalkEnhancer] 启动评论框字段增强');
}
// 立即尝试增强一次
2026-06-25 18:49:06 +08:00
this.enhance();
2026-06-25 19:07:58 +08:00
// 启动 DOM 监控
2026-06-25 18:49:06 +08:00
this.observe();
2026-06-25 19:07:58 +08:00
// 兜底:定时检查(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);
2026-06-25 18:49:06 +08:00
}
};
2026-06-25 19:07:58 +08:00
// 立即启动(不等待 DOMContentLoaded,尽早开始监控)
if (typeof window.__artalkEnhancer !== 'undefined') {
2026-06-25 18:49:06 +08:00
window.__artalkEnhancer.start();
}
2026-06-25 19:07:58 +08:00
// 监听 PJAX 切换:重置并重新增强
2026-06-25 18:49:06 +08:00
document.addEventListener('pjax:send', function() {
2026-06-25 19:07:58 +08:00
if (window.__artalkEnhancer && window.console && console.log) {
console.log('[ArtalkEnhancer] PJAX 切换,准备重新增强');
2026-06-25 18:49:06 +08:00
}
});
document.addEventListener('pjax:success', function() {
if (window.__artalkEnhancer) {
2026-06-25 19:07:58 +08:00
// 立即尝试
2026-06-25 18:49:06 +08:00
window.__artalkEnhancer.enhance();
2026-06-25 19:07:58 +08:00
// 延迟再试(Artalk 重新初始化需要时间)
setTimeout(function() { window.__artalkEnhancer.enhance(); }, 200);
setTimeout(function() { window.__artalkEnhancer.enhance(); }, 500);
setTimeout(function() { window.__artalkEnhancer.enhance(); }, 1000);
2026-06-25 18:49:06 +08:00
}
});
document.addEventListener('pjax:complete', function() {
if (window.__artalkEnhancer) {
2026-06-25 19:07:58 +08:00
setTimeout(function() { window.__artalkEnhancer.enhance(); }, 300);
setTimeout(function() { window.__artalkEnhancer.enhance(); }, 800);
2026-06-25 18:49:06 +08:00
}
});
2026-05-25 00:03:50 +08:00
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);
};
2026-01-31 19:24:22 +08:00
window.initArtalk = function() {
if (!document.querySelector("#Comments")) return;
2026-05-25 00:03:50 +08:00
2026-01-31 19:24:22 +08:00
// Check if configuration exists
if (!window.artalkConfig) {
console.warn('window.artalkConfig is not defined. Skipping Artalk init.');
return;
}
if (typeof Artalk === 'undefined') {
2026-05-25 00:03:50 +08:00
var artalkUrl = window.optionalAssetUrls && window.optionalAssetUrls.artalk;
if (!artalkUrl) return;
window.loadScriptOnce(artalkUrl, 'Artalk', window.initArtalk);
2026-01-31 19:24:22 +08:00
return;
}
2026-05-25 00:03:50 +08:00
2026-01-31 19:24:22 +08:00
try {
2026-01-31 22:06:19 +08:00
// 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
2026-06-25 18:49:06 +08:00
// (从 sessionStorage 加载,由文件顶部的逻辑处理)
2026-01-31 22:06:19 +08:00
if (typeof window.artalkInitCount === 'undefined') {
window.artalkInitCount = 0;
}
window.artalkInitCount++;
2026-06-25 18:49:06 +08:00
saveInitCount(); // 持久化到 sessionStorage
2026-01-31 22:06:19 +08:00
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
2026-02-05 21:53:08 +08:00
// Sync Theme Mode Immediately
var currentTheme = document.documentElement.getAttribute('data-theme');
if (typeof artalk.setDarkMode === 'function') {
artalk.setDarkMode(currentTheme === 'dark');
}
2026-06-25 18:49:06 +08:00
// ── 增强评论框字段属性 (兼容油猴自动填充脚本) ──
// 为 Artalk 输入框添加额外属性,确保第三方脚本(如博客网站留言评论信息自动填充)
// 能够通过 type/id/name/placeholder/class/aria-label 准确匹配
var enhanceEditorInputs = function() {
var editor = document.querySelector('.atk-editor');
2026-06-25 19:35:38 +08:00
if (!editor) return 0;
var enhanced = 0;
2026-06-25 18:49:06 +08:00
// 昵称输入框
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');
2026-06-25 19:35:38 +08:00
enhanced++;
2026-06-25 18:49:06 +08:00
}
// 邮箱输入框
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');
2026-06-25 19:35:38 +08:00
enhanced++;
2026-06-25 18:49:06 +08:00
}
// 网址输入框
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');
2026-06-25 19:35:38 +08:00
enhanced++;
2026-06-25 18:49:06 +08:00
}
// 内容输入框(避免被误识别为昵称)
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');
2026-06-25 19:35:38 +08:00
enhanced++;
}
// 触发油猴脚本自动填充(延迟到评论框稳定后)
if (enhanced > 0) {
triggerAutoFill(editor);
}
return enhanced;
};
// ── 触发油猴脚本自动填充 ──
// 问题:油猴脚本可能在评论框还没完全加载时就填充了,
// 导致填充的数据在 Artalk 重新渲染后丢失。
// 解决:使用 MutationObserver 监控评论框 DOM 变化,
// 在评论框稳定后(300ms 内没有变化)才触发填充。
var pendingFillTimer = null;
var fillDebounceDelay = 300; // 评论框稳定后 300ms 才触发填充
var triggerAutoFill = function(editor) {
if (pendingFillTimer) {
clearTimeout(pendingFillTimer);
}
// 延迟触发,等待评论框稳定
pendingFillTimer = setTimeout(function() {
pendingFillTimer = null;
// 再次检查评论框是否存在
editor = document.querySelector('.atk-editor');
if (!editor) return;
// 获取评论框中的输入框
var nickInput = editor.querySelector('input[name="nick"]');
var emailInput = editor.querySelector('input[name="email"]');
var linkInput = editor.querySelector('input[name="link"]');
// 保存当前输入框的值到 sessionStorage
var fillData = {
nick: nickInput ? nickInput.value : '',
email: emailInput ? emailInput.value : '',
link: linkInput ? linkInput.value : '',
timestamp: Date.now()
};
// 如果有填充数据,保存到 sessionStorage
if (fillData.nick || fillData.email || fillData.link) {
try {
sessionStorage.setItem('artalk_autofill_data', JSON.stringify(fillData));
console.log('[ArtalkEnhancer] 保存填充数据到 sessionStorage:', fillData);
} catch(e) {}
}
// 手动触发 input 事件,通知可能存在的自动填充脚本
// 一些油猴脚本会监听 input/change 事件来检测新字段
[nickInput, emailInput, linkInput].forEach(function(input) {
if (input && input.value) {
input.dispatchEvent(new Event('input', { bubbles: true }));
}
});
// 尝试调用油猴脚本的公开 API(如果有)
// 例如 Greasemonkey/Tampermonkey 的 GM_setValue 配合 GM_getValue
if (typeof window.GM !== 'undefined' && window.GM.setValue) {
// 通知油猴脚本评论框已加载完成
window.GM_setValue('artalk_editor_ready', true);
}
console.log('[ArtalkEnhancer] 评论框稳定,触发自动填充');
}, fillDebounceDelay);
};
// ── 恢复 sessionStorage 中的填充数据 ──
// 问题:Artalk 评论框可能在填充后又重新渲染,导致填充的数据丢失。
// 解决:在评论框稳定后,检查 sessionStorage 是否有之前保存的填充数据,
// 如果有,自动恢复到输入框中。
var restoreAutoFillData = function() {
var editor = document.querySelector('.atk-editor');
if (!editor) return;
try {
var stored = sessionStorage.getItem('artalk_autofill_data');
if (!stored) return;
var fillData = JSON.parse(stored);
if (!fillData || !fillData.timestamp) return;
// 数据是否在 24 小时内有效
var maxAge = 24 * 60 * 60 * 1000; // 24 小时
if (Date.now() - fillData.timestamp > maxAge) {
sessionStorage.removeItem('artalk_autofill_data');
return;
}
// 恢复到输入框
var nickInput = editor.querySelector('input[name="nick"]');
var emailInput = editor.querySelector('input[name="email"]');
var linkInput = editor.querySelector('input[name="link"]');
if (fillData.nick && nickInput && !nickInput.value) {
nickInput.value = fillData.nick;
nickInput.dispatchEvent(new Event('input', { bubbles: true }));
}
if (fillData.email && emailInput && !emailInput.value) {
emailInput.value = fillData.email;
emailInput.dispatchEvent(new Event('input', { bubbles: true }));
}
if (fillData.link && linkInput && !linkInput.value) {
linkInput.value = fillData.link;
linkInput.dispatchEvent(new Event('input', { bubbles: true }));
}
if (fillData.nick || fillData.email || fillData.link) {
console.log('[ArtalkEnhancer] 从 sessionStorage 恢复填充数据:', fillData);
}
} catch(e) {
console.warn('[ArtalkEnhancer] 恢复填充数据失败:', e);
2026-06-25 18:49:06 +08:00
}
};
// 立即执行一次
enhanceEditorInputs();
2026-06-25 19:35:38 +08:00
// 评论框加载完成(Artalk 内置事件)
2026-06-25 18:49:06 +08:00
artalk.on('mounted', function() {
2026-06-25 19:35:38 +08:00
setTimeout(function() {
enhanceEditorInputs();
restoreAutoFillData();
}, 100);
2026-06-25 18:49:06 +08:00
});
2026-06-25 19:35:38 +08:00
// 评论列表加载完成(PJAX 切换后)
2026-06-25 18:49:06 +08:00
artalk.on('list-loaded', function() {
2026-06-25 19:35:38 +08:00
setTimeout(function() {
enhanceEditorInputs();
restoreAutoFillData();
}, 200);
});
// 评论发送成功后保存填充数据
artalk.on('comment-submit', function() {
try {
var editor = document.querySelector('.atk-editor');
if (editor) {
var fillData = {
nick: editor.querySelector('input[name="nick"]') ? editor.querySelector('input[name="nick"]').value : '',
email: editor.querySelector('input[name="email"]') ? editor.querySelector('input[name="email"]').value : '',
link: editor.querySelector('input[name="link"]') ? editor.querySelector('input[name="link"]').value : '',
timestamp: Date.now()
};
if (fillData.nick || fillData.email || fillData.link) {
sessionStorage.setItem('artalk_autofill_data', JSON.stringify(fillData));
console.log('[ArtalkEnhancer] 评论提交后保存填充数据');
}
}
} catch(e) {}
2026-06-25 18:49:06 +08:00
});
// 兜底:定时检查(防止某些情况下事件未触发)
var enhanceInterval = setInterval(function() {
enhanceEditorInputs();
2026-06-25 19:35:38 +08:00
restoreAutoFillData();
2026-06-25 18:49:06 +08:00
// 找到所有字段都已增强后停止
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);
2026-01-31 22:06:19 +08:00
// 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) {
2026-01-31 22:52:19 +08:00
// 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));
2026-01-31 22:06:19 +08:00
// 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);
2026-05-24 19:58:07 +08:00
2026-01-31 22:06:19 +08:00
// '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);
2026-01-31 19:24:22 +08:00
2026-05-25 12:23:00 +08:00
// ── Badge system: 友链 / 友圈 / 评论等级 ──
var badgesInjected = false;
2026-01-31 19:24:22 +08:00
2026-05-25 12:23:00 +08:00
var normalizeHost = function(h) {
return (h || '').replace(/^www\./, '');
};
2026-01-31 19:24:22 +08:00
2026-05-25 12:23:00 +08:00
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);
2026-01-31 19:24:22 +08:00
});
2026-05-25 12:23:00 +08:00
}
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 九品十八阶
2026-05-25 13:55:45 +08:00
// Color scale: warm → cool gradient for visual hierarchy
2026-05-25 12:23:00 +08:00
var RANK_TABLE = [
2026-05-25 13:55:45 +08:00
{ 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)' }
2026-05-25 12:23:00 +08:00
];
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 = {};
2026-06-02 10:56:59 +08:00
document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) {
2026-05-25 12:23:00 +08:00
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);
2026-06-02 10:56:59 +08:00
document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) {
2026-05-25 12:23:00 +08:00
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 + '条评论)';
2026-05-25 13:55:45 +08:00
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) + ')';
});
2026-05-25 12:23:00 +08:00
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
2026-06-02 10:56:59 +08:00
document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) {
2026-05-25 12:23:00 +08:00
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();
}
});
2026-01-31 19:24:22 +08:00
} catch (e) {
console.error('Artalk init failed:', e);
}
};