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: always
cascade:
- 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);
}

参考#

Hugo PaperMod 安装与配置 | Mortal

Footnotes#

  1. 需要使用FastAPI与MySQL对接,以后再写

文章分享

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

PaperMod主题魔改:添加说说页面
https://blog.dotuoodo.top/posts/memos-hugo/
作者
DOTUOODO
发布于
2025-08-06
许可协议
CC BY-NC-SA 4.0

评论

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

文章目录