420 lines
18 KiB
JavaScript
420 lines
18 KiB
JavaScript
// Global Artalk Initialization Function (Supports PJAX)
|
|
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
|
|
if (typeof window.artalkInitCount === 'undefined') {
|
|
window.artalkInitCount = 0;
|
|
}
|
|
window.artalkInitCount++;
|
|
|
|
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');
|
|
}
|
|
|
|
// 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:not(.atk-ai-comment)').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:not(.atk-ai-comment)').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);
|
|
|
|
if (window.artalkConfig.aiComment) {
|
|
var ai = window.artalkConfig.aiComment;
|
|
var listBody = document.querySelector('.atk-list .atk-list-body');
|
|
if (listBody && !listBody.querySelector('.atk-ai-comment')) {
|
|
var aiCommentHtml = `
|
|
<div class="atk-comment-wrap atk-comment atk-ai-comment" id="atk-comment-ai">
|
|
<div class="atk-avatar" uk-scrollspy="cls: uk-animation-scale-up; delay: 200">
|
|
<img src="${ai.avatar}" alt="${ai.name}">
|
|
</div>
|
|
<div class="atk-main" uk-scrollspy="cls: uk-animation-slide-right-small; delay: 300">
|
|
<div class="atk-header">
|
|
<span class="atk-nick" uk-scrollspy="cls: uk-animation-slide-top-small; delay: 350">${ai.name}</span>
|
|
<span class="atk-badge">${window.artalkConfig.aiBadge || 'AI'}</span>
|
|
<span class="atk-date">${ai.date}</span>
|
|
</div>
|
|
<div class="atk-body">
|
|
<div class="atk-content">${ai.content}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
listBody.insertAdjacentHTML('afterbegin', aiCommentHtml);
|
|
|
|
var noComment = document.querySelector('.atk-list-no-comment');
|
|
if (noComment) {
|
|
noComment.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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:not(.atk-ai-comment)').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();
|
|
}
|
|
|
|
// Force hide "No Comment" if AI comment exists
|
|
if (window.artalkConfig.aiComment) {
|
|
var noComment = document.querySelector('.atk-list-no-comment');
|
|
var aiComment = document.querySelector('.atk-ai-comment');
|
|
if (aiComment && noComment) {
|
|
noComment.style.display = 'none';
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (e) {
|
|
console.error('Artalk init failed:', e);
|
|
}
|
|
};
|