Butterfly主题魔改:说说页面适配Memos
1525 字
8 分钟
Butterfly主题魔改:说说页面适配Memos
由于更换了Hugo框架,本文章内容已失效

效果见本博客的说说页面,代码有点乱,能用就行。
目前我使用的memos版本为0.24.0,以前的版本可能不适用。
开始
先使用 hexo new page shuoshuo创建一个页面,并填入以下内容。
---title: 说说type: 'shuoshuo'aside: falseshuoshuo_url: https://example.comlikes_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: falsetop_img: falseshuoshuo_url: https://memos.dotuoodo.toplikes_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 %}参考
更新日志
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/最后更新于 2025-08-06
部分内容可能已过时











