预览

点赞组件预览图

概述

点赞组件主要包括两个功能,一个是加载文章时,显示出最新点赞数,另一个是当用户点击点赞按钮时,点赞数自动加一,同时数据同步到云端数据库。

对于第一个功能,我设计成每次进入文章时都自动从云端拉取点赞数,主要请求参数为文章的 abbrlink 代码,采用 crc32 算法,一文一码,具有唯一性,也可以作为数据库的主键使用。文章的 abbrlink 可通过 window.location.pathname 得到。

对于第二个功能,为了避免同一用户重复点赞,我增加了一项 IP 参数,一篇文章一个 IP 只能点赞一次,当然,对于代理 IP 是无法识别的。由于 IP 的固定性,所以在网站首次加载时便将 IP 地址存入全局变量中,无需每次请求都获取一次 IP。

后端代码

数据库依旧采用 Supabase,新建一个 LikeCount 表,设三个键:index、count、ips,其中 index 为主键,保存文章 abbrlink 码,count 为点赞数,ips 为用户 IP 地址。表内无需插值,遇到新文章的请求,后端会自动在表中新建一行数据。

仿照 上篇文章 的后端部分内容,补充以下代码即可。

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
// 文章点赞计数
app.get('/likecount', async (req, res) => {
try {
const mode = req.query.mode.toString();
const id = req.query.id.toString();
if (mode == 'get') {
const { data: filterData, error: error1 } = await supabase
.from('LikeCount')
.select()
.eq('index', id)
if (error1) {
console.error('Error:', error1);
res.status(404).json({ code: '404', message: '查询失败', content: '' });
} else {
if (!filterData.length) {
const { data: insertData, error: error2 } = await supabase
.from('LikeCount')
.insert([
{ index: id, count: 0 }
])
.select()
if (error2) {
console.error('Error:', error2);
res.status(500).json({ code: '500', message: '新文章录入失败', content: insertData });
} else {
res.status(201).json({ code: '201', message: '新文章录入成功', content: insertData });
}
} else {
console.log('Data Search completely');
res.status(200).json({ code: '200', message: '查询成功', content: filterData });
}
}
} else if (mode == 'add') {
const ip = req.query.ip.toString();
const { data: oldData, error: error3 } = await supabase
.from('LikeCount')
.select()
.eq('index', id)
if (error3) {
console.error('Error:', error3);
res.status(404).json({ code: '404', message: '查询失败', content: oldData });
} else {
var ips = oldData[0].ips
if (ips.includes(ip)) {
res.status(200).json({ code: '205', message: '您已经点过赞啦', content: oldData });
} else {
var ipsArray = JSON.parse(ips)
ipsArray.push(ip)
var newIps = JSON.stringify(ipsArray)
var newNum = oldData[0].count + 1
const { data: updateData, error: error4 } = await supabase
.from('LikeCount')
.update({ count: newNum, ips: newIps })
.eq('index', id)
.select()
if (error4) {
console.error('Error:', error4);
res.status(501).json({ code: '501', message: '更新失败', content: oldData });
} else {
console.log('Data update completely');
res.status(200).json({ code: '200', message: '更新成功', content: updateData });
}
}
}
}
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
}
});

前端代码

前端设计按个人喜好,我是将点赞按钮和打赏按钮组合在一起,毕竟赞赏嘛,先赞再赏嘛哈哈哈哈。

修改 themes/butterfly/layout/includes/post/reward.pug 路径文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.post-reward
.like-button(onclick='ctrl.sendArticleLike()')
i.fas.fa-thumbs-up
= ' 赞 '
span.like-count= '0'
span.load
i.fas.fa-spinner.fa-spin
.reward-button
i.fas.fa-qrcode
= ' ' + _p('donate')
.reward-main
ul.reward-all
each item in theme.reward.QR_code
- var clickTo = item.link ? item.link : item.img
li.reward-item
a(href=url_for(clickTo) data-fancybox='gallery' target='_blank')
img.post-qr-code-img(src=url_for(item.img) alt=item.text)
.post-qr-code-desc=item.text

修改 themes\butterfly\source\css_layout\reward.styl 路径文件:

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
.post-reward
position: relative
margin-top: 80px
width: 100%
text-align: center
pointer-events: none
display: flex
justify-content: center
gap: 10px

& > *
pointer-events: auto

.like-button
display: inline-block
padding: 4px 12px
background: red
color: rgba(255,255,255,.8)
cursor: pointer
border-radius: 0.5rem
width: 100px
pointer-events: all
&:not(.loading):hover
background: rgb(255, 90, 90)

&.loading
cursor: default
pointer-events: none
.like-count
display: none
.load
display: inline-block

.like-count
width: 20px
display: inline-block
.load
width: 20px
display: none

.reward-button
display: inline-block
padding: 4px 12px
background: var(--gavin-blue3)
color: rgba(255,255,255,.8)
cursor: pointer
border-radius: 0.5rem
width: 100px
&:hover
background: var(--gavin-blue1)

& > .reward-main
display: block

.reward-main
position: absolute
bottom: 40px
left: 0
z-index: 100
display: none
padding: 0 0 10px
width: 100%

.reward-all
display: inline-block
margin: 0
padding: 20px 10px
border-radius: 4px
background: var(--reward-pop)

&:before
position: absolute
bottom: 0
left: 0
width: 100%
height: 10px
content: ''

&:after
position: absolute
right: 0
bottom: 2px
left: 0
margin: 0 auto
width: 0
height: 0
border-top: 13px solid var(--reward-pop)
border-right: 13px solid transparent
border-left: 13px solid transparent
content: ''

.reward-item
display: inline-block
padding: 0 8px
list-style-type: none
vertical-align: top

img
width: 110px
height: 110px

.post-qr-code-desc
width: 110px
line-height: 1
font-size: 90%
color: $reward-pop-up-color

添加自定义 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
// pjax适配
document.addEventListener("DOMContentLoaded", () => {
ctrl.getIp();
ctrl.refreshLikeCount();
}); //第一次加载

document.addEventListener("pjax:complete", () => {
ctrl.refreshLikeCount();
}) // pjax加载完成(切换页面)后再执行一次

var ipAddress = '';

var ctrl = {
getIp() {
fetch('https://api.ipify.org?format=json')
.then(response => response.json())
.then(data => {
ipAddress = data.ip;
console.log('您的 IP 地址:' + ipAddress);
})
.catch(error => {
console.error('获取 IP 地址失败:', error);
});
},

refreshLikeCount() {
var p = window.location.pathname
var q = p.substring(1,5)
if (q == 'post') {
var i = p.substring(6,14)
fetch(`https://xxx.xxxx.xxx/likecount?mode=get&id=${i}`)
.then(response => response.json())
.then(data => {
if (data.code == 200) {
var likeCount = data.content[0].count
document.querySelector(".post-reward .like-button .like-count").innerText = likeCount
} else console.log(data.message)
})
.catch(error => {
console.error('获取点赞信息失败:', error)
})
}
},

sendArticleLike() {
var a = document.querySelector(".post-reward .like-button")
var i = window.location.pathname.substring(6,14)
a.classList.add("loading")
fetch(`https://xxx.xxxx.xxx/likecount?mode=add&id=${i}&ip=${ipAddress}`)
.then(response => response.json())
.then(data => {
if (data.code == 200) {
var likeCount = data.content[0].count
a.querySelector(".like-count").innerText = likeCount
a.classList.remove("loading")
tools.showMessage("感谢您的认可!", "success", 2)
} else if(data.code == 205) {
a.classList.remove("loading")
tools.showMessage(data.message, "warning", 2)
} else {
a.classList.remove("loading")
console.log(data.message)
tools.showMessage(data.message, "error", 2)
}
})
.catch(error => {
console.error('获取点赞信息失败:', error)
a.classList.remove("loading")
})
}
}

注意,上面用到的 tools.showMessage 方法依赖于 ElementUI 库,如果你有自己的弹窗通知系统,可以将它替换掉。
如果你也要用 ElementUI 的弹窗,可以看上一篇文章 给友链朋友圈加上收藏栏