前些天简单整合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即可

如此便大功告成了,还是蛮简单的,不过也有可以优化的地方,若有问题可以在评论区评论留言,看到会回复

标签:typecho, memos

4 条评论

  1. 谢谢大佬,一顿乱改之后终于加到我用的主题上了,真的谢谢大佬

    1. 客气了,能帮到你我很高兴

  2. memos 还不错。加了评论后
    不过没有cmx好用感觉。

    1. 目前只用过memos,你说的cmx我有空看看

你的评论