给 Twikoo 评论加修仙境界徽章

AI 摘要

Twikoo 没有「按评论活跃度给用户分级」的功能。想要,就得自己动手。这篇文章记录我怎么在纯前端给评论区加了修仙境界徽章:访客每评论几次就升一级,从炼气到飞升,博主本人凌驾六境之上。全程不改后端、不动 Twikoo 源码,一个 JS 文件搞定。

最终效果

  • 每条评论的昵称后面跟着一枚发光胶囊:炼气 / 筑基 / 金丹 / 元婴 / 化神 / 飞升
  • 等级由该用户的全站评论总次数决定,评论越多境界越高
  • 每个境界一套专属渐变 + 双层光晕:炼气灰蓝、筑基青绿、金丹金黄、元婴紫、化神蓝、飞升橙粉
  • 博主用邮箱 MD5 直接指定专属称号(我的是「道主」,红紫渐变),不走次数阶梯

原理

Twikoo 的 GET_RECENT_COMMENTS 接口最多返回全站最近 100 条评论,每条带 nick(昵称)和 mailMd5(邮箱的 MD5)。前端拉一次这个接口,按 mailMd5 聚合计数,就得到每个人的评论次数,再落入阶梯:

1
炼气 1-2 | 筑基 3-5 | 金丹 6-9 | 元婴 10-19 | 化神 20-49 | 飞升 50+

页面上怎么知道某条评论是谁?不能靠头像 URL——评论者自定义头像时 URL 里没有 MD5。可靠的做法是建立「昵称 → MD5」映射(同一次接口返回里两个字段成对出现),再拿评论 DOM 里的昵称反查。

为什么按 MD5 而不是昵称计数:同一邮箱换昵称发评论很常见(我自己就用过 Amu / amu 两个昵称),按昵称计数会把一个人拆成两个「炼气」。按 MD5 聚合后,任何昵称发的评论都算同一个人。

实现

新建 source/js/comment-realm.js(Fluid 主题放 custom_js 里加载,其他主题同理在模板里引一次):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// ===== 评论区修仙境界徽章(按评论次数自动定级)=====
(function () {
// 阶梯:阈值 → 境界(从高到低匹配)
var LADDER = [
[50, '飞升'],
[20, '化神'],
[10, '元婴'],
[6, '金丹'],
[3, '筑基'],
[1, '炼气']
];

// 境界 → 徽章配色(渐变两端 + 光晕色)
var REALM_STYLE = {
'炼气': ['#8ea8c3', '#6a89a7', 'rgba(142,168,195,.45)'],
'筑基': ['#7ec8a9', '#4fa982', 'rgba(126,200,169,.45)'],
'金丹': ['#f0c27b', '#d9a441', 'rgba(240,194,123,.5)'],
'元婴': ['#c39bd3', '#9b6bb3', 'rgba(195,155,211,.5)'],
'化神': ['#7fb3e8', '#4a7fc9', 'rgba(127,179,232,.5)'],
'飞升': ['#ffb86b', '#ff6b9d', 'rgba(255,150,120,.55)'],
'道主': ['#ff4d6d', '#7b2ff7', 'rgba(255,77,109,.6)']
};

// 特殊称号:不走次数阶梯,按邮箱 MD5 直接指定(在阶梯之前命中)
// 算法:把评论者邮箱原文 md5 即得;Twikoo 后台「最近评论」接口返回的
// mailMd5 就是这个值,可直接复制
var TITLES_BY_MD5 = {
'你的邮箱md5放这里': '道主'
};

var CACHE_KEY = 'twikoo-realm-stats';
var CACHE_TTL = 60 * 60 * 1000; // 1 小时

function readCache() {
try {
var raw = localStorage.getItem(CACHE_KEY);
if (!raw) return null;
var data = JSON.parse(raw);
if (!data || !data.at || Date.now() - data.at > CACHE_TTL || !data.counts) return null;
return data;
} catch (e) { return null; }
}

function writeCache(stats) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({ at: Date.now(), counts: stats.counts, nickMap: stats.nickMap }));
} catch (e) {}
}

function realmOf(n) {
for (var i = 0; i < LADDER.length; i++) {
if (n >= LADDER[i][0]) return LADDER[i][1];
}
return null;
}

/** 拉最近评论 → { counts: {md5: 次数}, nickMap: {昵称(小写): md5} } */
function fetchStats(callback) {
var cached = readCache();
if (cached) { callback(cached, true); return; }
// 依次尝试:线上同源 /twikoo(主域反代场景);本地预览 404 时回落完整地址
var endpoints = ['/twikoo', 'https://你的后端地址/twikoo'];
function tryFetch(idx) {
if (idx >= endpoints.length) { callback({ counts: {}, nickMap: {} }, false); return; }
try {
var xhr = new XMLHttpRequest();
xhr.open('POST', endpoints[idx], true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
var stats = { counts: {}, nickMap: {} };
var ok = false;
try {
var list = JSON.parse(xhr.responseText).data || [];
for (var i = 0; i < list.length; i++) {
var c = list[i];
var md5 = c.mailMd5;
var nick = (c.nick || '').trim();
if (!md5 || !nick) continue;
ok = true;
stats.counts[md5] = (stats.counts[md5] || 0) + 1;
// 同一 md5 的多个昵称都指回去;同人改名后旧昵称评论也能命中
stats.nickMap[nick.toLowerCase()] = md5;
}
} catch (e) {}
if (ok) { writeCache(stats); callback(stats, false); }
else { tryFetch(idx + 1); }
};
xhr.onerror = function () { tryFetch(idx + 1); };
xhr.send(JSON.stringify({ event: 'GET_RECENT_COMMENTS', includeReply: true, pageSize: 100 }));
} catch (e) { tryFetch(idx + 1); }
}
tryFetch(0);
}

function makeBadge(realm) {
var c = REALM_STYLE[realm] || REALM_STYLE['炼气'];
var el = document.createElement('span');
el.className = 'tk-realm-badge';
el.setAttribute('data-realm', realm);
el.textContent = realm;
el.style.cssText =
'display:inline-flex;align-items:center;' +
'padding:.1em .55em;margin-left:.45em;' +
'font-size:.72em;font-weight:600;line-height:1.5;' +
'border-radius:999px;white-space:nowrap;vertical-align:1px;' +
'color:#fff;' +
'background:linear-gradient(120deg,' + c[0] + ',' + c[1] + ');' +
'box-shadow:0 0 6px ' + c[2] + ',0 0 14px ' + c[2] + ';' +
'text-shadow:0 0 4px rgba(255,255,255,.35);' +
'border:1px solid rgba(255,255,255,.25);';
return el;
}

function decorate(root, stats) {
var comments = (root || document).querySelectorAll('.twikoo .tk-comment:not([data-realm-done])');
for (var i = 0; i < comments.length; i++) {
var node = comments[i];
node.setAttribute('data-realm-done', '1'); // 防重复注入
var meta = node.querySelector(':scope > .tk-main > .tk-row > .tk-meta') || node.querySelector('.tk-meta');
var nickEl = meta && meta.querySelector('.tk-nick');
if (!meta || !nickEl) continue;
var nick = (nickEl.textContent || '').trim().toLowerCase();
var md5 = stats.nickMap[nick];
// 特殊称号优先,其次按次数阶梯
var realm = (md5 && TITLES_BY_MD5[md5]) || realmOf(md5 ? stats.counts[md5] : 0);
if (realm) nickEl.insertAdjacentElement('afterend', makeBadge(realm));
}
}

function start() {
var root = document.getElementById('twikoo');
if (!root) return false;
var started = false;
fetchStats(function (stats) {
if (started) { decorate(root, stats); return; }
started = true;
decorate(root, stats);
// Twikoo 是 Vue 动态渲染:翻页/回复/新评论都会重挂 DOM,常驻监听
var mo = new MutationObserver(function (muts) {
for (var i = 0; i < muts.length; i++) {
if (muts[i].addedNodes.length) { decorate(root, stats); break; }
}
});
mo.observe(root, { childList: true, subtree: true });
});
return true;
}

function boot() {
// twikoo 脚本 defer 加载,评论区可能还没挂出来,轮询等容器
var tries = 0;
var t = setInterval(function () {
if (start() || ++tries > 40) clearInterval(t);
}, 500);
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();

给 Twikoo 评论加修仙境界徽章
https://www.amubk.cn/2026/11/04/Twikoo 评论修仙境界徽章:纯前端按评论次数自动定级/
作者
阿木
发布于
2026年11月4日
许可协议