抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >
L
O
A
D
I
N
G

本文记录为博客添加类似QQ空间或朋友圈的功能,主要用于博主本人发点日常,记录美好生活。因为本站的框架是采用 Heox 框架 + Volantis 主题,评论系统使用的是 Waline ,所有此次教程也是基于此搭建的博客动态的。

一、前言

其实很早之前我就想弄一个这样的功能了,但苦于没有方向,又不想自己从头开始搭建服务(主要是博主赖…嘻嘻),然后呢博主就一直兜兜转转的寻找合适的方法或者插件,试过这样的 or 那样的,比如直接用文章来表达的,还有用插件的等等

但总体来说还是不太满意的,然后就继续找呀。终于,功夫不负有心人,2023年底我在B站上闲逛的时候发现了 Memos

Memos 是一个开源的、轻量级的笔记服务项目,可以用来记录、整理、分享你的知识和思考。

然后我就上网搜一下 Memos 的相关知识,卧槽塔猴,发现可以参考的相关资料还挺多,比如木木大佬的『哔哔点啥 2.0 By Memos』和 Leonus 大佬的『基于Memos实现说说和清单功能』 都是能借鉴参考的,也不少博主基于此搭建 说说、随笔、动态 页面。当然了,我也是其中的一员,基于 Memos 给博客重新整改许久 动态 功能,经过这段时间的魔改,目前我对这个功能还算是比较满意的~嘻嘻
下面给同学们展示成果:
Memos 地址:https://memos.yywen.top
博客内嵌地址:https://yywen.top/dailylife

上面几张图基本上涵盖了博主发布动态到展示的全过程,在外面的时候一般都是使用手机来发布动态,使用的是 MoeMemos 应用,它可以直接选择手机相册的图片,随时随地都可以发布动态,非常丝滑,在家里就使用网页端来发布,毕竟手机相册的图片太大了,不经过处理的话图片加载是真的慢。
OK ,废话不多说,说再多也不如自己上手体验一把,直接上教程。

二、安装相关服务

1. 服务器——Memos

自动安装
懒人必备,直接在 1Panel 上的应用商店搜索 Memos 直接安装即可

不了解 1Panel 的同学可以参考博主之前的这篇文章:『个人网站之部署篇(五)』

手动安装
部署的话官方推荐使用 Docker 的方式安装,而博主本人也是采用这种方式安装的,使用 Docker-Compose 安装方式如下:
新建一个文件夹:

1
2
work=~/docker/memos
mkdir -p $work; cd $work

创建一个 docker-compose.yml 文件:

1
vim $work/docker-compose.yml

填入以下内容:

1
2
3
4
5
6
7
8
9
version: '3'
services:
memos:
image: neosmemo/memos:stable
container_name: memos
volumes:
- ~/.memos/:/var/opt/memos
ports:
- 5230:5230

使用以下命令直接启动即可:

1
docker-compose up -d

如果想要自动升级的同学可以配上 Watchtower 来实现,只需要将docker-compose.yml内容修改如下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: '3'
services:
memos:
image: neosmemo/memos:0.22
labels: { com.centurylinklabs.watchtower.enable: true }
container_name: memos
hostname: memos
restart: unless-stopped
ports: ["5230:5230"]
volumes:
- ~/.memos:/var/opt/memos

watchtower:
image: containrrr/watchtower:1.7.1
container_name: watchtower
restart: unless-stopped
command: --stop-timeout 60s --cleanup --schedule "0 0 4 * * *" --label-enable
volumes: ["/var/run/docker.sock:/var/run/docker.sock"]

最后再运行启动命令 docker-compose up -d 即可在浏览器中输入 http://IP:5230 ,就可以看到主界面了

在云服务器上部署注意在防火墙上放行对应的端口5230

最后添加一下反向代理即可,不清楚的可以参考博主之前的这篇文章:『个人网站之代理篇(五)』

2. 移动端APP——MoeMemos

MoeMemosAndroid 是一个开源项目,使用起来非常便捷,下面介绍两种下载安装方式:

1. 使用 Google Play 安装
这个没什么好说的,直接打开 Google Play 应用搜索 MoeMemos 下载安装即可

2. 通过 APK 安装包进行安装
这个需要自己下载安装包到手机中,再使用安装包进行安装,直接上 Github 仓库中下载即可,如果有同学打不开 Github的可以使用我下载好的 安装包

安装完成之后再登录就可以正常使用了,基本界面如下:

3. 评论系统(选用)

博主这里使用的是 Waline 评论系统,个人感觉这个评论系统还挺不错,简洁美观,功能齐全,官方文档写得也很好,深得我心,所以我就选用了它作为评论系统,有小伙伴需要安装部署的话直接看官方文档即可。

当然了,其他的评论系统也不错,比如 Twikoo ,我看很多大佬的评论系统都是使用这个。

三、Memos 食用指南

1. Memos 使用说明

Memos 的使用其实很简单,基本使用如下所示:

除了 Markdown 语法,基本上只要自己上手体验一下就会个七七八八了,更多的自己研究一下就懂了

Memos 支持 Markdown 的纯文本体验,告别复杂的富文本格式,迎接更为简洁的书写方式

不了解 Markdown 的可以参考博主之前的这篇文章:『Markdown 语法大全』

2. Memos API 说明

后面在博客适配的时候需要用到 Memos API 来获取数据,所有需要对此有所了解,起码知道自己可以DIY什么功能。对于 Memos API ,官方文档好像并没有给出了,不过木木大佬的这篇文章『Memos API 非官方不完全说明』已经给我们列好了,就不需要再去看源码了~

嘻嘻~,真好,果然是大树底下好乘凉呀

四、博客引入 Memos

此过程是将在 Memos 发布的说说在博客上展示出来,用于丰富博客的内容。我一时发觉博客就好似花园菜地,博主在里面辛勤劳作种上自己喜欢的花花草草,为防止花草枯萎还需时不时要浇水除草(运维),只为多年后的菜园花草盛开,芳香四溢,引得远处飞来的蜜蜂、蝴蝶翩翩起舞。

哈哈,一时意起胡乱说了一堆,让我们继续回来种菜,先创建一个页面用于展示动态说说,步骤过程如下:

1. 创建页面

首先通过 hexo n page xxx 创建页面,相信大伙都很熟悉了。

1
hexo new page talkbb

2. 添加脚本和样式

脚本是拿木木大佬的来修改的,样式是就自己大概写了一下,大伙需要可以随意修改代码和样式来实现你想实现的效果。

Javascript
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
let bbMemos = {
memos: 'https://memos.yywen.top/',
limit: '5',
creatorId: '1',
domId: '#bber',
wlEnv: '评论区地址',
name: '黑兔小九',
avatar: 'https://dogecloud.res.yywen.top/img/friends/avatar/avatar-sirius.avif',
}
let limit = bbMemos.limit
let memos = bbMemos.memos
let memosOpenId
let mePage = 1,
offset = 0,
nextLength = 0,
nextDom = '',
apiV1 = '';
let bbDom = document.querySelector(bbMemos.domId);
let load = '<div class="bb-load"><button class="load-btn button-load">加载中……</button></div>'
if (bbDom) {
fetchStatus()
}
async function fetchStatus() {
let statusUrl = memos + "api/v1/ping";
let response = await fetch(statusUrl);
if (response.ok) {
apiV1 = 'v1/'
}
initMemo(apiV1);
}
function initMemo(apiV1) {
getFirstList(apiV1) //首次加载数据
meNums(apiV1) //加载总数
let btn = document.querySelector("button.button-load");
btn.addEventListener("click", function () {
btn.textContent = '加载中……';
if (bbMemos.wlEnv) {
updateWaline(nextDom)
} else {
updateHTMl(nextDom)
}
if (nextLength < limit) { //返回数据条数小于限制条数,隐藏
document.querySelector("button.button-load").remove()
return
}
getNextList(apiV1)
});
}
function getFirstList(apiV1) {
bbDom.insertAdjacentHTML('afterend', load);
let bbUrl = memos + "api/" + apiV1 + "memo?creatorId=" + bbMemos.creatorId + "&rowStatus=NORMAL&limit=" + limit;
fetch(bbUrl).then(res => res.json()).then(resdata => {
if (bbMemos.wlEnv) {
updateWaline(resdata)
} else {
updateHTMl(resdata)
}
let nowLength = resdata.length
if (nowLength < limit) { //返回数据条数小于 limit 则直接移除“加载更多”按钮,中断预加载
document.querySelector("button.button-load").remove()
return
}
mePage++
offset = limit * (mePage - 1)
getNextList(apiV1)
});
}
function getNextList(apiV1) { //预加载下一页数据
let bbUrl = memos + "api/" + apiV1 + "memo?creatorId=" + bbMemos.creatorId + "&rowStatus=NORMAL&limit=" + limit + "&offset=" + offset;
fetch(bbUrl).then(res => res.json()).then(resdata => {
nextDom = resdata
nextLength = resdata.length
mePage++
offset = limit * (mePage - 1)
if (nextLength < 1) { //返回数据条数为 0 ,隐藏
document.querySelector("button.button-load").remove()
return
}
})
}
function meNums(apiV1) { //加载总 Memos 数
let bbLoad = document.querySelector('.bb-load')
let bbUrl = memos + "api/" + apiV1 + "memo/stats?creatorId=" + bbMemos.creatorId
fetch(bbUrl).then(res => res.json()).then(resdata => {
if (resdata) {
let allnums = `<div id="bb-footer"><a href="https://memos.yywen.top/" target="_blank"><p class="bb-allnums">共 ${resdata.length} 条 </p><p class="bb-allpub"></a></p></div>`
bbLoad.insertAdjacentHTML('afterend', allnums);
}
})
}
function getTime(timestamp) {
let d = new Date(timestamp * 1000),
ls = [d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()];
for (let i = 0; i < ls.length; i++) {
ls[i] = ls[i] <= 9 ? '0' + ls[i] : ls[i] + ''
}
if (new Date().getFullYear() == ls[0]) return ls[1] + '月' + ls[2] + '日 ' + ls[3] +':'+ ls[4]
else return ls[0] + '年' + ls[1] + '月' + ls[2] + '日 ' + ls[3] +':'+ ls[4]
}
function getTimeAgo(timestamp) {
const days = Math.floor((new Date().getTime() - timestamp * 1000) / (24 * 60 * 60 * 1000));
switch (true) {
case (days == 0):
return ' 今天';
case (days == 1):
return ' 昨天';
case (days == 2):
return ' 前天';
case (days - 14 <= 0):
return ' 一周前';
default:
return ' '+ days +'天前';
}
}
async function updateWaline(data) {
await updateHTMl(data);
for (let i = 0; i < data.length; i++) {
let bbID = data[i].id;
window.getCommentCount({
serverURL: bbMemos.wlEnv,
selector: '.waline-memos-' + bbID,
path: bbMemos.memos + 'm/' + bbID,
});
}
}
// 插入 html
async function updateHTMl(data) {
let result = "",
resultAll = "";
const TAG_REG = /#([^#\s!.,;:?"'()]+)(?= )/g ///#([^/\s#]+?) /g
,
IMG_REG = /\!\[(.*?)\]\((.*?)\)/g,
LINK_REG = /\[(.*?)\]\((.*?)\)/g,
DEODB_LINK_REG = /(https:\/\/(www|movie|book)\.douban\.com\/(game|subject)\/[0-9]+\/).*?/g,
BILIBILI_REG = /<a.*?href="https:\/\/www\.bilibili\.com\/video\/((av[\d]{1,10})|(BV([\w]{10})))\/?".*?>.*<\/a>/g,
NETEASE_MUSIC_REG = /<a.*?href="https:\/\/music\.163\.com\/.*id=([0-9]+)".*?>.*<\/a>/g,
QQMUSIC_REG = /<a.*?href="https\:\/\/y\.qq\.com\/.*(\/[0-9a-zA-Z]+)(\.html)?".*?>.*?<\/a>/g,
QQVIDEO_REG = /<a.*?href="https:\/\/v\.qq\.com\/.*\/([a-z|A-Z|0-9]+)\.html".*?>.*<\/a>/g,
YOUKU_REG = /<a.*?href="https:\/\/v\.youku\.com\/.*\/id_([a-z|A-Z|0-9|==]+)\.html".*?>.*<\/a>/g,
YOUTUBE_REG = /<a.*?href="https:\/\/www\.youtube\.com\/watch\?v\=([a-z|A-Z|0-9]{11})\".*?>.*<\/a>/g;
marked.setOptions({
breaks: false,
smartypants: false,
langPrefix: 'language-',
headerIds: false,
mangle: false
});
for (let i = 0; i < data.length; i++) {
let bbID = data[i].id
let memoUrl = memos + "m/" + bbID
let bbCont = data[i].content + ' '
let bbContREG = ''
let bbPos = ''
bbContREG += bbCont.replace(TAG_REG, "")
.replace(IMG_REG, "")
.replace(DEODB_LINK_REG, '')
.replace(LINK_REG, '<a class="primary" href="$2" target="_blank">$1</a>')
//标签
bbContREG = bbContREG.replace(/#(.*?)\s/g, '').replace(/\!?\[(.*?)\]\((.*?)\)/g, '').replace(/\{(.*?)\}/g, '')
bbPos = bbCont.match(/\{(.*?)\}/g);
bbPos = bbPos ? bbPos[0].replace(/\{(.*?)\}/,'$1') : "***";
let tagArr = bbCont.match(TAG_REG);
bbContREG = marked.parse(bbContREG)
.replace(BILIBILI_REG, "<div class='video-wrapper'><iframe src='//www.bilibili.com/blackboard/html5mobileplayer.html?bvid=$1&as_wide=1&high_quality=1&danmaku=0' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen='true'></iframe></div>")
.replace(NETEASE_MUSIC_REG, "<meting-js auto='https://music.163.com/#/song?id=$1'></meting-js>")
.replace(QQMUSIC_REG, "<meting-js auto='https://y.qq.com/n/yqq/song$1.html'></meting-js>")
.replace(QQVIDEO_REG, "<div class='video-wrapper'><iframe src='//v.qq.com/iframe/player.html?vid=$1' allowFullScreen='true' frameborder='no'></iframe></div>")
.replace(YOUKU_REG, "<div class='video-wrapper'><iframe src='https://player.youku.com/embed/$1' frameborder=0 'allowfullscreen'></iframe></div>")
.replace(YOUTUBE_REG, "<div class='video-wrapper'><iframe src='https://www.youtube.com/embed/$1' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen title='YouTube Video'></iframe></div>")
//解析 content 内 md 格式图片
let IMG_ARR = data[i].content.match(IMG_REG) || '',
IMG_ARR_Grid = '';
if (IMG_ARR) {
let IMG_ARR_Length = IMG_ARR.length,
IMG_ARR_Url = '';
if (IMG_ARR_Length !== 1) {
let IMG_ARR_Grid = " grid grid-" + IMG_ARR_Length
}
IMG_ARR.forEach(item => {
let imgSrc = item.replace(/!\[.*?\]\((.*?)\)/g, '$1')
IMG_ARR_Url += `<a href="${imgSrc}" data-fancybox="gallery${bbID}" class="fancybox" data-thumb="${imgSrc}"><img class="no-lazyload" src="${imgSrc}"></a>`
});
bbContREG += `<div class="resimg${IMG_ARR_Grid}">${IMG_ARR_Url}</div>`
}
//解析内置资源文件
if (data[i].resourceList && data[i].resourceList.length > 0) {
let resourceList = data[i].resourceList;
let imgUrl = '',
resUrl = '',
resImgLength = 0;
for (let j = 0; j < resourceList.length; j++) {
let restype = resourceList[j].type.slice(0, 5)
let resexlink = resourceList[j].externalLink
let resLink = resexlink ? resexlink :
memos + 'o/r/' + resourceList[j].id + '/' + (resourceList[j].publicId || resourceList[j].filename)
if (restype == 'image') {
imgUrl += `<a href="${resLink}" data-fancybox="gallery${bbID}" class="fancybox" data-thumb="${resLink}"><img class="no-lazyload" src="${resLink}"></a>`
resImgLength = resImgLength + 1
} else if (restype == 'video') {
imgUrl += `<div class="video-wrapper"><video controls><source src="${resLink}" type="video/mp4"></video></div>`
} else {
resUrl += `<a target="_blank" rel="noreferrer" href="${resLink}">${resourceList[j].filename}</a>`
}
}
if (imgUrl) {
let resImgGrid = ""
if (resImgLength !== 1) {
resImgGrid = "grid grid-" + resImgLength
}
bbContREG += `<div class="resimg ${resImgGrid}">${imgUrl}</div>`
}
if (resUrl) {
bbContREG += `<p class="bb-source">${resUrl}</p>`
}
}
let bbTime1 = getTime(data[i].createdTs);
let bbTime2 = getTimeAgo(data[i].createdTs);
let memosIdNow = memos.replace(/https\:\/\/(.*\.)?(.*)\..*/, 'id-$2-')
let emojiReaction = `<emoji-reaction theme="system" class="reaction" endpoint="https://api-emaction.immmmm.com" reacttargetid="${memosIdNow + 'memo-' + bbID}" style="line-height:normal;display:inline-flex;"></emoji-reaction>`
let commentBtn = ``
if (bbMemos.wlEnv) {
commentBtn = `<div class="comment-btn"><a class="msg-btn" data-id="${bbID}" onclick="loadWaline(this)"><span class="icon"><i class="fa-solid fa-message fa-fw"></i></span><span class="waline-memos-${bbID}"></span></a></div>`
}
result += `<li class="memo-${bbID}">
<div class="bb-item">
<div class="bb-head">
<img class="no-lightbox no-lazyload user-avatar" style="border-radius: 50%;" src="${bbMemos.avatar}">
<div class="info">
<span class="name">${bbMemos.name} ${icon}</span>
<span class="datatime"><i class="fa-solid fa-calendar-days WISTERIA fa-fw"></i> ${bbTime1}</span>
</div>
<a href="${memoUrl}" target="_blank"><i class="fa-solid fa-share-from-square"></i></a>
</div>
<div class="bb-content">
${bbContREG}
</div>
<div class="bb-bottom">
<div>
<span><i class="fa-solid fa-clock WISTERIA fa-fw"></i> ${bbTime2}</span>
<span><i class="fa-solid fa-location-dot WISTERIA fa-fw"></i> ${bbPos}</span>
</div>
<div class="emoji">${emojiReaction}</div>
${commentBtn}
</div>
<div id="container-${bbID}" class="item-waline d-none">
<div style="font-size:20px;padding:5px 10px;font-weight:600;"><i class="fa-solid fa-comments"></i> 评论</div>
<div id="waline-${bbID}"></div>
</div>
</div>
</li>`
} // end for
let bbBefore = "<section class='bb-timeline'><ul class='bb-list-ul'>"
let bbAfter = "</ul></section>"
resultAll = bbBefore + result + bbAfter
let loaderDom = document.querySelector('.loader') || ""
if (loaderDom) loaderDom.remove()
bbDom.insertAdjacentHTML('beforeend', resultAll);
if (document.querySelector('button.button-load')) document.querySelector('button.button-load').textContent = '加载更多';
}
function loadWaline(e) {
let memosId = e.getAttribute("data-id");
let walineDom = document.getElementById("container-" + memosId)
if (walineDom.classList.contains('d-none')) {
document.querySelectorAll('.item-waline').forEach((item) => {item.classList.add('d-none');})
if(document.getElementById("waline-" + memosId)){
walineDom.classList.remove('d-none');
window.scrollTo({
top: walineDom.offsetTop - 30,
behavior: "smooth"
});
window.initComment({
el: '#waline-' + memosId ,
serverURL: bbMemos.wlEnv ,
emoji: [
'https://dogecloud.res.yywen.top/emoji/sticker/',
],
path: bbMemos.memos + 'm/' + memosId,
});
}
}else{
walineDom.classList.add('d-none');
}
}
CSS
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
#bber {
margin-top: 1rem;
width: auto !important;
margin: auto !important;
min-height: 100vh;
}
.bb-timeline ul {
margin: 0;
padding: 0;
li {
margin-bottom: 1.5rem;
list-style-type: none;
.bb-cont ul li {
margin-bottom: 0;
}
}
}
.bb-timeline .bb-item {
padding: 20px;
font-size: 16px;
margin-bottom: 15px;
background: var(--card-bg);
border-bottom: 1px solid #e0e3ed;
box-shadow: 3px 3px 5px rgba(0, 0, 0, .1);
transition: all .3s ease-in-out;
border-radius: 12px;
&:hover {
border:1px solid #49b1f5;
}
}
.bb-timeline .bb-head{
display: flex;
align-items: center;
margin-bottom: 10px;
.user-avatar {
margin: 0 !important;
width: 50px;
height: 50px;
}
.info {
display: flex;
flex-direction: column;
margin-left: 10px;
margin-right: auto;
.name {
color: #30a5a7;
font-size: 1.2rem;
}
.datatime {
opacity: .6;
}
}
}
.bb-timeline .bb-content{
margin: 5px 0 5px 5px;
}
.bb-timeline .bb-bottom{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: flex-start;
margin-top: 10px;
.emoji {
margin-left: 15px;
}
.comment-btn {
margin-left: auto !important;
}
}
.bb-load button {
border: 1px solid #dcdcdc;
border-radius: 8px;
box-shadow: 3px 3px 5px rgba(0, 0, 0, .1);
padding: 10px 30px;
width: 100%;
background: 0 0;
letter-spacing: .8rem;
font-style: italic;
font-size: .8rem;
&:hover {
color:#FFFFFF;
background:#4C4C4C;
}
}
#bb-footer {
margin: 1rem 1rem auto;
display: flex;
flex-direction: column;
align-items: center;
p {
margin: 0 0 .6rem;
}
}
.resimg.grid {
display: grid;
box-sizing: border-box;
margin: 4px 0 0;
width: auto;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
gap: 4px;
}
.resimg.grid-2 {
width: 80%;
grid-template-columns: repeat(2, 1fr);
}
.resimg.grid-4 {
width: calc(80% * 2 / 3);
grid-template-columns: repeat(2, 1fr);
}
.resimg a {
display: block;
border-radius: 9px;
width: 98%;
max-height: 60vh;
aspect-ratio: 1/1;
position: relative;
}
.resimg img {
width: 100%;
height: 100%;
border-radius: 9px;
margin: 0 !important;
object-fit: cover;
}
.d-none{display:none!important;}
.item-waline {
min-height: 100px;
padding: 10px;
margin-top: 15px;
border: 1px solid #e0e3ed;
border-radius: 12px;
box-shadow: 0px 3px 5px rgba(0, 0, 0, .1);
}
@media screen and (max-width: 625px) {
.emoji{
display:none!important;
}
}

3. 添加评论系统

Waline 评论系统
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
<script type="module">
import { init, commentCount } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';
window.initComment = function(params) {
try {
init({
el: params.el,
serverURL: params.serverURL,
emoji: params.emoji,
path: params.path,
comment: params.comment,
});
} catch (error) {
console.log(`Waline ${error}`);
}
}
window.getCommentCount = function(params) {
try {
commentCount({
serverURL: params.serverURL,
selector: params.selector,
path: params.path,
})
} catch (error) {
console.log(`Waline ${error}`);
}
}
</script>

4. 小结

基本上就这样,把全部组合起来就OK了,完整的代码如下:

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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
<style>
.page-top-card-box{
height: 250px;
}
.page-top-card{
background-image: url(https://img.yywen.top/hy-blog/img/wallpaper/005.webp);
}
.text-english{
font-family: 'Roboto';
}
.clock{
position:absolute;
right: 2.7rem;
}
.clock-canvas {
width:150px;
margin-top:5px;
}
svg.is-badge.icon {
width: 15px;
margin-left: 5px;
padding-top: 3px;
}
#bber {
margin-top: 1rem;
width: auto !important;
margin: auto !important;
min-height: 100vh;
}
.bb-timeline ul {
margin: 0;
padding: 0;
li {
margin-bottom: 1.5rem;
list-style-type: none;
.bb-cont ul li {
margin-bottom: 0;
}
}
}
.bb-timeline .bb-item {
padding: 20px;
font-size: 16px;
margin-bottom: 15px;
background: var(--card-bg);
border-bottom: 1px solid #e0e3ed;
box-shadow: 3px 3px 5px rgba(0, 0, 0, .1);
transition: all .3s ease-in-out;
border-radius: 12px;
&:hover {
border:1px solid #49b1f5;
}
}
.bb-timeline .bb-head{
display: flex;
align-items: center;
margin-bottom: 10px;
.user-avatar {
margin: 0 !important;
width: 50px;
height: 50px;
}
.info {
display: flex;
flex-direction: column;
margin-left: 10px;
margin-right: auto;
.name {
color: #30a5a7;
font-size: 1.2rem;
}
.datatime {
opacity: .6;
}
}
}
.bb-timeline .bb-content{
margin: 5px 0 5px 5px;
}
.bb-timeline .bb-bottom{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: flex-start;
margin-top: 10px;
.emoji {
margin-left: 15px;
}
.comment-btn {
margin-left: auto !important;
}
}
.bb-load button {
border: 1px solid #dcdcdc;
border-radius: 8px;
box-shadow: 3px 3px 5px rgba(0, 0, 0, .1);
padding: 10px 30px;
width: 100%;
background: 0 0;
letter-spacing: .8rem;
font-style: italic;
font-size: .8rem;
&:hover {
color:#FFFFFF;
background:#4C4C4C;
}
}
#bb-footer {
margin: 1rem 1rem auto;
display: flex;
flex-direction: column;
align-items: center;
p {
margin: 0 0 .6rem;
}
}
.resimg.grid {
display: grid;
box-sizing: border-box;
margin: 4px 0 0;
width: auto;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
gap: 4px;
}
.resimg.grid-2 {
width: 80%;
grid-template-columns: repeat(2, 1fr);
}
.resimg.grid-4 {
width: calc(80% * 2 / 3);
grid-template-columns: repeat(2, 1fr);
}
.resimg a {
display: block;
border-radius: 9px;
width: 98%;
max-height: 60vh;
aspect-ratio: 1/1;
position: relative;
}
.resimg img {
width: 100%;
height: 100%;
border-radius: 9px;
margin: 0 !important;
object-fit: cover;
}
.d-none{display:none!important;}
.item-waline {
min-height: 100px;
padding: 10px;
margin-top: 15px;
border: 1px solid #e0e3ed;
border-radius: 12px;
box-shadow: 0px 3px 5px rgba(0, 0, 0, .1);
}
@media screen and (max-width: 625px) {
.emoji{
display:none!important;
}
}
</style>
<script type="module" src="https://immmmm.com/emaction.js?v=230811"></script>
<script src="https://fastly.jsdelivr.net/npm/marked/marked.min.js"></script>
<div id="bber"></div>
<script data-pjax>
let icon = '<svg viewBox="0 0 512 512"xmlns="http://www.w3.org/2000/svg"class="is-badge icon"><path d="m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z"fill="#1da1f2"></path></svg>';
let bbMemos = {
memos: 'https://memos.yywen.top/',
limit: '5',
creatorId: '1',
domId: '#bber',
wlEnv: '评论区地址',
name: '黑兔小九',
avatar: 'https://dogecloud.res.yywen.top/img/friends/avatar/avatar-sirius.avif',
}
let limit = bbMemos.limit
let memos = bbMemos.memos
let memosOpenId
let mePage = 1, offset = 0, nextLength = 0, nextDom = '', apiV1 = '';
let bbDom = document.querySelector(bbMemos.domId);
let load = '<div class="bb-load"><button class="load-btn button-load">加载中……</button></div>'
if (bbDom) {
fetchStatus()
}
async function fetchStatus() {
let statusUrl = memos + "api/v1/ping";
let response = await fetch(statusUrl);
if (response.ok) {
apiV1 = 'v1/'
}
initMemo(apiV1);
}
function initMemo(apiV1) {
getFirstList(apiV1) //首次加载数据
meNums(apiV1) //加载总数
let btn = document.querySelector("button.button-load");
btn.addEventListener("click", function () {
btn.textContent = '加载中……';
if (bbMemos.wlEnv) {
updateWaline(nextDom)
} else {
updateHTMl(nextDom)
}
if (nextLength < limit) { //返回数据条数小于限制条数,隐藏
document.querySelector("button.button-load").remove()
return
}
getNextList(apiV1)
});
}
function getFirstList(apiV1) {
bbDom.insertAdjacentHTML('afterend', load);
let bbUrl = memos + "api/" + apiV1 + "memo?creatorId=" + bbMemos.creatorId + "&rowStatus=NORMAL&limit=" + limit;
fetch(bbUrl).then(res => res.json()).then(resdata => {
if (bbMemos.wlEnv) {
updateWaline(resdata)
} else {
updateHTMl(resdata)
}
let nowLength = resdata.length
if (nowLength < limit) { //返回数据条数小于 limit 则直接移除“加载更多”按钮,中断预加载
document.querySelector("button.button-load").remove()
return
}
mePage++
offset = limit * (mePage - 1)
getNextList(apiV1)
});
}
function getNextList(apiV1) { //预加载下一页数据
let bbUrl = memos + "api/" + apiV1 + "memo?creatorId=" + bbMemos.creatorId + "&rowStatus=NORMAL&limit=" + limit + "&offset=" + offset;
fetch(bbUrl).then(res => res.json()).then(resdata => {
nextDom = resdata
nextLength = resdata.length
mePage++
offset = limit * (mePage - 1)
if (nextLength < 1) { //返回数据条数为 0 ,隐藏
document.querySelector("button.button-load").remove()
return
}
})
}
function meNums(apiV1) { //加载总 Memos 数
let bbLoad = document.querySelector('.bb-load')
let bbUrl = memos + "api/" + apiV1 + "memo/stats?creatorId=" + bbMemos.creatorId
fetch(bbUrl).then(res => res.json()).then(resdata => {
if (resdata) {
let allnums = `<div id="bb-footer"><a href="${bbMemos.memos}" target="_blank"><p class="bb-allnums">共 ${resdata.length} 条 </p><p class="bb-allpub"></a></p></div>`
bbLoad.insertAdjacentHTML('afterend', allnums);
}
})
}
function getTime(timestamp) {
let d = new Date(timestamp * 1000),
ls = [d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()];
for (let i = 0; i < ls.length; i++) {
ls[i] = ls[i] <= 9 ? '0' + ls[i] : ls[i] + ''
}
if (new Date().getFullYear() == ls[0]) return ls[1] + '月' + ls[2] + '日 ' + ls[3] +':'+ ls[4]
else return ls[0] + '年' + ls[1] + '月' + ls[2] + '日 ' + ls[3] +':'+ ls[4]
}
function getTimeAgo(timestamp) {
const days = Math.floor((new Date().getTime() - timestamp * 1000) / (24 * 60 * 60 * 1000));
switch (true) {
case (days == 0):
return ' 今天';
case (days == 1):
return ' 昨天';
case (days == 2):
return ' 前天';
case (days - 14 <= 0):
return ' 一周前';
default:
return ' '+ days +'天前';
}
}
async function updateWaline(data) {
await updateHTMl(data);
for (let i = 0; i < data.length; i++) {
let bbID = data[i].id;
window.getCommentCount({
serverURL: bbMemos.wlEnv,
selector: '.waline-memos-' + bbID,
path: bbMemos.memos + 'm/' + bbID,
});
}
}
// 插入 html
async function updateHTMl(data) {
let result = "",
resultAll = "";
const TAG_REG = /#([^#\s!.,;:?"'()]+)(?= )/g ///#([^/\s#]+?) /g
,
IMG_REG = /\!\[(.*?)\]\((.*?)\)/g,
LINK_REG = /\[(.*?)\]\((.*?)\)/g,
DEODB_LINK_REG = /(https:\/\/(www|movie|book)\.douban\.com\/(game|subject)\/[0-9]+\/).*?/g,
BILIBILI_REG = /<a.*?href="https:\/\/www\.bilibili\.com\/video\/((av[\d]{1,10})|(BV([\w]{10})))\/?".*?>.*<\/a>/g,
NETEASE_MUSIC_REG = /<a.*?href="https:\/\/music\.163\.com\/.*id=([0-9]+)".*?>.*<\/a>/g,
QQMUSIC_REG = /<a.*?href="https\:\/\/y\.qq\.com\/.*(\/[0-9a-zA-Z]+)(\.html)?".*?>.*?<\/a>/g,
QQVIDEO_REG = /<a.*?href="https:\/\/v\.qq\.com\/.*\/([a-z|A-Z|0-9]+)\.html".*?>.*<\/a>/g,
YOUKU_REG = /<a.*?href="https:\/\/v\.youku\.com\/.*\/id_([a-z|A-Z|0-9|==]+)\.html".*?>.*<\/a>/g,
YOUTUBE_REG = /<a.*?href="https:\/\/www\.youtube\.com\/watch\?v\=([a-z|A-Z|0-9]{11})\".*?>.*<\/a>/g;
marked.setOptions({
breaks: false,
smartypants: false,
langPrefix: 'language-',
headerIds: false,
mangle: false
});
for (let i = 0; i < data.length; i++) {
let bbID = data[i].id
let memoUrl = memos + "m/" + bbID
let bbCont = data[i].content + ' '
let bbContREG = ''
let bbPos = ''
bbContREG += bbCont.replace(TAG_REG, "")
.replace(IMG_REG, "")
.replace(DEODB_LINK_REG, '')
.replace(LINK_REG, '<a class="primary" href="$2" target="_blank">$1</a>')
//标签
bbContREG = bbContREG.replace(/#(.*?)\s/g, '').replace(/\!?\[(.*?)\]\((.*?)\)/g, '').replace(/\{(.*?)\}/g, '')
bbPos = bbCont.match(/\{(.*?)\}/g);
bbPos = bbPos ? bbPos[0].replace(/\{(.*?)\}/,'$1') : "***";
let tagArr = bbCont.match(TAG_REG);
bbContREG = marked.parse(bbContREG)
.replace(BILIBILI_REG, "<div class='video-wrapper'><iframe src='//www.bilibili.com/blackboard/html5mobileplayer.html?bvid=$1&as_wide=1&high_quality=1&danmaku=0' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen='true'></iframe></div>")
.replace(NETEASE_MUSIC_REG, "<meting-js auto='https://music.163.com/#/song?id=$1'></meting-js>")
.replace(QQMUSIC_REG, "<meting-js auto='https://y.qq.com/n/yqq/song$1.html'></meting-js>")
.replace(QQVIDEO_REG, "<div class='video-wrapper'><iframe src='//v.qq.com/iframe/player.html?vid=$1' allowFullScreen='true' frameborder='no'></iframe></div>")
.replace(YOUKU_REG, "<div class='video-wrapper'><iframe src='https://player.youku.com/embed/$1' frameborder=0 'allowfullscreen'></iframe></div>")
.replace(YOUTUBE_REG, "<div class='video-wrapper'><iframe src='https://www.youtube.com/embed/$1' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen title='YouTube Video'></iframe></div>")
//解析 content 内 md 格式图片
let IMG_ARR = data[i].content.match(IMG_REG) || '',
IMG_ARR_Grid = '';
if (IMG_ARR) {
let IMG_ARR_Length = IMG_ARR.length,
IMG_ARR_Url = '';
if (IMG_ARR_Length !== 1) {
let IMG_ARR_Grid = " grid grid-" + IMG_ARR_Length
}
IMG_ARR.forEach(item => {
let imgSrc = item.replace(/!\[.*?\]\((.*?)\)/g, '$1')
IMG_ARR_Url += `<a href="${imgSrc}" data-fancybox="gallery${bbID}" class="fancybox" data-thumb="${imgSrc}"><img class="no-lazyload" src="${imgSrc}"></a>`
});
bbContREG += `<div class="resimg${IMG_ARR_Grid}">${IMG_ARR_Url}</div>`
}
//解析内置资源文件
if (data[i].resourceList && data[i].resourceList.length > 0) {
let resourceList = data[i].resourceList;
let imgUrl = '',
resUrl = '',
resImgLength = 0;
for (let j = 0; j < resourceList.length; j++) {
let restype = resourceList[j].type.slice(0, 5)
let resexlink = resourceList[j].externalLink
let resLink = resexlink ? resexlink :
memos + 'o/r/' + resourceList[j].id + '/' + (resourceList[j].publicId || resourceList[j].filename)
if (restype == 'image') {
imgUrl += `<a href="${resLink}" data-fancybox="gallery${bbID}" class="fancybox" data-thumb="${resLink}"><img class="no-lazyload" src="${resLink}"></a>`
resImgLength = resImgLength + 1
} else if (restype == 'video') {
imgUrl += `<div class="video-wrapper"><video controls><source src="${resLink}" type="video/mp4"></video></div>`
} else {
resUrl += `<a target="_blank" rel="noreferrer" href="${resLink}">${resourceList[j].filename}</a>`
}
}
if (imgUrl) {
let resImgGrid = ""
if (resImgLength !== 1) {
resImgGrid = "grid grid-" + resImgLength
}
bbContREG += `<div class="resimg ${resImgGrid}">${imgUrl}</div>`
}
if (resUrl) {
bbContREG += `<p class="bb-source">${resUrl}</p>`
}
}
let bbTime1 = getTime(data[i].createdTs);
let bbTime2 = getTimeAgo(data[i].createdTs);
let memosIdNow = memos.replace(/https\:\/\/(.*\.)?(.*)\..*/, 'id-$2-')
let emojiReaction = `<emoji-reaction theme="system" class="reaction" endpoint="https://api-emaction.immmmm.com" reacttargetid="${memosIdNow + 'memo-' + bbID}" style="line-height:normal;display:inline-flex;"></emoji-reaction>`
let commentBtn = ``
if (bbMemos.wlEnv) {
commentBtn = `<div class="comment-btn"><a class="msg-btn" data-id="${bbID}" onclick="loadWaline(this)"><span class="icon"><i class="fa-solid fa-message fa-fw"></i></span><span class="waline-memos-${bbID}"></span></a></div>`
}
result += `<li class="memo-${bbID}">
<div class="bb-item">
<div class="bb-head">
<img class="no-lightbox no-lazyload user-avatar" style="border-radius: 50%;" src="${bbMemos.avatar}">
<div class="info">
<span class="name">${bbMemos.name} ${icon}</span>
<span class="datatime"><i class="fa-solid fa-calendar-days WISTERIA fa-fw"></i> ${bbTime1}</span>
</div>
<a href="${memoUrl}" target="_blank"><i class="fa-solid fa-share-from-square"></i></a>
</div>
<div class="bb-content">
${bbContREG}
</div>
<div class="bb-bottom">
<div>
<span><i class="fa-solid fa-clock WISTERIA fa-fw"></i> ${bbTime2}</span>
<span><i class="fa-solid fa-location-dot WISTERIA fa-fw"></i> ${bbPos}</span>
</div>
<div class="emoji">${emojiReaction}</div>
${commentBtn}
</div>
<div id="container-${bbID}" class="item-waline d-none">
<div style="font-size:20px;padding:5px 10px;font-weight:600;"><i class="fa-solid fa-comments"></i> 评论</div>
<div id="waline-${bbID}"></div>
</div>
</div>
</li>`
} // end for
let bbBefore = "<section class='bb-timeline'><ul class='bb-list-ul'>"
let bbAfter = "</ul></section>"
resultAll = bbBefore + result + bbAfter
let loaderDom = document.querySelector('.loader') || ""
if (loaderDom) loaderDom.remove()
bbDom.insertAdjacentHTML('beforeend', resultAll);
if (document.querySelector('button.button-load')) document.querySelector('button.button-load').textContent = '加载更多';
}
function loadWaline(e) {
let memosId = e.getAttribute("data-id");
let walineDom = document.getElementById("container-" + memosId)
if (walineDom.classList.contains('d-none')) {
document.querySelectorAll('.item-waline').forEach((item) => {item.classList.add('d-none');})
if(document.getElementById("waline-" + memosId)){
walineDom.classList.remove('d-none');
window.scrollTo({
top: walineDom.offsetTop - 30,
behavior: "smooth"
});
window.initComment({
el: '#waline-' + memosId ,
serverURL: bbMemos.wlEnv ,
emoji: [
'自定义 emoji 地址',
],
path: bbMemos.memos + 'm/' + memosId,
comment: '.waline-memos-' + memosId,
});
}
}else{
walineDom.classList.add('d-none');
}
}
</script>
<script type="module">
import { init, commentCount } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';
window.initComment = function(params) {
try {
init({
el: params.el,
serverURL: params.serverURL,
emoji: params.emoji,
path: params.path,
comment: params.comment,
});
} catch (error) {
console.log(`Waline ${error}`);
}
}
window.getCommentCount = function(params) {
try {
commentCount({
serverURL: params.serverURL,
selector: params.selector,
path: params.path,
})
} catch (error) {
console.log(`Waline ${error}`);
}
}
</script>

好了,基本上已经完成了,使用hexo一键三连(hexo clean && hexo g && hexo d)就可以看到效果啦!

评论