Deploy to Production / pre-check (push) Successful in 53s
Deploy to Production / build (push) Canceled after 3m23s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
876 lines
40 KiB
JavaScript
876 lines
40 KiB
JavaScript
// 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 里,头衔/友链/友圈/官职
|
||
// 则是直接插进昵称的 —— 两套 DOM 会导致尺寸与垂直对齐都不一致。这里把包装层
|
||
// 拆掉,让所有徽章都成为昵称的直接子元素,视觉顺序:
|
||
// 系统徽章 > 头衔 > 友链 > 友圈 > 官职
|
||
var badgeWrap = el.querySelector('.atk-badge-wrap');
|
||
if (badgeWrap && badgeWrap.parentNode !== nickEl) nickEl.appendChild(badgeWrap);
|
||
|
||
// 1) 头衔徽章(金色,管理员在后台手动设置,仅次于系统徽章;配色走 .atk-title-badge 类)
|
||
if (data.title_name && !nickEl.querySelector('.atk-title-badge')) {
|
||
injectBadge(nickEl, 'atk-title-badge', data.title_name, '头衔: ' + data.title_name);
|
||
}
|
||
// 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 || '');
|
||
}
|
||
|
||
// 5) 拆掉 .atk-badge-wrap 包装层:把 博主/置顶 提成昵称的直接子元素(保持原顺序)
|
||
var wrap = nickEl.querySelector('.atk-badge-wrap');
|
||
if (wrap && wrap.parentNode) {
|
||
var wrapParent = wrap.parentNode;
|
||
while (wrap.firstChild) wrapParent.insertBefore(wrap.firstChild, wrap);
|
||
wrapParent.removeChild(wrap);
|
||
}
|
||
// 6) 所有徽章挂同一个类,几何样式(尺寸/内边距/圆角/字重/对齐)只由
|
||
// .atk-cmt-badge 一处定义,从此不会有"两种徽章尺寸不一致"的问题
|
||
var allBadges = nickEl.querySelectorAll(
|
||
'.atk-badge, .atk-pinned-badge, .atk-title-badge, .atk-link-badge, .atk-feed-badge, .atk-rank-badge'
|
||
);
|
||
for (var bi = 0; bi < allBadges.length; bi++) allBadges[bi].classList.add('atk-cmt-badge');
|
||
} catch (e) { /* 徽章注入失败不影响评论 */ }
|
||
|
||
// ── 友链申请入口卡片:评论内容就是约定 URL(https://usj.cc/link-apply)时,
|
||
// 整条评论解析成申请入口卡片(点击打开申请弹窗),其余评论不受影响。
|
||
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 || !el2) return;
|
||
var content = String(d2.content || '').trim().replace(/\/+$/, '').toLowerCase();
|
||
if (content !== 'https://usj.cc/link-apply') return;
|
||
// 只转换顶层评论:回复里出现同一串 URL 不该被吃掉(也避免出现游离的卡片)
|
||
if (d2.rid) return;
|
||
|
||
// 幂等 + 自愈:class 每次渲染都补一次(CSS 靠它隐藏整条评论),
|
||
// 卡片始终挂在评论列表"上方"(.atk-list-body 的第一个子节点),
|
||
// 这样发新评论/切换排序都不会把卡片夹到评论中间。
|
||
el2.classList.add('atk-fl-apply');
|
||
var listBody = document.querySelector('.atk-list-body') || document.querySelector('.atk-list');
|
||
var oldHolder = document.querySelector('.atk-fl-card-wrap');
|
||
if (oldHolder && listBody && oldHolder.parentNode === listBody && oldHolder.isConnected) return;
|
||
if (oldHolder && oldHolder.parentNode) oldHolder.parentNode.removeChild(oldHolder);
|
||
|
||
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');
|
||
// wrap 已在上面拆掉,博主徽章现在是昵称的直接子元素
|
||
var sysBadge = (nickEl2 || el2).querySelector('.atk-badge') ||
|
||
el2.querySelector('.atk-badge');
|
||
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;
|
||
var contentEl = main.querySelector('.atk-content');
|
||
// 卡片挂在 .atk-comment 这一级,而不是塞进 .atk-content 里:
|
||
// .atk-main 被主题锁了 margin-left:34px !important(头像缩进),塞在里面
|
||
// 卡片永远比评论输入框窄一圈;提到 .atk-comment 这一级就能和输入框同宽对齐。
|
||
var commentEl = el2.querySelector('.atk-comment') || main.parentElement || el2;
|
||
if (main && main !== el2) main.style.display = 'none'; // 常规内容整块隐藏
|
||
|
||
var FL_DESC = '想和我交换友链?点击申请,审核结果会邮件通知你';
|
||
var card = document.createElement('div');
|
||
card.className = 'atk-fl-card'; // 独立类名:容器用的是 atk-fl-apply 做标记
|
||
// 跟随评论列表原生观感:白底 + 细边框,只有按钮带主题色
|
||
card.style.cssText = 'display:flex;align-items:center;gap:12px;padding:12px 14px;margin:2px 0;' +
|
||
'background:var(--bg-card,#fff);border:1px solid var(--border-main,#eeeeee);' +
|
||
'border-radius:10px;transition:border-color .2s,background .2s;';
|
||
card.innerHTML =
|
||
'<img class="uk-animation-scale-up" src="' + avatarSrc + '" alt="" referrerpolicy="no-referrer" ' +
|
||
'style="flex:none;width:40px;height:40px;border-radius:6px;object-fit:cover">' +
|
||
'<span style="flex:1;min-width:0">' +
|
||
'<span style="display:flex;align-items:center;gap:0;font-size:14px;font-weight:600;color:var(--text-main,#333)">' +
|
||
'<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + nickText.replace(/</g, '<') + '</span>' +
|
||
'</span>' +
|
||
'<span style="display:block;font-size:13px;margin-top:2px;color:var(--text-desc,#777);line-height:1.5">' +
|
||
'<span class="atk-fl-typed"></span><i class="atk-fl-caret"></i>' +
|
||
'</span>' +
|
||
'</span>' +
|
||
'<span class="atk-fl-btn" style="flex:none;background:var(--theme-main-20,rgba(7,193,96,.2));color:var(--theme-main,#07c160);' +
|
||
'font-size:13px;font-weight:600;padding:6px 14px;border-radius:5px;white-space:nowrap">申请友链</span>';
|
||
// 昵称行追加官方徽章(博主),样式与评论区徽章一致
|
||
if (sysBadge) {
|
||
var bw = sysBadge.cloneNode(true);
|
||
bw.style.marginLeft = '0';
|
||
var nickRow = card.querySelector('span > span');
|
||
nickRow.appendChild(bw);
|
||
}
|
||
// 卡片包一层容器挂到列表上方(宽度与评论区一致,且不受列表排序/新增影响)
|
||
if (contentEl) contentEl.innerHTML = '';
|
||
var holder = document.createElement('div');
|
||
holder.className = 'atk-fl-card-wrap';
|
||
holder.appendChild(card);
|
||
if (listBody) {
|
||
if (listBody.firstChild) listBody.insertBefore(holder, listBody.firstChild);
|
||
else listBody.appendChild(holder);
|
||
} else {
|
||
commentEl.appendChild(holder);
|
||
}
|
||
// 点击卡片即打开申请弹窗(原来挂在 el2 上,现在卡片已移出评论节点)
|
||
holder.addEventListener('click', function () {
|
||
if (window.flApplyOpenModal) window.flApplyOpenModal();
|
||
});
|
||
|
||
// ── 打字机效果(PJAX 安全:卡片被移除/替换时定时器自动停止) ──
|
||
// 注意:不要用 prefers-reduced-motion 跳过打字 —— Windows 关闭"动画效果"
|
||
// 或部分浏览器默认就会报告 reduce,会导致效果完全不出现。这里始终打字。
|
||
try {
|
||
var typedEl = card.querySelector('.atk-fl-typed');
|
||
var caretEl = card.querySelector('.atk-fl-caret');
|
||
var alive = function () { return card.isConnected !== undefined ? card.isConnected : document.body.contains(card); };
|
||
if (typedEl) {
|
||
var ti = 0;
|
||
setTimeout(function () {
|
||
if (!alive()) return;
|
||
var timer = setInterval(function () {
|
||
if (!alive()) { clearInterval(timer); return; } // PJAX 切页后自动停止
|
||
ti += 1;
|
||
typedEl.textContent = FL_DESC.slice(0, ti);
|
||
if (ti >= FL_DESC.length) {
|
||
clearInterval(timer);
|
||
if (caretEl) caretEl.style.display = 'none';
|
||
}
|
||
}, 42);
|
||
}, 320); // 等入场动画播完再打字,观感更自然
|
||
}
|
||
} catch (e) {}
|
||
|
||
el2.addEventListener('click', function () {
|
||
if (window.flApplyOpenModal) window.flApplyOpenModal();
|
||
});
|
||
el2.addEventListener('mouseenter', function () {
|
||
card.style.borderColor = 'var(--theme-main,#07c160)';
|
||
card.style.background = 'var(--bg-card,#fff)';
|
||
});
|
||
el2.addEventListener('mouseleave', function () {
|
||
card.style.borderColor = 'var(--border-main,#eeeeee)';
|
||
});
|
||
} 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);
|
||
}
|
||
};
|