Typecho整合Memos打造哔哔空间
前些天简单整合Memos形成了自己的哔哔空间,有些朋友说能不能分享一下,正好今天有空,就水一篇文章好了
- 支持Memos最新版本,低版本不确定是否能正常使用,Memos的api改动很频繁,有可能上个版本有这个api,下个版本就废弃了
- 会自己修改处理的话其他非Typecho平台也能使用
- 全为原生方法,无需jquery
- 支持Memos图片和文件引用以及位置信息
- 使用了marked.js进行markdown语法解析
- 引入了Fancybox图片灯箱进行图片展示
- 支持多人/单人Memos信息展示,会自动获取昵称
- 哦,还有一点就是仅展示公开发布的Memos
首先就是在当前主题根目录创建一个memos.php,将以下代码复制进去(是在Typecho 1.3的新主题classic-22基础上整合的,所以包含了一些classic-22的类和Typecho 1.3的方法,如<?php postMeta($this, 'page'); ?>,这个方法在1.2版本是没有的
,需自行调整修改)
放进去后需要修改代码中的:
var MEMO_DOMAIN = 'https://memos.xxxx.com';//你的Memos域名
var pageSize = 10;//一次加载几条
var avatarUrl = 'https://xxxx.com/xxxx.jpg';//Memos头像
var userName = '';//这个是Memos的用户ID,第一个就是1,以此类推,为空就获取全部
var autoAvatar = false;//这个是使用的dicebear的api,如果是多人的memos,可以将该项改为true,会自动生成每个用户都不同的唯一头像,开启该项后填写的avatarUrl项无效
然后下载资源文件压缩包,将压缩包内整个目录放到当前主题根目录即可(戳这里下载)
需要注意:已经注释掉了Emaction互动组件,若有需要请看文章末尾
memos.php文件内容如下:
<?php
/**
* Memos
*
* @package custom
*/
?>
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
<?php $this->need('header.php'); ?>
<link rel='stylesheet' href='<?php $this->options->themeUrl('memos/fancybox.min.css');?>'>
<link rel='stylesheet' href='<?php $this->options->themeUrl('memos/memos.css');?>'>
<main class="container">
<div class="container-thin">
<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
<?php postMeta($this, 'page'); ?>
<div class="entry-content fmt" itemprop="articleBody">
<div id="memo-container"></div>
<button id="memosLoadMore" class="memos-load-more" style="margin-bottom:10px;">加载更多</button>
<script src="<?php $this->options->themeUrl('memos/fancybox.umd.min.js');?>"></script>
<script src="<?php $this->options->themeUrl('memos/marked.min.js');?>"></script>
<script src="<?php $this->options->themeUrl('memos/purify.min.js');?>"></script>
<script type="module" src="<?php $this->options->themeUrl('memos/emaction.js');?>" defer></script>
</div>
</article>
</div>
</main>
<script>
document.addEventListener('DOMContentLoaded', function() {
Fancybox.bind('[data-fancybox]', {
Toolbar: {
display: {
left: ["infobar"],
middle: [
"zoomIn",
"zoomOut",
"toggle1to1",
"rotateCCW",
"rotateCW",
"flipX",
"flipY",
],
right: ["slideshow", "thumbs", "close"],
},
},
});
});
var nextPageToken = '';
var MEMO_DOMAIN = 'https://memos.xxxx.com';//你的Memos域名
var pageSize = 10;//一次加载几条
var avatarUrl = 'https://xxxx.com/xxxx.jpg';//Memos头像
var userName = '';//这个是Memos的用户ID,第一个就是1,以此类推,为空就获取全部
var autoAvatar = false;//这个是使用的dicebear的api,如果是多人的memos,可以将该项改为true,会自动生成每个用户都不同的唯一头像,开启该项后填写的avatarUrl项无效
var isLoading = false;
var userCache = {};
var pendingUserRequests = {};
async function fetchUserNickname(creator) {
try {
const response = await fetch(`${MEMO_DOMAIN}/api/v1/${creator}`);
if (!response.ok) throw new Error('用户信息获取失败');
const data = await response.json();
return data.nickname || '未知用户';
} catch (error) {
console.error('获取用户信息失败:', error);
return '用户加载失败';
}
}
function updateUserDisplay(creator, nickname) {
document.querySelectorAll(`.author-name[data-creator="${creator}"]`).forEach(el => {
el.textContent = nickname;
});
}
function getResourceUrl(resource) {
if (resource.externalLink) return resource.externalLink;
return `${MEMO_DOMAIN}/file/${encodeURIComponent(resource.name)}/${encodeURIComponent(resource.filename)}`;
}
function renderResources(resources,name) {
return resources.map(res => {
const url = getResourceUrl(res);
if (res.type.startsWith('image/')) {
return `<div class="resource-item">
<img src="${url}" alt="${res.filename}" loading="lazy" data-fancybox="${name}">
</div>`;
}
return `<div class="resource-item">
<a href="${url}" target="_blank" rel="noopener">📄 ${res.filename}</a>
</div>`;
}).join('');
}
function renderMemoCard(memo) {
const container = document.createElement('div');
container.className = 'memo-item';
const creator = memo.creator;
const memoId = memo.name;
const cleanContent = DOMPurify.sanitize(marked.parse(memo.content));
container.innerHTML = `
<div class="avatar-container">
<div class="avatar">
${autoAvatar === '' || autoAvatar === true ?
`<img src="https://api.dicebear.com/7.x/miniavs/svg?seed=${creator.split('/').pop()}">` :
`<img src="${avatarUrl}">`}
</div>
</div>
<div class="memo-card">
<div class="memo-header">
<span class="author-name" data-creator="${creator}">
${userCache[creator] || '加载中...'}
</span>
<span class="memo-time">${new Date(memo.displayTime).toLocaleString('zh-CN')}</span>
</div>
<div class="memo-body">
${cleanContent}
${memo.resources?.length ? `<div class="memo-resources">${renderResources(memo.resources,memo.name)}</div>` : ''}
${renderLocation(memo.location)}
</div>
<!--<div class="emoji-reaction">
<emoji-reaction theme="light" endpoint="Emaction Api地址" reacttargetid="${memoId}" ></emoji-reaction>
</div>-->
</div>
`;
if (!userCache[creator] && !pendingUserRequests[creator]) {
pendingUserRequests[creator] = true;
fetchUserNickname(creator).then(nickname => {
userCache[creator] = nickname;
pendingUserRequests[creator] = false;
updateUserDisplay(creator, nickname);
});
}
return container;
}
function renderLocation(location) {
if (!location || typeof location !== 'object') return '';
const { placeholder, latitude, longitude } = location;
if (typeof latitude === 'number' && typeof longitude === 'number') {
return `
<div class="memo-location">
<div class="location-icon"></div>
<div class="location-text">
<span>${placeholder || '当前位置'}</span>
</div>
</div>
`;
}
if (placeholder) {
return `
<div class="memo-location placeholder">
<div class="location-icon">❔</div>
<div class="location-text">
${placeholder}
</div>
</div>
`;
}
return '';
}
async function loadMemos() {
if (isLoading || nextPageToken === null) return;
const loadBtn = document.getElementById('memosLoadMore');
try {
isLoading = true;
loadBtn.disabled = true;
loadBtn.textContent = '加载中...';
const params = new URLSearchParams({ pageSize });
if (nextPageToken) params.set('pageToken', nextPageToken);
const response = await fetch(`${MEMO_DOMAIN}/api/v1/memos?${params}`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP错误 ${response.status}`);
}
const data = await response.json();
nextPageToken = data.nextPageToken || null;
const container = document.getElementById('memo-container');
data.memos.forEach(memo => container.appendChild(renderMemoCard(memo)));
loadBtn.textContent = nextPageToken ? '加载更多' : '没有更多了';
} catch (error) {
console.error('加载失败:', error);
loadBtn.textContent = `加载失败: ${error.message}`;
nextPageToken = null;
} finally {
isLoading = false;
loadBtn.disabled = !nextPageToken;
}
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('memosLoadMore').addEventListener('click', loadMemos);
loadMemos();
});
</script>
<?php $this->need('footer.php'); ?>
若有需要使用Emaction互动组件,可把
<!--<div class="emoji-reaction">
<emoji-reaction theme="light" endpoint="Emaction Api地址" reacttargetid="${memoId}" ></emoji-reaction>
</div>-->
上面代码中的这一段注释给去掉,然后按照如下进行操作:
1、先创建数据库
在主题根目录下functions.php中添加如下代码:
function create_db(){
$db = \Typecho\Db::get();
$emaction_table = $db->getPrefix() . 'emaction_data';
$adapter = $db->getAdapterName();
if ("Pdo_SQLite" === $adapter || "SQLite" === $adapter) {
//Emaction表检测初始化
$db->query(" CREATE TABLE IF NOT EXISTS " . $emaction_table . " (
id INTEGER PRIMARY KEY,
target_id TEXT,
reaction_name TEXT,
diff TEXT)");
}
if ("pgsql" === $adapter || "Pdo_Pgsql" === $adapter) {
//Emaction表检测初始化
$db->query(" CREATE TABLE IF NOT EXISTS " . $emaction_table . " (
id SERIAL PRIMARY KEY,
target_id TEXT,
reaction_name TEXT,
diff TEXT)");
}
if ("Pdo_Mysql" === $adapter || "Mysql" === $adapter || "Mysqli" === $adapter) {
$dbConfig = null;
if (class_exists('\Typecho\Db')) {
$dbConfig = $db->getConfig($db::READ);
} else {
$dbConfig = $db->getConfig()[0];
}
$charset = $dbConfig->charset;
//Emaction表检测初始化
$db->query(" CREATE TABLE IF NOT EXISTS " . $emaction_table . " (
`id` int(11) NOT NULL AUTO_INCREMENT,
`target_id` varchar(255) NOT NULL,
`reaction_name` varchar(255) NOT NULL,
`diff` int(11) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) DEFAULT CHARSET=$charset AUTO_INCREMENT=1");
}
}
create_db();
然后刷新一下网站首页或其他前台页面以确保创建数据库。
刷新后可去数据库中查看是否已创建新表
2、在functions.php中加入如下代码:
function themeInit($self){
if (strpos($_SERVER['REQUEST_URI'], 'getEmaction') !== false) {
$self->response->setStatus(200);
$self->setThemeFile("memos/getEmaction.php");
}
}
若报错可能themeInit方法已存在,则在原有的themeInit方法中添加如上themeInit方法中的代码即可。
3、最后只需要将刚才去掉注释的那一段代码中endpoint="Emaction Api地址"
Emaction Api地址改成https://你的域名/index.php/getEmaction
即可
如此便大功告成了,还是蛮简单的,不过也有可以优化的地方,若有问题可以在评论区评论留言,看到会回复 。
memos 还不错。加了评论后
不过没有cmx好用感觉。
目前只用过memos,你说的cmx我有空看看