Memos 是一款轻量级的开源笔记应用,不仅部署方便,还支持多端 APP 发布,非常适合用来当作个人的‘朋友圈’记录日常、发发牢骚。最近刚重新搭建了 Typecho 博客,便萌生了将 Memos 嵌入到博客页面中的想法。起初在网上找到了一些现成的插件,但由于年久失修无法正常运行。最后,我决定借助 AI 自己动手实现这个功能。目前我的博客使用的是 Joe 主题,完美适配了嵌入效果。具体效果大家可以点击博客导航栏的【说说】跳转预览
具体代码如下:
在主题文件夹新建memos.php,复制下面代码粘贴:
<?php
/**
* memos 朋友圈模板 - 优化版
* 适配:Typecho + Joe主题
* 功能:真正分页加载、图片灯箱、兼容性处理
**/
?>
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) {
http_response_code(404);
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<?php $this->need('module/head.php'); ?>
<?php if (!empty($this->options->JPrismTheme)) : ?>
<link rel="stylesheet" href="<?= \Joe\theme_url('assets/plugin/prism/themes/' . $this->options->JPrismTheme) ?>">
<?php else : ?>
<link rel="stylesheet" href="<?= \joe\cdn('prism/1.23.0/themes/prism.min.css') ?>">
<?php endif; ?>
<script src="<?= joe\cdn('clipboard.js/2.0.6/clipboard.min.js') ?>"></script>
<script src="<?= joe\theme_url('assets/plugin/prism/prism.min.js') ?>"></script>
<script src="<?= joe\theme_url('assets/js/joe.post_page.js'); ?>"></script>
<style>
.memo-container { margin: 0 auto; background: #fff; min-height: 200px; }
.memo-item { padding: 20px; border-bottom: 1px solid #f2f2f2; animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.memo-content { font-size: 15px; line-height: 1.6; color: #333; white-space: pre-wrap; word-break: break-all; }
.memo-date { font-size: 12px; color: #bbb; margin-top: 12px; }
/* 图片网格布局 */
.memo-res-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: 10px; }
.memo-res-grid img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 4px; border: 1px solid #eee; cursor: pointer; transition: opacity 0.2s; }
.memo-res-grid img:hover { opacity: 0.8; }
/* 只有单张图片时的大图显示 */
.memo-res-grid:has(img:only-child) { display: block; }
.memo-res-grid:has(img:only-child) img { width: auto; max-width: 80%; max-height: 400px; aspect-ratio: auto; }
/* 加载按钮 */
.load-more-wrapper { padding: 40px 20px; text-align: center; }
#loadMoreBtn {
background: #f8f8f8; border: 1px solid #ddd; color: #576b95;
padding: 10px 40px; border-radius: 20px; font-size: 14px;
cursor: pointer; transition: all 0.2s; outline: none;
}
#loadMoreBtn:disabled { color: #ccc; cursor: not-allowed; }
#loadMoreBtn:not(:disabled):hover { background: #f0f0f0; border-color: #ccc; }
/* 灯箱 */
#lightbox { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: 0.3s; cursor: zoom-out; }
#lightbox.active { opacity: 1; visibility: visible; }
#lightbox img { max-width: 95%; max-height: 95%; transform: scale(0.9); transition: 0.3s; }
#lightbox.active img { transform: scale(1); }
</style>
</head>
<body>
<div id="Joe">
<?php $this->need('module/header.php'); ?>
<div class="joe_container">
<div class="joe_main">
<div class="joe_detail" data-cid="<?php echo $this->cid ?>">
<div class="memo-container" id="memoList"></div>
<div class="load-more-wrapper">
<div id="initLoading" style="color: #999; font-size: 14px; margin-bottom: 10px;">正在加载动态...</div>
<button id="loadMoreBtn" style="display: none;">加载更多</button>
</div>
<div id="lightbox"><img src=""></div>
<script>
const CONFIG = {
apiUrl: 'https://你的memos地址/api/v1/memos',
token: 'memos后台token',
step: 10 // 每次获取 10 条
};
let nextPageToken = '';
let isFinished = false;
const memoList = document.getElementById('memoList');
const loadMoreBtn = document.getElementById('loadMoreBtn');
const initLoading = document.getElementById('initLoading');
const lightbox = document.getElementById('lightbox');
const lightboxImg = lightbox.querySelector('img');
// 获取并渲染数据
async function fetchMemos() {
if (isFinished) return;
loadMoreBtn.disabled = true;
loadMoreBtn.innerText = "加载中...";
let url = `${CONFIG.apiUrl}?pageSize=${CONFIG.step}`;
if (nextPageToken) url += `&pageToken=${nextPageToken}`;
try {
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${CONFIG.token}` }
});
const data = await response.json();
const memos = data.memos || (Array.isArray(data) ? data : []);
nextPageToken = data.nextPageToken || '';
if (memos.length > 0) {
renderMemos(memos);
}
initLoading.style.display = 'none';
loadMoreBtn.style.display = 'inline-block';
loadMoreBtn.disabled = false;
loadMoreBtn.innerText = "加载更多";
if (!nextPageToken || memos.length < CONFIG.step) {
isFinished = true;
loadMoreBtn.innerText = "没有更多内容了";
loadMoreBtn.disabled = true;
loadMoreBtn.style.opacity = '0.6';
}
} catch (error) {
console.error('Fetch error:', error);
initLoading.innerText = "加载失败,请检查网络或配置";
}
}
function renderMemos(memos) {
let html = '';
memos.forEach(item => {
// 兼容处理时间
const rawTime = item.createTime || item.createdTs;
const date = new Date(rawTime).toLocaleString('zh-CN', { hour12: false });
// 资源/图片处理
let imgs = '';
const res = item.resources || item.attachments || [];
res.forEach(r => {
const u = r.externalLink || (r.name ? `${CONFIG.apiUrl.replace('/api/v1/memos','')}/file/${r.name}` : '');
if(u) imgs += `<img src="${u}" loading="lazy">`;
});
html += `
<div class="memo-item">
<div class="memo-content">${escapeHtml(item.content)}</div>
${imgs ? `<div class="memo-res-grid">${imgs}</div>` : ''}
<div class="memo-date">${date}</div>
</div>`;
});
memoList.insertAdjacentHTML('beforeend', html);
}
function escapeHtml(t) {
const d = document.createElement('div');
d.textContent = t;
return d.innerHTML.replace(/\n/g, '<br>'); // 保留换行
}
// 事件绑定
loadMoreBtn.addEventListener('click', fetchMemos);
memoList.addEventListener('click', (e) => {
if (e.target.tagName === 'IMG') {
lightboxImg.src = e.target.src;
lightbox.classList.add('active');
document.body.style.overflow = 'hidden';
}
});
lightbox.addEventListener('click', () => {
lightbox.classList.remove('active');
document.body.style.overflow = '';
});
// 初始加载
fetchMemos();
</script>
</div>
<?php $this->need('module/comment.php'); ?>
</div>
<?php $this->need('module/aside.php'); ?>
</div>
<?php $this->need('module/footer.php'); ?>
</div>
</body>
</html>最后新建一个页面,模板选择memos就可以。
6 条评论
想要玩memos,固定在一个版本就不要升级,否则
memos 之前一直使用,整合到博客中,发布什么的都在其中,蛮好。还花了很长时间来适配新的api 新功能。不过最近又不用了,直接用博客原生的 hugo 来搞,都是瞎折腾,哈哈
能网站和App同步,真是个不