基于Memos实现说说和清单功能

Memos官网:memos

前言

一般来讲,Memos通过Docker来部署,所以想要部署使用Memos的话,最好有服务器

如果你没有服务器的话,可以尝试使用小N同学和杜老师维护的公益项目:memos纯公益代部署服务

简介

Memos是一个轻量级的自托管备忘录中心。开源和永久免费。可以说是支持 Docker 自部署的 flomo ,而且有 API 调取数据和发 Memos 。在短短两年的时间内,Memos就在GitHub上获得了20k的星星。

效果展示

说说页面展示:空间动态

清单页面展示:清单列表

本教程参考了Leonus木木木木木关于Memos的文章,感兴趣的同学可以去看一看。

部署

下面是在服务器上使用docker进行的Memos部署方法。

第一步

新建一个文件夹memos,作为项目的根文件夹。

第二步

在根文件夹下新建文件docker-compose.yml。 推荐使用docker-compose.yml方式进行部署,方便制定数据储存位置及更新版本,其中使用${PWD}指定路径为当前文件夹。

version: "3.0"
services:
memos:
  image: neosmemo/memos:latest
  container_name: memos
  volumes:
    - ${PWD}/.memos/:/var/opt/memos
  ports:
    - 5230:5230

第三步

以宝塔面板为例:

  1. 安装docker

宝塔面板好像已经自带了docker环境(不确定忘记了)

  1. 安装docker compose

pip install docker compose

验证docker compose是否安装成功

docker compose version

  1. 构建并启动容器

docker compose up -d

  1. 由于Memos是一个新的项目,还处于快速迭代的时期,所以我们可能会经常更新版本

docker compose down && docker image rm neosmemo/memos:latest && docker compose up -d

我们的数据都会存放在.memos文件夹中,所以建议定时备份目录下的.memos文件夹。

  1. docker compose的用法\命令:

用法:docker compose [OPTIONS] COMMAND

使用Docker定义并运行多容器应用程序。

OPTIONS:

命令注释
–ansi string控制何时打印ANSI控制字符(”never”|”always”|”auto”)(默认:”auto”)
–compatibility在向后兼容模式下运行compose
–dry-rundry run模式下执行命令
–env-file stringArray指定备用环境文件
-f, –file stringArray编写配置文件
–parallel int控制最大并行度,-1表示无限制(默认值-1)
–profile stringArray指定要启用的配置文件
–progress string设置进度输出类型(auto、tty、plain、quiet)(默认为“auto”)
–project-directory string指定备用工作目录(默认值:首次指定的Compose文件的路径)
-p, –project-name string项目名称

COMMAND:

命令注释
build构建或重建服务
config分析、解析并呈现规范格式的compose文件
cp在服务容器和本地文件系统之间复制文件/文件夹
create为服务创建容器
down停止并移除容器、网络
events从容器接收实时事件
exec在运行的容器中执行命令
images列出创建的容器使用的图像
kill强制停止服务容器
logs查看容器的输出
ls列出正在运行的撰写项目
pause暂停服务
port打印端口绑定的公共端口
ps列出容器
pull拉取服务图像
push推送服务图片
restart重新启动服务容器
rm删除已停止的服务容器
run对服务运行一次性命令
start启动服务
stop停止服务
top显示正在运行的进程
unpause无偿服务
up创建和启动容器
version显示Docker Compose版本信息
wait阻塞,直到第一个服务容器停止

有关命令的详细信息,请运行docker compose COMMAND--help

第四步

添加反向代理:

操作如下:网站—>添加站点(之后的操作建立在自己创建的站点上面)—>设置—>反向代理—>添加反向代理

其中目标url填写的是http://自己的服务器ip:5230

Memos api

Memos api的格式如下:https://memos地址/api/v1/memo?creatorId=用户ID&tag=标签名&limit=限制数量

memos地址

就是自己memos服务的域名地址,比如我的就是:memos.zhaozeyu.top

用户ID

在自己的memos主页点击头像,点击RSS,可以获取到RSS地址如:https://memos.zhaozeyu.top/u/1/rss.xml,其中1就是自己的用户ID。

标签名

自己在memos服务中创建的标签名,用来区分不同类型的备忘录,你可以自定义为“说说”、“清单”、“相册”等。

限制数量

一个数字,用来限制要显示的数量。

完整memos api示例:https://memos.zhaozeyu.top/api/v1/memo?creatorId=1&tag=说说&limit=30

说说功能

在子比主题的网站中新建页面,选择自定义HTML区块,然后嵌入一下代码(记得将Memos api改成自己的):​

<style>
  #memos-box {
    width: 100%;
    position: relative;
  }

  .memos-talk {
    padding: 10px;
    border: 1px solid lightgray;
    border-radius: 20px;
    position: absolute;
    transition: all 0.2s linear;
  }

  .memos-talk:hover {
    box-shadow: 0 0 12px rgba(0, 0, 0, 0.24);
  }

  .memos-meta {
    display: flex;
    align-items: center;
    height: 60px;
  }

  .memos-avatar {
    width: 60px!important;
    height: 60px!important;
    margin: 0 !important;
    border-radius: 15px;
  }

  .memos-info {
    margin-left: 10px;
    display: flex;
    flex-direction: column;
  }

  span.memos-nick {
    color: var(--theme-color);
  }

  span.memos-date {
    opacity: 0.6;
    font-size: 1.4rem;
  }

  /* 中间内容 */
  .memos-content {
    margin-top: 10px;
  }

  .memos-images {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
  }

  .memos-img {
    margin: 2px !important;
    object-fit: cover;
    aspect-ratio: 1 / 1;
  }

  .memos-files {
    display: flex;
    flex-wrap: wrap;
  }

  .memos-file {
    margin: 5px;
    padding: 2px 5px;
    background-color: lightgray;
    border: 1px lightgrey solid;
    border-radius: 8px;
    font-size: 1.2rem;
  }

  .memos-file:hover {
    background-color: transparent;
    transition: all 1s linear;
  }

  /* 底部  */
  .memos-tag {
    margin: 0 5px;
    opacity: 0.6;
    font-size: 1.4rem;
  }
</style>
<div id="memos-box"></div>
<script>
  const url = "https://memos.zhaozeyu.top";
  fetch(`${url}/api/v1/memo?creatorId=1&tag=说说&limit=30`)
      .then((res) => res.json())
      .then((res) => {
        let items = [];
        let html = "";
        res.forEach((item) => {
          items.push(format(item));
        });
        items.forEach((item) => {
          let imagesHtml = "";
          let tagsHtml = "";
          let resourcesHtml = "";
          item.images.forEach((t, index) => {
            imagesHtml += `<img class="memos-img" src="${t}" alt="${index}"/>`;
          });
          item.resources.forEach((t) => {
            resourcesHtml += `<div class="memos-file"><a href="${t.url}" target="_blank">${t.filename}</a></div>`;
          });
          item.tags.forEach((t) => {
            tagsHtml += `<span class="memos-tag">#${t}</span>`;
          });
          html += `<div class="memos-talk">
        <div class="memos-meta">
          <img
            class="memos-avatar"
            src="https://imagecloud.zhaozeyu.top/2023/10/07/652166bad3c71.webp"
            alt="avatar"
          />
          <div class="memos-info">
            <span class="memos-nick">是zzy呀</span>
            <span class="memos-date">${item.createDate}</span>
          </div>
        </div>
        <div class="memos-content">
          <div style="white-space: pre-wrap">${item.content}</div>
          <div class="memos-images">${imagesHtml}</div>
          <div class="memos-files">${resourcesHtml}</div>
        </div>
        <div class="memos-bottom">
          <div>${tagsHtml}</div>
        </div>
      </div>`;
        });
        document.getElementById("memos-box").innerHTML = html;
        setPositions();
        setPositions();
      });

  /**
   * 格式化
   * @param {{createdTs: string, content: string, resourceList: [{filename: string, externalLink: string}]}} item
   * @returns {{images: (string[]|*[]), resources: *[], content: string, createDate: string, tags: (string[]|*[])}}
   */
  function format(item) {
    // 创建日期
    let createDate = getTime(new Date(item.createdTs * 1000).toString());
    // 标签数组
    let tagMatches = item.content.match(/\{(.*?)}/g);
    let tags = tagMatches
        ? tagMatches.map((t) => t.replace(/^\{(.*)}$/, "$1"))
        : [];
    // 图片数组
    let imageMatches = item.content.match(/!\[.*?]\(.*?\)/g);
    let images = imageMatches
        ? imageMatches.map((t) => t.replace(/!\[.*?]\((.*?)\)/, "$1"))
        : [];
    // 去除 标签、图片、自定义tag
    let content = item.content
        .replace(/#(.*?)\s/g, "")
        .replace(/!\[(.*?)]\((.*?)\)/g, "")
        .replace(/\{(.*?)}/g, "")
        .replace(/^\s+|\s+$/g, "")
        .replace(
            /\[(.*?)]\((.*?)\)/g,
            (match, alt, url) => `<a href=${url}>${alt}</a>`
        );
    // 资源数组
    let resources = [];
    if (item.resourceList.length > 0) {
      item.resourceList.forEach((t) => {
        resources.push({
          filename: t.filename,
          url: t.externalLink
              ? t.externalLink
              : `${url}/o/r/${t.id}/${t.publicId}/${t.filename}`,
        });
      });
    }

    return {
      createDate,
      content,
      tags,
      images,
      resources,
    };
  }

  // 时间转化
  function getTime(time) {
    let d = new Date(time);
    let ls = [
      d.getFullYear(),
      d.getMonth() + 1,
      d.getDate(),
      d.getHours(),
      d.getMinutes(),
      d.getSeconds(),
    ];

    for (let i = 0; i < ls.length; i++) {
      ls[i] = ls[i] <= 9 ? "0" + ls[i] : ls[i] + "";
    }

    if (Number(ls[0]) === new Date().getFullYear()) {
      return ls[1] + "月" + ls[2] + "日 " + ls[3] + ":" + ls[4];
    } else {
      return (
          ls[0] + "年" + ls[1] + "月" + ls[2] + "日 " + ls[3] + ":" + ls[4]
      );
    }
  }

  // 瀑布流布局
  // 计算一共有多少列,以及每一列之间的间隙
  function cal() {
    let boxElement = document.getElementById("memos-box");
    let boxWidth = boxElement.clientWidth; // box容器的宽度
    // 计算列的数量
    let columns;
    if (boxWidth > 1536) {
      columns = 5;
    } else if (boxWidth > 1280) {
      columns = 4;
    } else if (boxWidth > 1024) {
      columns = 3;
    } else if (boxWidth > 768) {
      columns = 2;
    } else if (boxWidth > 512) {
      columns = 2;
    } else if (boxWidth > 375) {
      columns = 1;
    } else {
      columns = 1;
    }
    // 计算间隙个数,两边不设置间隙
    let spaceNum = columns - 1; // 间隙数量
    // 定义每一个间隙的宽度
    let spaceWidth = 10;
    // 计算 talk 的宽度
    let talkWidth = Math.floor((boxWidth - spaceNum * spaceWidth) / columns);
    return {columns, talkWidth};
  }

  // 设置每个 talk 的位置
  function setPositions() {
    let boxElement = document.getElementById("memos-box");
    let talksElement = document.querySelectorAll(".memos-talk");
    // 得到列数和间隙空间
    let info = cal();
    // 该数组的长度位列数,每一项表述该列的下一个图片的纵坐标
    let nextTops = new Array(info.columns);
    nextTops.fill(10); // 初始化,每一项都为0
    for (let i = 0; i < talksElement.length; i++) {
      let talk = talksElement[i];
      // 找到 nextTops最小值作为当前图片的纵坐标
      let minTop = Math.min.apply(null, nextTops);
      talk.style.width = `${info.talkWidth}px`;
      talk.style.top = `${minTop}px`;
      // 检测 talk 中是否有 images 元素
      let imagesElement = talk
          .querySelector(".memos-content")
          .querySelector(".memos-images");
      let imgWidth = (imagesElement.clientWidth - 12) / 3;
      if (imagesElement.children.length === 1) {
        imagesElement.children[0].style.aspectRatio = "auto";
        imagesElement.children[0].style.height = `${imgWidth}px`;
        imagesElement.children[0].style.width = "auto";
      } else {
        for (let j = 0; j < imagesElement.children.length; j++) {
          imagesElement.children[j].style.width = `${imgWidth}px`;
        }
      }
      // 更新数组这一项的的下一个top值
      // 得到使用的是第几列的top值
      let index = nextTops.indexOf(minTop);
      nextTops[index] += talk.clientHeight + 10;
      // 横坐标
      let left = index * (info.talkWidth + 10);
      talk.style.left = `${left}px`;
    }
    // 求最大值
    let maxTops = Math.max.apply(null, nextTops);
    // 这只容器的高度
    boxElement.style.height = `${maxTops}px`;
  }

  // 检测窗口尺寸变化
  let timeId = null;
  window.onresize = function () {
    if (timeId) {
      clearTimeout(timeId);
    }
    timeId = setTimeout(setPositions, 250);
  };
</script>

发表说说的格式如下:

#说说 {说说}{测试}

memos说说功能测试

超链接测试

图片九宫格测试

文件上传测试

[Memos](https://memos.zhaozeyu.top)

![夜晚雨天街道女孩.jpg](https://imagecloud.zhaozeyu.top/2023/09/03/64f48c61901a8.jpg)

![蓝眼睛的猫女孩.jpg](https://imagecloud.zhaozeyu.top/2023/09/03/64f48b1f10e7a.jpg)

![微信图片_20230919221351.webp](https://imagecloud.zhaozeyu.top/2023/09/19/6509adfe72008.webp)

![微信图片_20230919221354.webp](https://imagecloud.zhaozeyu.top/2023/09/19/6509adff97992.webp)

![微信图片_20230919221352.webp](https://imagecloud.zhaozeyu.top/2023/09/19/6509ae0004d60.webp)

![avatar.webp](https://imagecloud.zhaozeyu.top/2023/10/07/652166bb173ae.webp)

![git.webp](https://imagecloud.zhaozeyu.top/2023/10/07/652166bad3c71.webp)

![小N同学.webp](https://imagecloud.zhaozeyu.top/2023/10/07/652166badb01c.webp)

![森鹿语.webp](https://imagecloud.zhaozeyu.top/2023/10/07/652166bae2cc7.webp)

选择标签 —> 大括号包裹自定义标签 —> 回车后开始写正文,链接和图片都是用markdown书写格式。

目前实现了文字、超链、图片、文件的展示。

清单功能

操作同上面的说说功能,嵌入代码如下:​

发表清单的格式如下:

#清单 {想去的地方}

- [x] 甘坑客家小镇

- [x] 大芬油画村

- [x] 文博宫

- [x] 华为坂田研发总部

- [ ] 大小梅沙海滨公园

- [ ] 深圳湾公园

- [ ] 深圳人才公园

- [ ] 深圳天文台栈道

- [ ] 深圳博物馆

- [ ] 南山博物馆

- [ ] 梧桐山

- [ ] 长白山

- [ ] 黑龙江冰雪世界

选择标签 —> 大括号写清单的标题 —> 回车开始写清单,一样使用markdown书写格式。

© 版权声明
WWW.ANXKJ.TOP
喜欢就支持一下吧
点赞11 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容