Files
blog/themes/Ying/assets/js/modules/artalk.js
T
zqlit e84de9033d
Deploy to Production / pre-check (push) Successful in 1m9s
Deploy to Production / build (push) Successful in 4m9s
Deploy to Production / deploy-edgeone (push) Successful in 3m46s
Deploy to Production / finalize (push) Successful in 28s
Deploy to Production / notify-failure (push) Skipped
友链申请卡片重设计(头像+昵称+徽章+主题色) + 弹窗横向滚动条/显隐bug修复
2026-10-03 14:28:50 +08:00

812 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 0;
var enhanced = 0;
// 昵称输入框
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');
enhanced++;
}
// 邮箱输入框
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');
enhanced++;
}
// 网址输入框
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');
enhanced++;
}
// 内容输入框(避免被误识别为昵称)
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');
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);
}
};
// 立即执行一次
enhanceEditorInputs();
// 评论框加载完成(Artalk 内置事件)
artalk.on('mounted', function() {
setTimeout(function() {
enhanceEditorInputs();
restoreAutoFillData();
}, 100);
});
// 评论列表加载完成(PJAX 切换后)
artalk.on('list-loaded', function() {
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) {}
});
// 兜底:定时检查(防止某些情况下事件未触发)
var enhanceInterval = setInterval(function() {
enhanceEditorInputs();
restoreAutoFillData();
// 找到所有字段都已增强后停止
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 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;
};
// ── 评论区徽章统一注入(comment-rendered 每条触发)──
// 优先级:官方系统徽章(博主等,Artalk 自渲染)独立显示;
// 主题徽章互斥取最高一级:友链 > 友圈 > 评论官职。
// 数据源:window.friendLinks/friendFeeds(友链、友圈)+ 后端 rank_* 字段(官职)。
var ensureBadgeMaps = function() {
if (!linkMap && window.friendLinks) linkMap = buildMap(window.friendLinks);
if (!feedMap && window.friendFeeds) feedMap = buildMap(window.friendFeeds);
return !!(linkMap || feedMap);
};
var matchBadge = function(map, host, nickLower) {
if (!map) return null;
if (host && map.hosts.has(host)) return map.hosts.get(host);
if (map.names.has(nickLower)) return map.names.get(nickLower);
return null;
};
var injectBadge = function(nickEl, cls, text, title, color, bg) {
if (nickEl.querySelector('.' + cls)) return;
var b = document.createElement('span');
b.className = cls;
b.textContent = text;
if (title) b.title = title;
if (color) b.style.color = color;
if (bg) b.style.backgroundColor = bg;
nickEl.appendChild(b);
};
artalk.on('comment-rendered', function(node) {
try {
var data = null;
if (node && typeof node.getData === 'function') data = node.getData();
else if (node && node.comment && typeof node.comment.getData === 'function') data = node.comment.getData();
else data = node;
if (!data) return;
var el = null;
if (node && node.$el) el = node.$el;
else if (node && typeof node.getEl === 'function') el = node.getEl();
if (!el || !el.querySelector) return;
var nickEl = el.querySelector('.atk-nick');
if (!nickEl) return;
if (!ensureBadgeMaps()) return;
var nickLower = (data.nick || '').toLowerCase();
var host = null;
try { if (data.link) host = normalizeHost(new URL(data.link).hostname); } catch (e) {}
var linkMatch = matchBadge(linkMap, host, nickLower);
var feedMatch = matchBadge(feedMap, host, nickLower);
// 官方系统徽章(博主等)由 Artalk 渲染在 .atk-badge-wrap,把它挪进
// 昵称元素内,保证视觉顺序:系统徽章 > 头衔 > 友链 > 友圈 > 官职
var badgeWrap = el.querySelector('.atk-badge-wrap');
if (badgeWrap && badgeWrap.parentNode !== nickEl) nickEl.appendChild(badgeWrap);
// 1) 头衔徽章(金色,管理员在后台手动设置,仅次于系统徽章)
if (data.title_name && !nickEl.querySelector('.atk-title-badge')) {
injectBadge(nickEl, 'atk-title-badge', data.title_name, '头衔: ' + data.title_name, '#b8860b', 'rgba(197,136,27,.16)');
}
// 2) 友链
if (linkMatch) injectBadge(nickEl, 'atk-link-badge', '友链', '友链: ' + linkMatch);
// 3) 友圈
if (feedMatch) injectBadge(nickEl, 'atk-feed-badge', '友圈', '友圈: ' + feedMatch);
// 4) 评论官职(按全站评论数)
if (data.rank_name && !nickEl.querySelector('.atk-rank-badge')) {
var tip = data.rank_title || data.rank_name;
if (data.rank_count != null) tip += ' (' + data.rank_count + '条评论)';
injectBadge(nickEl, 'atk-rank-badge', data.rank_name, tip, data.rank_color || '', data.rank_bg || '');
}
} catch (e) { /* 徽章注入失败不影响评论 */ }
// ── 置顶评论卡片化:管理员置顶且内容含「友链自助申请」的评论,
// 整条解析成申请入口卡片(点击打开申请弹窗),其余置顶评论不受影响。
try {
var d2 = null;
if (node && typeof node.getData === 'function') d2 = node.getData();
else if (node && node.comment && typeof node.comment.getData === 'function') d2 = node.comment.getData();
else d2 = node;
var el2 = null;
if (node && node.$el) el2 = node.$el;
else if (node && typeof node.getEl === 'function') el2 = node.getEl();
if (!d2 || !d2.is_pinned || !el2) return;
var content = String(d2.content || '');
if (content.indexOf('友链自助申请') === -1) return;
if (el2.querySelector('.atk-fl-apply')) return;
el2.classList.add('atk-fl-apply');
el2.style.cursor = 'pointer';
el2.title = '点击申请友链';
// 先取原评论的头像 / 昵称 / 官方徽章(博主),再隐藏常规元素
var avImg = el2.querySelector('.atk-avatar img');
var avatarSrc = avImg ? avImg.getAttribute('src') : '';
var nickEl2 = el2.querySelector('.atk-nick');
var sysBadge = (nickEl2 || el2).querySelector('.atk-badge-wrap');
var nickText = String(d2.nick || '博主');
// 隐藏评论常规元素(头像列/昵称行/页脚操作),整条变成一张卡片
var hd = el2.querySelector('.atk-header');
var ft = el2.querySelector('.atk-footer');
var av0 = el2.querySelector('.atk-avatar');
var meta = el2.querySelector('.atk-meta');
if (hd) hd.style.display = 'none';
if (ft) ft.style.display = 'none';
if (av0) av0.style.display = 'none';
if (meta) meta.style.display = 'none';
var main = el2.querySelector('.atk-main') || el2;
main.style.marginLeft = '0'; // 卡片占满整行,原头像列不再占位(修复错位)
var contentEl = main.querySelector('.atk-content');
var card = document.createElement('div');
card.className = 'atk-fl-apply';
card.style.cssText = 'display:flex;align-items:center;gap:12px;padding:14px 16px;margin:6px 0 2px;' +
'background:var(--bg-card,#fff);border:1px solid var(--theme-main-30,rgba(7,193,96,.3));' +
'border-radius:10px;transition:border-color .2s,box-shadow .2s;';
card.innerHTML =
'<img src="' + avatarSrc + '" alt="" referrerpolicy="no-referrer" style="flex:none;width:44px;height:44px;' +
'border-radius:8px;object-fit:cover;box-shadow:0 0 0 2px var(--theme-main-20,rgba(7,193,96,.2))">' +
'<span style="flex:1;min-width:0">' +
'<span style="display:flex;align-items:center;gap:6px;font-size:14px;font-weight:600;color:var(--text-main,#333)">' +
'<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + nickText.replace(/</g, '&lt;') + '</span>' +
'</span>' +
'<span style="display:block;font-size:13px;margin-top:3px;color:var(--text-desc,#777);line-height:1.5">' +
'<b style="color:var(--theme-main,#07c160);font-weight:600">友链自助申请</b>' +
'<span style="color:var(--text-muted,#999)"> · </span>想和我交换友链?点这里填写申请,审核结果会邮件通知你' +
'</span>' +
'</span>' +
'<span style="flex:none;color:var(--theme-main,#07c160);font-size:20px;line-height:1">›</span>';
// 昵称行追加官方徽章(博主),样式与评论区徽章一致
if (sysBadge) {
var bw = sysBadge.cloneNode(true);
bw.style.marginLeft = '2px';
var nickRow = card.querySelector('span > span');
nickRow.appendChild(bw);
}
if (contentEl) {
contentEl.innerHTML = '';
contentEl.appendChild(card);
contentEl.style.cursor = 'pointer';
} else {
main.appendChild(card);
}
el2.addEventListener('click', function () {
if (window.flApplyOpenModal) window.flApplyOpenModal();
});
el2.addEventListener('mouseenter', function () {
card.style.borderColor = 'var(--theme-main,#07c160)';
card.style.boxShadow = '0 2px 12px var(--theme-main-20,rgba(7,193,96,.2))';
});
el2.addEventListener('mouseleave', function () {
card.style.borderColor = 'var(--theme-main-30,rgba(7,193,96,.3))';
card.style.boxShadow = 'none';
});
} catch (e) { /* 卡片化失败不影响评论 */ }
});
artalk.on('list-loaded', function() {
// 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);
}
};