Butterfly主题魔改:说说页面适配Memos

1525 字
8 分钟
Butterfly主题魔改:说说页面适配Memos

由于更换了Hugo框架,本文章内容已失效

效果见本博客的说说页面,代码有点乱,能用就行。

目前我使用的memos版本为0.24.0,以前的版本可能不适用。

开始#

先使用 hexo new page shuoshuo创建一个页面,并填入以下内容。

---
title: 说说
type: 'shuoshuo'
aside: false
shuoshuo_url: https://example.com
likes_url: https://api.example.com
---
{% raw %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.2/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.1/purify.min.js"></script>
<p id="loading"><i class="fa-solid fa-spinner fa-spin"></i>加载中...</p>
{% endraw %}

在shuoshuo_url中填写你的memos服务器地址。

修改主题文件#

在主题目录->layout->includes->page->shuoshuo.pug中,全部替换为以下内容:

//- - author:
//- avatar:
//- date:
//- content:
//- tags:
//- - tag1
//- - tag2
- page.toc = false
- const artalkServer = theme.artalk.server
- const artalkSite = theme.artalk.site
#article-container
if page.comments !== false && theme.comments.use
- commentsJsLoad = true
script.
(() => {
const commentDiv = `!{partial('includes/third-party/comments/index', {}, {cache: true})}`
const runDestroy = (shuoshuoComment) => {
if (!shuoshuoComment) return
for (const [key, fn] of Object.entries(shuoshuoComment)) {
if (key.startsWith('destroy')) fn()
}
}
window.addCommentToShuoshuo = e => {
const btn = e.target.closest('.shuoshuo-comment-btn')
if (!btn) return
const ele = btn.closest('.container').nextElementSibling
const { shuoshuoComment } = window
const isInclude = ele.classList.contains('no-comment')
runDestroy(shuoshuoComment)
if (isInclude) {
ele.classList.remove('no-comment')
ele.innerHTML = commentDiv
const key = `${location.pathname.replace(/\/$/, '')}?key=${ele.getAttribute('data-key')}`
btf.switchComments(ele, key)
shuoshuoComment.loadComment && shuoshuoComment.loadComment(ele, key)
}
}
})()
if page.shuoshuo_url
script.
(() => {
const limitConfig = !{ JSON.stringify(page.limit || {}) }
let pageToken = ""
let isLoading = false
let baseUrl = `!{url_for(page.shuoshuo_url)}/api/v1/memos?parent=users/1`
let count_url = `!{url_for(page.shuoshuo_url)}/api/v1/users/-/stats`
let page_title = document.querySelector(".page-title")
let totalCount = 0
const formatToTimeZone = (date) => {
const fullDate = date.length === 10 ? `${date} 00:00:00` : date
const visitorTimeZone = '#{config.timezone}' || Intl.DateTimeFormat().resolvedOptions().timeZone
const options = {
timeZone: visitorTimeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}
const [day, month, year, hour, minute, second] = new Intl.DateTimeFormat('en-GB', options)
.format(new Date(fullDate))
.match(/\d+/g)
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}
const createLoadButton = () => {
const existingBtn = document.querySelector("#loadmore")
if (!existingBtn && pageToken) {
const btn = document.createElement('button')
btn.id = 'loadmore'
btn.type = 'button'
btn.textContent = '加载更多'
btn.addEventListener('click', loadMoreHandler)
document.querySelector("#page").appendChild(btn)
}
}
const loadMoreHandler = async () => {
if (isLoading || !pageToken) return
const btn = document.querySelector("#loadmore")
btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i>`
btn.disabled = true
isLoading = true
try {
await loadShuoshuo(false)
} finally {
isLoading = false
btn.disabled = false
btn.textContent = '加载更多'
}
}
const loadShuoshuo = async (isFirstLoad = true) => {
try {
const currentUrl = isFirstLoad ? baseUrl : `${baseUrl}&pageToken=${pageToken}`
const response = await fetch(currentUrl)
const data = await response.json()
let memos = data.memos
if (isFirstLoad) {
const count_response = await fetch(count_url,{method:"POST"})
const count_data = await count_response.json()
totalCount = count_data.userStats[0].memoDisplayTimestamps.length
page_title.innerHTML = "共有" + totalCount + "条说说"
}
pageToken = data.nextPageToken || ""
if (!pageToken) {
const btn = document.querySelector("#loadmore")
btn.remove()
} else if (isFirstLoad) {
createLoadButton()
}
const container = document.getElementById('article-container')
let start = 0
const renderData = (dataSlice) => {
const content = dataSlice.map(item => {
let formattedDate = formatToTimeZone(item.createTime)
let date = ""
let pinned = ""
let resources = ""
let location = null
let isSingleImage = ''
if(btf.diffDate(formattedDate, false) <= 30) {
date = btf.diffDate(formattedDate, true)
}else{
date = formattedDate
}
if (item.pinned != ""){
pinned = `<i class="fa-solid fa-thumbtack"></i>`
}
const tags = item.tags && item.tags.map(tag => `<span class="shuoshuo-tag">${tag}</span>`).join('') || ''
if(item.resources.length != 0){
resources = item.resources.map(r => {
const isExternal = r.externalLink !== "";
const [thumbLink, fileUrl] = isExternal ? [
r.externalLink.replace("memos/", "memos/thumb/").split('?')[0],
r.externalLink
] : [
`!{url_for(page.shuoshuo_url)}/file/${r.name}/${r.filename}?thumbnail=true`,
`!{url_for(page.shuoshuo_url)}/file/${r.name}/${r.filename}`
];
return `<span class="shuoshuo-resources-image">
<a href="${fileUrl}" data-fancybox="gallery">
<img class="lazy" src="${thumbLink}">
</a>
</span>`;
}).join('');
}
if(item.location!=undefined){
location = item.location.placeholder
}
let content = item.content.replace(/^(#\S+\s*)+/, '')
content = DOMPurify.sanitize(marked.parse(content.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,"")))
const commentButton = item.name && !{commentsJsLoad}
? `
<div class="shuoshuo-comment-btn" onclick="addCommentToShuoshuo(event)">
<span class="artalk-count" data-page-key="/essay?key=${item.name}"><i class="fa-solid fa-spinner fa-spin"></i></span>
<i class="fa-solid fa-comments"></i>
</div>
<div class="shuoshuo-like-btn" onclick="addLike('${item.name}')">
<span class="like-count" data-page-name="${item.name}"><i class="fa-solid fa-spinner fa-spin"></i></span>
<i class="fa-solid fa-thumbs-up"></i>
</div>
`
: ''
const commentContainer = item.name
? `<div class="shuoshuo-comment no-comment" data-key="${item.name}"></div>`
: ''
return `
<div class="shuoshuo-item">
<div class="container">
<div class="shuoshuo-item-header">
<div class="shuoshuo-avatar">
<img class="no-lightbox" src="${item.avatar || '!{url_for(theme.avatar.img)}'}">
</div>
<div class="shuoshuo-info">
<div class="shuoshuo-author">${item.author || '!{config.author}'}</div>
<time class="shuoshuo-date" title="${formattedDate}">
${date}
</time>
</div>
<div class="shuoshuo-other-info">
${pinned}
${location ? `<div class="shuoshuo-location"><i class="fa-solid fa-location-dot"></i> ${location}</div>` : ''}
</div>
</div>
<div class="shuoshuo-content">${content}</div>
<div class="shuoshuo-resources${isSingleImage}">${resources}</div>
<div class="shuoshuo-footer ${tags ? 'flex-between' : 'flex-end'}">
${tags ? `<div class="shuoshuo-tags">${tags}</div>` : ''}
${commentButton}
</div>
</div>
${commentContainer}
</div>`
}).join('')
container.insertAdjacentHTML('beforeend', content)
window.lazyLoadInstance.update()
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
}
const handleIntersection = (entries) => {
if (!entries[0].isIntersecting) return
observer.unobserve(entries[0].target)
const slice = memos.slice(start, start + 10)
renderData(slice)
start += 10
if (start < memos.length) {
setTimeout(() => observer.observe(container.lastElementChild), 100)
} else {
observer.disconnect()
}
};
const observer = new IntersectionObserver(handleIntersection, {
root: null,
rootMargin: '0px',
threshold: 1.0
})
renderData(memos.slice(start, 10))
start += 10
// 移除加载文本
let loadingText = document.querySelector("#loading")
if (loadingText){
loadingText.remove()
}
// 获取点赞数
if(page.likes_url != ""){
let likeId = localStorage.getItem('like_id') || crypto.randomUUID();
localStorage.setItem('like_id', likeId);
const likeGroup = document.querySelectorAll('.like-count')
const pageNameArray = Array.from(likeGroup).map(i => i.getAttribute('data-page-name'))
try{
const params = new URLSearchParams({
'page_names': pageNameArray
})
const likesData = await fetch(`!{url_for(page.likes_url)}/like/pages?like_id=${likeId}&${params}`)
const likesResult = await likesData.json()
const likedArray = likesResult.liked
pageNameArray.forEach((key, index) => {
const btn = likeGroup[index].closest('.shuoshuo-like-btn');
likeGroup[index].textContent = likesResult.data[key]
btn.classList.toggle('active', likedArray.includes(key));
})
}catch(e){
console.log(e)
pageNameArray.forEach((key, index) => {
likeGroup[index].textContent = "-"
})
}
}
// 获取评论数
const eleGroup = document.querySelectorAll('.artalk-count')
const keyArray = Array.from(eleGroup).map(i => i.getAttribute('data-page-key'))
const headerList = {
method: 'GET',
}
const searchParams = new URLSearchParams({
'site_name': '!{artalkSite}',
'page_keys': keyArray
})
const res = await fetch(`!{artalkServer}/api/v2/stats/page_comment?${searchParams}`, headerList)
const result = await res.json()
keyArray.forEach((key, index) => {
eleGroup[index].textContent = result.data[key] || 0
})
if(Prism != null) Prism.highlightAll()
if (container.lastElementChild) observer.observe(container.lastElementChild)
} catch (error) {
console.error(error)
const main = document.querySelector("#page")
main.innerHTML = '<h1>加载失败,请刷新重试</h1>'
}
};
window.pjax ? loadShuoshuo() : window.addEventListener('load', () => loadShuoshuo(true))
})()
else
if site.data.shuoshuo
each i in shuoshuoFN(site.data.shuoshuo, page)
.shuoshuo-item
.container
.shuoshuo-item-header
.shuoshuo-avatar
img.no-lightbox(src=i.avatar || url_for(theme.avatar.img))
.shuoshuo-info
.shuoshuo-author=i.author || config.author
time.shuoshuo-date(title=i.date)=i.date
.shuoshuo-content
!=markdown(i.content)
.shuoshuo-footer(class=i.tags && i.tags.length ? 'flex-between' : 'flex-end')
if i.tags
.shuoshuo-tags
each tag in i.tags
span.shuoshuo-tag=tag
if i.key && commentsJsLoad
.shuoshuo-comment-btn(onclick='addCommentToShuoshuo(event)')
i.fa-solid.fa-comments
if i.key && commentsJsLoad
.shuoshuo-comment.no-comment(data-key=i.key)
#article-container.container
!= page.content

修改的内容主要为:适配memos的api格式,能获取tags以及图片,并能加载更多说说。

注意,评论数统计功能只支持Artalk,如果使用其他评论系统,需自行修改代码。

插入CSS#

在刚刚创建的shuoshuo.md中插入样式:

---
title: 说说
type: 'shuoshuo'
aside: false
top_img: false
shuoshuo_url: https://memos.dotuoodo.top
likes_url: https://api.example.com
---
{% raw %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.2/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.1/purify.min.js"></script>
<p id="loading"><i class="fa-solid fa-spinner fa-spin"></i>加载中...</p>
<style>
/* 说说 */
.layout.hide-aside {
max-width: 720px;
}
#loadmore {
width: max-content;
padding: 15px 30% 15px 30%;
margin: 0 auto;
display: block;
border: 1px solid transparent;
border-color: var(--btn-beautify-color, #777);
background-color: transparent;
color: var(--btn-beautify-color, #777);
transition: .3s;
border-radius: 5px;
}
#loadmore:hover{
background-color: var(--btn-beautify-color, #428bca);
color: #fff !important;
}
.shuoshuo-other-info {
margin-left: auto;
}
#article-container .shuoshuo-content {
padding: 10px 0 0px !important;
}
.shuoshuo-resources:not(.single-image) {
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
margin-bottom: 10px;
}
.shuoshuo-resources:empty {
margin-bottom: 3px;
}
.shuoshuo-resources-image {
width: 100%;
height: 0;
padding-bottom: 100%;
position: relative;
overflow: hidden;
}
.shuoshuo-resources-image img {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
object-fit: cover;
transform: translate(-50%, -50%);
}
/* 当只有一张图片时,调整显示方式 */
.shuoshuo-resources .shuoshuo-resources-image:only-child {
width: 100%;
height: auto;
padding-bottom: 0;
display: block;
}
.shuoshuo-resources .shuoshuo-resources-image:only-child img {
position: static;
width: 100%;
height: auto;
object-fit: cover;
transform: none;
}
.shuoshuo-resources.single-image {
display: block;
}
.inline-image {
display: inline !important;
vertical-align: middle;
}
#article-container .shuoshuo-item {
padding: 20px 30px 20px !important;
}
#article-container .shuoshuo-footer {
display: flex;
justify-content: space-between;
position: relative;
}
#article-container .shuoshuo-footer .shuoshuo-comment-btn {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
#article-container .shuoshuo-footer .shuoshuo-like-btn {
margin-left: auto;
}
#article-container .shuoshuo-footer .shuoshuo-like-btn {
padding: 2px;
color: #90a4ae;
cursor: pointer;
}
#article-container .shuoshuo-footer .shuoshuo-like-btn.active {
color: #49b1f5;
}
</style>
{% endraw %}

参考#

0.22.0版本Memos API介绍-星の野

将最新版的memos嵌入hexo-Keyle’s Blog

更新日志#

2025.03.03:

【新增】点赞功能,需要后端

【修复】出现多个”加载更多”按钮,优化代码逻辑

2025.03.01:

【修复】置顶说说无法显示在最前面

【新增】显示说说总数

2025.02.25:

【适配】对OSS外链图片的支持

2025.02.23:

【适配】memos 0.24.0

【新增】显示位置功能、加载提示

【删除】相对时间显示功能

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Butterfly主题魔改:说说页面适配Memos
https://blog.dotuoodo.top/posts/memos-essay/
作者
DOTUOODO
发布于
2024-11-23
许可协议
CC BY-NC-SA 4.0

评论

Profile Image of the Author
DOTUOODO
Nothing is immortal, but at least we can be extraordinary.
公告
欢迎来到我的博客!目前还在施工中...
分类
标签

文章目录