PaperMod主题魔改:添加说说页面
3425 字
17 分钟
PaperMod主题魔改:添加说说页面
也算是重操旧业了,不知道为什么我就是会对这种功能特别感兴趣。
在AI的帮助下终于搞好了,并且把点赞和评论功能完善了一下,具体效果见本博客的说说页面。
我使用的Memos版本为0.26.1,不知道Memos改了多少次api了,以前的版本可能不适用。
从0.25.2版本开始,使用s3存储方式的图片可以自动生成缩略图了。
功能
✅渲染Memos卡片
✅统计说说条数
✅显示外链图片附件(S3)
✅图片缩略图
✅视频播放
✅评论功能(Artalk)
✅点赞功能1
✅显示标签
✅加载更多(无限滚动)
✅图片灯箱(Glightbox)
✅支持Markdown(marked.js)
✅位置显示
✅显示引用和被引用的memos
✅显示单条memos
🔳显示非外链图片附件(未测试)
开始
创建 content/memos/_index.md文件,并填入以下内容。
---title: "说说"build: render: alwayscascade: - build: list: local publishResources: false render: never---在 hugo.yml中添加Memos和Artalk服务器地址:
params: memosURL: 'https://memos.xxx.com' artalk: server: 'https://comment.xxx.com' site: "xxx's Blog"创建页面模板
创建 layouts/memos/list.html文件,并填入以下内容。
{{- define "main" }}{{ $dateformat := .Params.DateFormat }}{{ $css := resources.Get "css/extended/memos.css" | minify | fingerprint }}<article class="post-single"> <header class="page-header"> <h1> {{- (printf "%s" .Title ) | htmlUnescape -}} </h1> <div class="post-description"> 共有 - 条说说 </div> </header>
<div class="post-content"> <div class="memos-list"> <div id="loading" class="infinite-loader"> <div class="loader-spinner"></div>加载中... </div> </div> </div>
</article>
<footer class="page-footer"> <div id="load-more-container"></div></footer>
<link href="/css/glightbox.min.css" rel="stylesheet" /><link href="/css/Artalk.css" rel="stylesheet" /><script src="/js/marked.umd.js" defer></script><script src="/js/glightbox.min.js" async></script>
<script> PageToken = ''; isLoading = false; artalk = null; activeCommentContainer = null; galleryLightbox = null;
async function loadMemos(Token) { isLoading = true; memosURL = "{{ .Site.Params.memosURL }}"; apiURL = !Token ? memosURL + "/api/v1/memos?parent=users/1&orderBy=pinned" : memosURL + `/api/v1/memos?parent=users/1&pageToken=${Token}`; data = null; statsURL = memosURL + "/api/v1/users/1:getStats"
try { const response = await fetch(apiURL); if (!response.ok) { throw new Error(`请求失败: ${response.status}`); } data = await response.json(); PageToken = data.nextPageToken || null; if (!PageToken || data.memos.length === 0) showEndOfListMessage(); } catch (error) { console.error("请求错误:", error); } finally { if (!Token){ const statsResponse = await fetch(statsURL); let stats = await statsResponse.json(); document.querySelector(".post-description").innerHTML = "共有" + stats.totalMemoCount + "条说说" } isLoading = false; } return data; }
// 渲染单个memo的函数 function renderMemo(memo, options = {}) { const { isModal = false, showFullContent = false } = options;
const memosLocation = memo.location || null; let locationHTML = ''; if (memosLocation !== null) locationHTML = `<div class="memos-location">${memosLocation.placeholder}</div>`;
const time = new Date(memo.createTime).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
let attachmentsHTML = ''; let tagsHTML = '';
if(memo.tags && memo.tags.length !== 0){ tagsHTML = '<div class="memos-tags">' + memo.tags.map(tag => `<span class="memos-tag">${tag}</span>`).join('') + '</div>'; }
if(memo.attachments && memo.attachments.length !== 0){ attachmentsHTML = '<div class="memos-attachments">'; memo.attachments.forEach(item => { const fileExtension = item.externalLink.split('.').pop().toLowerCase().split('?')[0]; const isVideo = ['mp4', 'webm'].includes(fileExtension); const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension);
if (isImage) { const thumbLink = memosURL + "/file/" + item.name + "/" + item.filename + "?thumbnail=true"; attachmentsHTML += ` <div class="memos-attachments-image"> <a href="${item.externalLink}" class="glightbox" data-gallery="${isModal ? 'modal-gallery' : 'gallery'}"> <img src="${thumbLink}" loading="lazy"> </a> </div> `; } else if (isVideo) { attachmentsHTML += ` <div class="memos-attachments-video"> <video controls preload="metadata" class="memos-video-player" style="max-width: 100%; height: auto;"> <source src="${item.externalLink}" type="video/${fileExtension}"> 您的浏览器不支持 HTML5 视频标签。 </video> </div> `; } else { attachmentsHTML += ` <div class="memos-attachments-file"> <a href="${item.externalLink}" target="_blank" download>下载文件 (${fileExtension})</a> </div> `; } }); attachmentsHTML += '</div>'; }
const content = marked.parse(memo.content.replace(/^(#\S+\s*)+/, ''));
// 添加置顶图标 let pinnedHTML = ''; if (memo.pinned) { pinnedHTML = '<div class="memos-pinned-icon">置顶</div>'; }
// 收集引用和被引用关系 const referencedMemos = []; // 当前memo引用的memos const referredByMemos = []; // 引用当前memo的memos
if (memo.relations && memo.relations.length > 0) { memo.relations.forEach(relation => { if (relation.type === 'REFERENCE') { // 判断是引用还是被引用 if (relation.memo && relation.memo.name === memo.name && relation.relatedMemo) { referencedMemos.push({ name: relation.relatedMemo.name, snippet: relation.relatedMemo.snippet }); } // 如果当前memo是被引用的memo if (relation.relatedMemo && relation.relatedMemo.name === memo.name && relation.memo) { referredByMemos.push({ name: relation.memo.name, snippet: relation.memo.snippet }); } } }); }
// 添加引用relations显示 let relationsHTML = ''; if (referencedMemos.length > 0 || referredByMemos.length > 0) { relationsHTML = '<div class="memos-relations">';
// 显示被引用(被哪些memos引用) if (referredByMemos.length > 0) { relationsHTML += '<div class="memos-relation-section">'; relationsHTML += '<div class="memos-relation-title">被引用:</div>'; referredByMemos.forEach(ref => { relationsHTML += `<div class="memos-relation-item" onclick="showSingleMemo('${ref.name}')">${ref.snippet}</div>`; }); relationsHTML += '</div>'; }
// 显示引用(引用了哪些memos) if (referencedMemos.length > 0) { relationsHTML += '<div class="memos-relation-section">'; relationsHTML += '<div class="memos-relation-title">引用:</div>'; referencedMemos.forEach(ref => { relationsHTML += `<div class="memos-relation-item" onclick="showSingleMemo('${ref.name}')">${ref.snippet}</div>`; }); relationsHTML += '</div>'; }
relationsHTML += '</div>'; }
// 生成HTML const html = ` <div class="memos-item" id="memo-${memo.name.replace('memos/', '')}"> <div class="memos-item-body"> ${pinnedHTML} <div class="memos-content"> ${content} </div> ${attachmentsHTML} ${tagsHTML} ${relationsHTML} <div class="memos-item-bottom"> <div class="memos-time"> ${time} </div> ${locationHTML} <button class="memos-like-btn" onclick="addLikeHandler(event, ${isModal})" data-page-key="${memo.name}"> <svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> <span class="like-count" data-page-key="${memo.name}">-</span> </button> ${!isModal ? `<button class="memos-comment-btn" onclick="showComment(event)" data-page-key="${memo.name}"> <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M281.535354 387.361616c-31.806061 0-57.664646 26.763636-57.664647 59.733333 0 32.969697 25.858586 59.733333 57.664647 59.733334s57.664646-26.763636 57.664646-59.733334c0-33.09899-25.858586-59.733333-57.664646-59.733333z m230.529292 0c-31.806061 0-57.664646 26.763636-57.664646 59.733333 0 32.969697 25.729293 59.733333 57.664646 59.733334 31.806061 0 57.535354-26.763636 57.535354-59.733334 0-33.09899-25.858586-59.733333-57.535354-59.733333z m230.4 0c-31.806061 0-57.664646 26.763636-57.664646 59.733333 0 32.969697 25.858586 59.733333 57.664646 59.733334s57.664646-26.763636 57.664647-59.733334c-0.129293-33.09899-25.858586-59.733333-57.664647-59.733333z m115.2-270.222222H166.335354c-63.612121 0-115.2 53.527273-115.2 119.59596v390.981818c0 65.939394 52.751515 126.836364 117.785858 126.836363h175.579798c30.513131 32.581818 157.220202 149.979798 157.220202 149.979798 5.559596 5.818182 14.739394 5.818182 20.29899 0 0 0 92.832323-91.410101 153.212121-149.979798h179.717172c65.034343 0 117.785859-60.89697 117.785859-126.836363V236.606061c0.129293-65.939394-51.458586-119.466667-115.070708-119.466667z m57.535354 510.577778c0 32.969697-27.668687 67.620202-60.250505 67.620202H678.335354c-21.462626 0-40.727273 21.979798-40.727273 21.979798l-124.121212 114.941414-124.121212-114.941414s-23.660606-21.979798-43.830303-21.979798H168.921212c-32.581818 0-60.250505-34.650505-60.250505-67.620202V236.606061c0-32.969697 25.729293-59.733333 57.664647-59.733334h691.329292c31.806061 0 57.535354 26.763636 57.535354 59.733334v391.111111z m0 0"></path></svg> <span class="artalk-count" data-page-key="${memo.name}">-</span> </button>` : ''} </div> </div> </div> `;
return html; }
// 渲染memos列表 function appendMemos(memos) { const domContent = document.querySelector(".memos-list"); let renderHTML = '';
memos.forEach(el => { renderHTML += renderMemo(el); });
const loadingEl = document.getElementById('loading'); if (loadingEl) loadingEl.remove();
domContent.insertAdjacentHTML('beforeend', renderHTML);
setTimeout(initGallery, 50); }
// 显示单个memo的弹窗 async function showSingleMemo(memoId) { // 防止重复弹窗 if (document.querySelector('.memo-modal')) return;
// 保存当前滚动位置 scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
// 禁止body滚动 document.body.classList.add('no-scroll'); document.body.style.top = `-${scrollPosition}px`;
// 创建弹窗结构(先显示加载动画) const modal = document.createElement('div'); modal.className = 'memo-modal'; modal.innerHTML = ` <div class="memo-modal-overlay"></div> <button class="memo-modal-close" onclick="closeMemoModal()">×</button> <div class="memo-modal-content"> <div class="memo-modal-loading"> <div class="memo-loading-text">加载中...</div> </div> <div class="memo-modal-body" id="memo-modal-content" style="display: none;"> </div> </div> `;
document.body.appendChild(modal);
try { const response = await fetch(`${memosURL}/api/v1/${memoId}`); if (!response.ok) { throw new Error(`请求失败: ${response.status}`); } const memo = await response.json();
const loadingEl = modal.querySelector('.memo-modal-loading'); const contentEl = modal.querySelector('.memo-modal-body');
// 渲染memo内容 contentEl.innerHTML = renderMemo(memo, { isModal: true });
loadingEl.style.opacity = '0'; loadingEl.style.display = 'none'; contentEl.style.display = 'block';
// 添加评论功能到弹窗 if (memo.visibility === 'PUBLIC') { const commentContainer = document.createElement('div'); commentContainer.className = 'artalk-comment-box'; commentContainer.dataset.target = memo.name; contentEl.querySelector('.memos-item-body').appendChild(commentContainer);
initArtalk(commentContainer, memo.name); }
// 获取点赞数 await fetchLikeAndCommentCounts([memo.name]);
// 初始化弹窗内的图片画廊 setTimeout(() => { const modalLightbox = GLightbox({ selector: '.memo-modal .glightbox' }); }, 50);
} catch (error) { console.error('获取memo详情失败:', error); // 显示错误信息 const loadingEl = modal.querySelector('.memo-modal-loading'); loadingEl.innerHTML = ` <div class="memo-loading-error"> <div class="error-text">加载失败 ${error.message}</div> </div> `; } }
// 关闭弹窗 function closeMemoModal() { const modal = document.querySelector('.memo-modal'); if (!modal) return;
// 销毁弹窗中的评论框 if (artalk && document.querySelector('.memo-modal .artalk-comment-box')) { artalk.destroy(); artalk = null; }
// 添加关闭动画 modal.classList.add('closing');
setTimeout(() => { // 恢复body滚动 document.body.classList.remove('no-scroll'); document.body.style.top = '';
// 恢复滚动位置 if (scrollPosition) { window.scrollTo(0, scrollPosition); }
modal.remove(); }, 300); // 等待动画完成 }
// 初始化画廊函数(在动态内容加载后调用) function initGallery() { // 如果已有实例则销毁 if (galleryLightbox) { galleryLightbox.destroy(); }
galleryLightbox = GLightbox({ selector: '.glightbox' }); }
function initScrollListener() { window.addEventListener('scroll', throttle(scrollHandler,100)); }
//节流函数 function throttle(fn, delay) { var last = 0; return function() { var now = Date.now(); if (now - last > delay) { last = now; fn.apply(this, arguments); } }; }
// 滚动处理函数 function scrollHandler() { if (isLoading || !PageToken) return;
const scrollPosition = window.innerHeight + window.scrollY; const pageHeight = document.documentElement.scrollHeight; const loadThreshold = 800;
if (scrollPosition > pageHeight - loadThreshold) { loadMoreContent(); } }
// 加载更多内容 async function loadMoreContent() { dom = document.querySelector('.infinite-loader'); if (isLoading || dom !== null) return; isLoading = true;
const loader = document.createElement('div'); loader.className = 'infinite-loader'; loader.innerHTML = '<div class="loader-spinner"></div>加载中...'; document.getElementById('load-more-container').appendChild(loader);
const data = await loadMemos(PageToken); if (data && data.memos) { loader.remove(); isLoading = false; appendMemos(data.memos); await fetchLikeAndCommentCounts(data.memos.map(memo => memo.name)); }
if (!PageToken || data.memos.length === 0) { showEndOfListMessage(); } }
function showEndOfListMessage() { const container = document.getElementById('load-more-container'); if (document.querySelector('.no-more-items')) return;
const endMessage = document.createElement('div'); endMessage.className = 'no-more-items'; endMessage.innerHTML = `<p>没有更多内容了</p>`; container.appendChild(endMessage); }
// 获取评论数量 async function fetchLikeAndCommentCounts(keys) { if (!keys || keys.length === 0) return;
const apiURL = "{{ .Site.Params.apiURL }}"; const likeId = localStorage.getItem('like_id');
const likeParams = new URLSearchParams(); likeParams.append('page_names', keys.join(',')); if (likeId) likeParams.append('like_id', likeId);
const commentParams = new URLSearchParams({ 'site_name': '{{ .Site.Params.artalk.site }}', 'page_keys': keys.join(',') });
try { const likeRes = await fetch(`${apiURL}/like/pages?${likeParams}`); const likeResult = await likeRes.json(); const commentRes = await fetch(`{{ .Site.Params.artalk.server }}/api/v2/stats/page_comment?${commentParams}`); const commentResult = await commentRes.json();
keys.forEach(key => { const likeCount = likeResult.data[key] || 0; const likeCountElements = document.querySelectorAll(`.like-count[data-page-key="${key}"]`); likeCountElements.forEach(el => el.textContent = likeCount); if (likeId && likeResult.liked) { likeResult.liked.forEach(key => { document.querySelectorAll(`.memos-like-btn[data-page-key="${key}"]`).forEach(btn => { btn.classList.add('liked'); }); }); }
const commentElements = document.querySelectorAll(`.artalk-count[data-page-key="${key}"]`); commentElements.forEach(el => { el.textContent = commentResult.data?.[key] || 0; }); }); } catch (error) { console.error(error); } }
function showComment(event) { event.stopPropagation(); const button = event.currentTarget; const memoId = button.dataset.pageKey;
// 判断是否在弹窗中 const isInModal = button.closest('.memo-modal') !== null; const containerSelector = isInModal ? '.memo-modal .memos-item-body' : '.memos-item-body'; const targetContainer = button.closest(containerSelector);
// 如果已经打开同一个评论框,则关闭它 const existingCommentBox = targetContainer.querySelector('.artalk-comment-box'); if (existingCommentBox && existingCommentBox.dataset.target === memoId) { if (artalk) { artalk.destroy(); artalk = null; } existingCommentBox.remove(); if (activeCommentContainer === existingCommentBox) { activeCommentContainer = null; } return; }
// 关闭其他评论框 destroyCommentBox();
// 创建新的评论框 const commentContainer = document.createElement('div'); commentContainer.className = 'artalk-comment-box'; commentContainer.dataset.target = memoId; targetContainer.appendChild(commentContainer);
// 初始化Artalk initArtalk(commentContainer, memoId); activeCommentContainer = commentContainer; }
function destroyCommentBox() { if (artalk) { artalk.destroy(); artalk = null; }
if (activeCommentContainer) { activeCommentContainer.remove(); activeCommentContainer = null; } }
async function initArtalk(container, pageKey) { artalk = Artalk.init({ el: container, pageKey: pageKey, server: '{{ .Site.Params.artalk.server }}', site: '{{ .Site.Params.artalk.site }}', darkMode: document.body.className.includes("dark"), pvAdd: false }); }
//点赞处理 async function addLikeHandler(event) { event.stopPropagation(); const button = event.currentTarget; const pageKey = button.dataset.pageKey; const countSpan = button.querySelector('.like-count');
const isLiked = button.classList.contains('liked'); const newCount = isLiked ? parseInt(countSpan.textContent) - 1 : parseInt(countSpan.textContent) + 1; button.classList.toggle('liked'); countSpan.textContent = newCount;
try { const response = await addLike(pageKey); if (response.status!=="success") { throw new Error(response.error || "Unknown error"); button.classList.toggle('liked'); countSpan.textContent = isLiked ? newCount + 1 : newCount - 1; } } catch (error) { button.classList.toggle('liked'); countSpan.textContent = isLiked ? newCount + 1 : newCount - 1; } }
// 初始渲染 async function renderMemos() { if(typeof Artalk =='undefined') { const Artalk = await import('/js/Artalk.js'); } const data = await loadMemos(); appendMemos(data.memos); await fetchLikeAndCommentCounts(data.memos.map(memo => memo.name)); initScrollListener(); }
renderMemos();</script>{{- end }}{{/* end main */}}注意,评论功能只支持Artalk,如果使用其他评论系统,需自行修改代码。
CSS
在 assets/css/extended/memos.css中:
.memos-container { max-width: 600px; margin: 0 auto;}
.memos-item { background-color: var(--entry); border-radius: 16px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); margin-bottom: 20px; overflow: hidden; transition: all 0.3s ease;}
.memos-item:hover { box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);}
.memos-item-body { padding: 20px; position: relative;}
.memos-content { margin-bottom: 15px; margin-top: 4px; font-size: 16px; color: var(--content);}
/* 标签样式 */.memos-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 15px;}
.memos-tag { display: block; padding: 0 14px; color: var(--secondary); font-size: 14px; line-height: 34px; background: var(--code-bg); border-radius: 8px;}
.memos-tag:hover { background: var(--border); cursor: pointer;}
/* 图片布局处理*/.memos-attachments { position: relative; margin-bottom: 15px; width: 100%;}
/* 一张图片时的大图样式 */.memos-attachments:has(.memos-attachments-image:only-child) .memos-attachments-image { width: 100%;}
.memos-attachments:has(.memos-attachments-image:only-child) .memos-attachments-image img { width: 100%; max-height: 720px; border-radius: 10px; object-fit: contain;}
/* 多张图片时的网格布局 */.memos-attachments:has(.memos-attachments-image:not(:only-child)) { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px;}
.memos-attachments-image { overflow: hidden; border-radius: 8px;}
.memos-attachments-image img{ margin: 0 0;}
.memos-attachments:has(.memos-attachments-image:not(:only-child)) img { width: 100%; height: 150px; object-fit: cover; transition: transform 0.3s ease;}
.memos-attachments:has(.memos-attachments-image:not(:only-child)) img:hover { transform: scale(1.05);}
/* 底部信息 */.memos-item-bottom { display: flex; align-items: center; justify-content: space-between; border-top: 1px solid var(--tertiary); padding-top: 15px; font-size: 13px; color: #8a8f99;}
.memos-time { flex-grow: 1;}
.memos-location { flex-grow: 38;}
/* 响应式设计 */@media (max-width: 480px) { .memos-item { border-radius: 12px; }
.memos-item-body { padding: 15px; }
.memos-attachments:has(.memos-attachments-image:not(:only-child)) { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
}
/* 无限滚动加载动画 */.infinite-loader { display: flex; align-items: center; justify-content: center; padding: 20px; color: #888; gap: 10px; opacity: 0; animation: fadeIn 0.3s ease-out forwards;}
@keyframes fadeIn { to { opacity: 1; }}
.loader-spinner { width: 20px; height: 20px; border: 3px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: #3498db; animation: spin 1s linear infinite;}
@keyframes spin { to { transform: rotate(360deg); }}
/* 尽头提示样式 */.no-more-items { text-align: center; padding: 30px 20px; color: #777; font-size: 0.9em; margin-top: 20px; border-top: 1px dashed var(--tertiary);}
.no-more-items p { margin: 10px 0; font-size: 1.1em; font-weight: 500; color: var(--primary);}
/* 小型设备上的尽头提示 */@media (max-width: 600px) { .no-more-items { padding: 20px 10px; font-size: 0.85em; }}
.artalk-comment-box { margin-top: 1rem;
/* 动画设置 */ animation: commentBoxAppear 0.3s ease-out forwards; opacity: 0; transform-origin: top center;}
@keyframes commentBoxAppear { 0% { opacity: 0; transform: translateY(-10px) scale(0.98); } 100% { opacity: 1; transform: translateY(0) scale(1); }}
/* 点赞按钮样式 */.memos-like-btn { display: flex; align-items: center; background: none; border: none; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: all 0.2s; margin-left: 10px;}
.memos-like-btn:hover{ background: var(--code-bg);}
.memos-like-btn:active{ background: var(--border);}
.memos-like-btn svg { width: 16px; height: 16px; margin-right: 4px; fill: #666;}
.memos-like-btn.liked svg { fill: #ff3b30;}
.memos-like-btn.liked .like-count { color: #ff3b30;}
.memos-comment-btn { display: flex; align-items: center; background: none; border: none; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: all 0.2s; margin-left: 10px;}
.memos-comment-btn svg { width: 16px; height: 16px; margin-right: 4px; fill: #666;}
.memos-comment-btn:hover{ background: var(--code-bg);}
.memos-comment-btn:active{ background: var(--border);}
/* 置顶图标样式 */.memos-pinned-icon { position: absolute; top: 15px; right: 15px; padding: 4px 10px; font-size: 12px; font-weight: 600; z-index: 1; display: flex; align-items: center; gap: 4px;}
/* 引用关系样式 */.memos-relations { margin-top: 15px; margin-bottom: 15px; padding: 12px 15px; border: 1px solid var(--border); border-radius: 12px; background-color: rgba(0, 0, 0, 0.02); font-size: 14px; line-height: 1.5; position: relative;}
.memos-relations::before { content: "🔗"; position: absolute; left: 0px; top: -8px; padding: 2px; font-size: 12px;}
.memos-relation { color: var(--secondary); padding-left: 10px; position: relative; margin-bottom: 8px;}
.memos-relation:last-child { margin-bottom: 0;}
.memos-relation::before { content: "›"; position: absolute; left: 0; color: var(--primary); font-weight: bold;}
/* 弹窗样式 */.memo-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; display: flex; justify-content: center; align-items: center;}
.memo-modal-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 1);}
.memo-modal-content { position: relative; width: 90%; max-width: 700px; max-height: 85vh; background-color: var(--entry); border-radius: 20px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); overflow: hidden; z-index: 1001; animation: modalSlideIn 0.3s ease-out;}
.memo-modal-close { position: absolute; top: 15px; right: 15px; width: 36px; height: 36px; background: var(--code-bg); border: none; border-radius: 50%; color: var(--primary); font-size: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 1002; transition: all 0.2s ease;}
.memo-modal-close:hover { background: var(--border); transform: scale(1.1);}
/* 防止背景滚动 */body.no-scroll { overflow: hidden; position: fixed; width: 100%; height: 100%;}
.memo-modal-body { overflow-y: auto; max-height: calc(85vh - 60px);}
.memo-modal-body .memos-item { margin: 30px; box-shadow: none;}
.memo-modal-body .memos-item:hover { box-shadow: none;}
.memo-modal-body .memos-item-body { padding: 0px;}
.memo-modal-body .memos-content { font-size: 17px; line-height: 1.6;}
.memo-modal.closing .memo-modal-content { animation: modalSlideOut 0.3s ease-out forwards;}
/* 加载界面 */.memo-modal-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 300px; padding: 60px 30px; transition: opacity 0.3s ease;}
.memo-loading-text { color: var(--secondary); font-size: 16px; font-weight: 500;}
/* 弹窗动画 */@keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; }}
@keyframes modalFadeOut { from { opacity: 1; } to { opacity: 0; }}
@keyframes modalSlideIn { from { opacity: 0; transform: translateY(30px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); }}
@keyframes modalSlideOut { from { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 0; transform: translateY(30px) scale(0.95); }}
/* 引用关系分节样式 */.memos-relation-section { margin-bottom: 15px;}
.memos-relation-section:last-child { margin-bottom: 0;}
.memos-relation-title { font-size: 13px; font-weight: 600; color: var(--primary); margin-bottom: 8px; padding-left: 5px; border-left: 3px solid var(--primary);}
.memos-relation-item { color: var(--secondary); padding: 8px 12px; background: var(--code-bg); border-radius: 8px; margin-bottom: 5px; font-size: 14px; line-height: 1.5; cursor: pointer; transition: all 0.2s ease; border: 1px solid transparent;}
.memos-relation-item:hover { background: var(--border); border-color: var(--primary); transform: translateX(3px);}
.memos-relation-item:last-child { margin-bottom: 0;}
/* 弹窗中的引用项样式 */.memo-modal-body .memos-relation-item { font-size: 15px; padding: 10px 15px;}
/* 加载失败 */.memo-loading-error { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 300px;}
.error-text { color: var(--primary); font-size: 18px; margin-bottom: 20px;}
/* 移动端优化 */@media (max-width: 768px) { .memo-modal-content { width: 100%; height: 100%; max-width: 100%; max-height: 100%; border-radius: 0; }
.memo-modal-body { max-height: 100vh; border-radius: 0; }
.memo-modal-body .memos-item { margin: 10px; box-shadow: none; }
.memo-modal-close { top: 12px; right: 12px; font-size: 28px; background: rgba(0, 0, 0, 0.2); backdrop-filter: blur(10px); color: white; }
.memo-modal-close:hover { background: rgba(0, 0, 0, 0.3); }
.memo-modal-body .memos-item-body { padding: 16px; }
.memo-modal-body .memos-content { font-size: 16px; }
.memo-modal-loading { padding: 40px 20px; min-height: calc(100vh - 80px); }}
/* 平板设备优化 */@media (min-width: 769px) and (max-width: 1024px) { .memo-modal-content { width: 95%; max-height: 90vh; }
.memo-modal-body { max-height: calc(90vh - 60px); }}
/* 滚动条样式 */.memo-modal-body::-webkit-scrollbar { width: 8px;}
.memo-modal-body::-webkit-scrollbar-track { background: var(--code-bg);}
.memo-modal-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px;}
.memo-modal-body::-webkit-scrollbar-thumb:hover { background: var(--tertiary);}参考
Footnotes
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
PaperMod主题魔改:添加说说页面
https://blog.dotuoodo.top/posts/memos-hugo/最后更新于 2026-02-21
部分内容可能已过时











