4.主题魔改:侧边栏添加“最新评论”模块
.webp)
AI-摘要
Tianli GPT
AI初始化中...
介绍自己 🙈
生成本文简介 👋
推荐相关文章 📖
前往主页 🏠
前往爱发电购买
4.主题魔改:侧边栏添加“最新评论”模块
Prorise4.主题魔改:侧边栏添加“最新评论”模块
重要前提:评论后端不匹配问题
提供的这份教程是为 Twikoo 评论系统设计的。
核心流程概览
- 创建功能核心JS文件:一份JS文件将包含所有的样式和逻辑。
- 在侧边栏添加小组件配置:告诉主题要在侧边栏的哪个位置显示“最新评论”这个卡片。
- 通过主题配置注入JS文件:让网站加载我们创建的功能脚本。
第一步:创建核心功能文件 (comments.js
)
创建JS文件
- 在您主题的
source/js/
目录下,新建一个文件,命名为comments.js
。 - 文件路径示例:
themes/anzhiyu/source/js/comments.js
- 在您主题的
粘贴JS代码
- 将下面的JavaScript代码完整复制到您刚创建的
comments.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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212(() => {
const injectCSS = () => {
const style = document.createElement('style');
style.textContent = `
#aside-content .aside-list > .aside-list-item .content{
width: 3.2em !important;
height: 3.2em !important;
display: flex;
flex-direction: column;
justify-content: space-around;
}
#aside-content .aside-list > .aside-list-item .thumbnail {
width: 3.2em!important;
height: 3.2em!important;
}
.card-latest-comments .item-headline i {
color: var(--anzhiyu-main);
}
.card-latest-comments .headline-right {
position: absolute;
right: 24px;
top: 16px;
transition: all 0.3s;
opacity: 0.6;
}
.card-latest-comments .headline-right:hover {
color: var(--anzhiyu-main);
opacity: 1;
transform: rotate(90deg);
}
.aside-list-author {
display: flex;
align-items: center;
font-weight: bold;
height: 22px;
gap: 5px;
}
.aside-list-date {
font-size: 0.7rem;
font-weight: normal;
margin-left: auto;
}
.aside-list-content {
font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-decoration: none;
line-height: 1.2;
}
.aside-list-item:last-child {
margin-bottom: 0!important;
}
[data-theme='dark'] .aside-list-item-right {
filter: brightness(0.95);
}
.aside-list-author-name {
display: flex;
align-items: center;
white-space: nowrap;
gap: 4px;
max-width: 65%;
}
.aside-list-author-name span {
overflow: hidden;
text-overflow: ellipsis;
}
.aside-list-author-name svg {
flex-shrink: 0;
}
`;
document.head.appendChild(style);
};
const LatestComments = {
API_URL: 'https://twikoo.ruom.top',
ADMIN_EMAIL_MD5: 'f2c9c64c90a00afeed5ba410e5447a0d01aa294874bd662032a27c5385bcde1c',
PAGE_SIZE: 5,
LOADING_GIF: 'https://lib.bsgun.cn/Hexo-static/img/loading.gif',
async fetchComments() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(this.API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'GET_RECENT_COMMENTS',
includeReply: true,
pageSize: this.PAGE_SIZE
}),
signal: controller.signal
});
const { data } = await response.json();
return data;
} catch (error) {
console.error('获取评论出错:', error);
return null;
} finally {
clearTimeout(timeoutId);
}
},
formatTimeAgo(timestamp) {
const diff = Math.floor((Date.now() - new Date(timestamp)) / 1000);
if (diff < 60) return '刚刚';
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
if (diff < 604800) return `${Math.floor(diff / 86400)}天前`;
return new Date(timestamp).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }) + '日';
},
formatContent(content) {
if (!content) return '';
return content
.replace(/<pre><code>[\s\S]*?<\/code><\/pre>/g, '[代码块]')
.replace(/<code>([^<]{4,})<\/code>/g, '[代码]')
.replace(/<code>([^<]{1,3})<\/code>/g, '$1')
.replace(/<img[^>]*>/g, '[图片]')
.replace(/<a[^>]*?>[\s\S]*?<\/a>/g, '[链接]')
.replace(/<[^>]+>/g, '')
.replace(/&(gt|lt|amp|quot|#39|nbsp);/g, m =>
({'>':'>', '<':'<', '&':'&', 'quot':'"', '#39':"'", 'nbsp':' '})[m.slice(1,-1)])
.replace(/\s+/g, ' ')
.trim();
},
generateCommentHTML(comment) {
const { created, comment: content, url, avatar, nick, mailMd5, id } = comment;
const timeAgo = this.formatTimeAgo(created);
const formattedContent = this.formatContent(content);
const adminBadge = mailMd5 === this.ADMIN_EMAIL_MD5 ? `
<svg t="1731283534336" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29337" width="22" height="22"><path d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m291.84 366.08c-46.08 0-79.36 23.04-92.16 66.56l-163.84 358.4h-66.56L312.32 435.2c-17.92-46.08-46.08-71.68-89.6-71.68v-35.84H512v35.84h-40.96c-25.6 2.56-30.72 23.04-12.8 61.44l102.4 225.28 89.6-199.68c25.6-56.32 2.56-84.48-71.68-89.6v-35.84h225.28v40.96z" fill="#06c013" p-id="29338" data-spm-anchor-id="a313x.search_index.0.i73.2b2d3a81BgxnVW" class=""></path></svg>` : '';
return `
<div class="aside-list-item" title="${formattedContent}" onclick="pjax.loadUrl('${url}#${id}')">
<div class="thumbnail">
<img class="aside-list-avatar" src="${avatar}" alt="avatar">
</div>
<div class="content">
<div class="aside-list-author">
<div class="aside-list-author-name">
<span>${nick}</span>${adminBadge}
</div>
<span class="aside-list-date">${timeAgo}</span>
</div>
<div class="aside-list-content">${formattedContent}</div>
</div>
</div>
`;
},
getErrorTemplate(icon, message) {
return `
<div style="min-height: 346px;display: flex;padding: 20px;text-align: center;justify-content: center;align-items: center;flex-direction: column;">
<i class="fas fa-${icon}" style="font-size: 2rem; color: ${icon === 'exclamation-circle' ? '#ff6b6b' : '#999'}; margin-bottom: 10px;"></i>
<p style="color: #666;margin: 0;">${message}</p>
</div>
`;
},
async insertComponent() {
const container = document.getElementById("latest-comments");
if (!container) return;
container.innerHTML = `<img src="${this.LOADING_GIF}" style="display: flex;min-height: 346px;object-fit: cover;">`;
const comments = await this.fetchComments();
let content;
if (comments === null) {
content = this.getErrorTemplate('exclamation-circle', '评论加载失败,请稍后再试');
} else if (comments.length === 0) {
content = this.getErrorTemplate('comment-slash', '还没有评论呢~ 快来抢沙发吧!');
} else {
content = comments.map(this.generateCommentHTML.bind(this)).join('');
}
container.style.opacity = '0';
container.innerHTML = content;
requestAnimationFrame(() => {
container.style.transition = 'opacity 0.3s ease-in';
container.style.opacity = '1';
});
}
};
// 初始化时注入CSS并启动组件
['DOMContentLoaded', 'pjax:success'].forEach(event =>
document.addEventListener(event, () => {
injectCSS();
LatestComments.insertComponent();
})
);
})();- 将下面的JavaScript代码完整复制到您刚创建的
第二步:在侧边栏中添加小组件
找到或创建
widget.yml
- 这个文件控制了侧边栏显示哪些小组件以及它们的顺序。
- 文件路径:
source/_data/widget.yml
。如果_data
或widget.yml
不存在,请手动创建。
添加“最新评论”卡片配置
- 在
widget.yml
文件中添加以下内容。您可以把它放在top:
列表的任意位置,来决定它在侧边栏顶部区域的显示顺序。
1
2
3
4
5
6
7
8
9top:
- class_name: card-latest-comments
name: 最新评论
icon: fas fa-comments
html: |
<a href="/messages/" class="headline-right" title="查看更多">
<i class="fas fa-angle-right"></i>
</a>
<div class="aside-list" id="latest-comments"></div>- 在
第三步:在主题配置中注入JS文件
- 打开主题配置文件 (
themes/anzhiyu/_config.yml
)。 - 找到
inject:
配置项,在bottom:
列表中添加我们新建的JS文件。1
2
3
4inject:
bottom:
# - 其他 bottom 内容
- '<script src="/js/comments.js"></script>'
第四步:个性化配置说明
您需要修改 comments.js
文件顶部的几个关键参数,使其与您自己的Twikoo服务匹配。
API_URL
: 【必填】 将'https://twikoo.ruom.top'
替换为您自己部署的Twikoo后端服务地址。ADMIN_EMAIL_MD5
: 【必填】 替换为您自己作为博主的邮箱的MD5值。这用于在评论列表中为您显示“博主”标识。您可以右键点击评论的头像选择在新窗口打开截取一下的字段1
2
3https://weavatar.com/avatar/ec3291d59a8d7d3675df8a0537fcbc979f0fa611c8df55841fb7f4fd162bd07a?d=initials&name=Prorise
ec3291d59a8d7d3675df8a0537fcbc979f0fa611c8df55841fb7f4fd162bd07a <- 即为MD5值PAGE_SIZE
: 显示的最新评论数量,您可以按需修改。LOADING_GIF
: 加载时显示的动画图片,您可以替换为您喜欢的图片链接。
评论
隐私政策
TwikooWaline
✅ 你无需删除空行,直接评论以获取最佳展示效果