预览
概述
点赞组件主要包括两个功能,一个是加载文章时,显示出最新点赞数,另一个是当用户点击点赞按钮时,点赞数自动加一,同时数据同步到云端数据库。
对于第一个功能,我设计成每次进入文章时都自动从云端拉取点赞数,主要请求参数为文章的 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
| document.addEventListener("DOMContentLoaded", () => { ctrl.getIp(); ctrl.refreshLikeCount(); });
document.addEventListener("pjax:complete", () => { ctrl.refreshLikeCount(); })
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 的弹窗,可以看上一篇文章 给友链朋友圈加上收藏栏