完善的轻量说说笔记系统及memos的开源替代品-你的私人朋友圈

前言:近期碰到一个喜欢的一个项目,看到可以类似memos的轻松发布自己的内容…本着喜欢就改造的原则,这个版本的说说笔记就诞生了,使用它,你的数据将由你自由掌控,且可完全免费一键部署,重要的是碎片化的信息记录加自动化就会变的非常高自由度,感兴趣的话不妨来试试
介绍
这是基于Ech0基本框架的二次开发、魔改及完善,类似朋友圈样式风格,支持后台配置修改如背景图、个性签名等,支持api 获取内容、更新操作等,支持对b站视频、网易云音乐、youtube等的解析添加、支持一键复制,一键生成内容图片、支持http post发送内容到平台,支持对接webhook、telegram、企业微信、飞书的一键推送,支持内容热力图组件等个性化组件,它完全属于个人的自定化使用,会加入定制化的一些功能,由于代码已重构,不同步于原版
源码:https://github.com/rcy1314/echo-noise
原版介绍
Ech0 是一款专为轻量级分享而设计的开源自托管平台,支持快速发布与分享你的想法、文字与链接。简单直观的操作界面,轻松管理你的内容,让分享变得更加自由,确保数据完全掌控,随时随地与世界连接。
原版地址:https://github.com/lin-snow/Ech0
整体改版
编辑器部分:
- 自适应高度和拖拽调整功能
- 扩展的工具栏功能
- 完整的响应式支持
- 平滑的过渡动画效果
- 优化的间距和字体设置
- md格式图片即时预览
- 添加定制化的组件
主页部分:
- 调整页面内容自适应高度和宽度
- 添加随机背景图的展示并带有模糊效果
- 增加md 格式下对网易云音乐、哔哩哔哩视频、youtube、qq 音乐的解析渲染
- 调整信息条目的背景ui 及显示尺寸的优化
- 调整ui及加载响应页面的整体显示效果
- 添加朋友圈样式主图banner,并和背景图使用相同
- 所有链接都可通过新标签页打开
- 长内容的折叠展开处理
- 完善的二次编辑及预览保存
- 一键复制及生成内容图片的功能化组件
- 增加标签系统路由及组件
代码部分
- 调整jwk验证为session方式,同时调整token的验证机制
- 调整优化数据库的迁移及连接处理
- 增加不同的路由及调整控制器
- 增加额外的外挂插件文件
- 增加定期清理缓存
特征
- 一键部署无服务器平台-fly.io、zeabur、railway、vercel
- 外部扩展-支持快捷指令及popclip一键发布内容到站点
- 支持推送渠道(webhook、tg、企业微信、飞书)
- 标签系统和图片api 路由
- 支持链接远程数据库PostgreSQL、MySQL的连接支持,默认SQLite
- 个性化前端组件如发布日历-热力图组件,默认不显示,点击日历图标后显示
- 内容二次编辑及一键复制一键生成内容图片
- 数据库文件的一键备份、上传
安装部署
💡 部署完成后访问 ip:1314 即可使用
docker部署
一键部署
docker run -d \
--name Ech0-Noise \
--platform linux/amd64 \
-p 1314:1314 \
-v /opt/data/noise.db:/app/data/noise.db \
noise233/echo-noise
/opt/data/noise.db
是你本地的原有数据库文件,如果没有,可以去掉这个挂载命令,它也会自动创建
说明:如果你是经常使用附件图片发布内容的则可以这样:-v /opt/data:/app/data \
默认用户名:admin
默认用户密码:admin
docker-componse构建部署
在该目录下执行以下命令启动服务(不修改环境变量时默认使用本地数据库.db 文件):
docker-compose up -d
无服务器平台+postgres免费数据库部署
数据库使用 Neon PostgreSQL 云数据库服务,其它也支持
请先前往官网https://console.neon.tech部署好你的基础数据库
以下部署文件已放入根目录下的noise文件夹内
部署成功示例:
Fly.io部署
fly.toml
app = 'ech0-noise' # 修改为你的自定义容器名
primary_region = 'hkg'
[experimental]
auto_rollback = true
[build]
image = 'noise233/echo-noise'
dockerfile = 'Dockerfile'
[env]
CGO_ENABLED = '1'
DB_HOST = 'example.aws.neon.tech' # 修改为数据库的HOST地址
DB_NAME = 'noise' # 修改为数据库的名称
DB_PASSWORD = 'example' # 修改为数据库的密码
DB_PORT = '5432'
DB_SSL_MODE = 'require'
DB_TYPE = 'postgres'
DB_USER = 'noise_owner' # 修改为数据库的用户名
TZ = 'Asia/Shanghai'
[http_service]
internal_port = 1314
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 0
[[services]]
protocol = 'tcp'
internal_port = 1314
[[services.ports]]
port = 1314
[[vm]]
memory = '512mb'
cpu_kind = 'shared'
cpus = 1
部署命令 在准备好 fly.toml 文件后,你可以使用以下命令来部署你的应用到 Fly.io:
初始化 Fly.io 应用(如果尚未初始化)
fly launch
部署应用
fly deploy
确保你已经安装并配置好了 Fly.io 的 CLI 工具,并且已经登录到你的 Fly.io 账号。如果你还没有安装 Fly.io CLI,可以通过以下命令安装:
curl -L https://fly.io/install.sh | sh
安装完成后,使用 fly auth login
登录到你的 Fly.io 账号。
zeabur部署
zeabur.toml
app = "ech0-noise"
[build]
dockerfile = "Dockerfile"
image = "noise233/echo-noise"
[env]
DB_TYPE = "postgres"
DB_HOST = 'example.aws.neon.tech' # 修改为数据库的HOST地址
DB_PORT = "5432"
DB_USER = 'noise_owner' # 修改为数据库的用户名
DB_PASSWORD = 'example' # 修改为数据库的密码
DB_NAME = 'noise' # 修改为数据库的名称
DB_SSL_MODE = "require"
CGO_ENABLED = "1"
TZ = "Asia/Shanghai"
[http_service]
internal_port = 1314
force_https = true
[[services]]
protocol = "tcp"
internal_port = 1314
[[services.ports]]
port = 1314
[[vm]]
memory = "512mb"
cpu_kind = "shared"
cpus = 1
部署命令:
zeabur deploy
Railway部署
railway.toml
app = "ech0-noise"
[build]
dockerfile = "Dockerfile"
image = "noise233/echo-noise"
[env]
DB_TYPE = "postgres"
DB_HOST = 'example.aws.neon.tech' # 修改为数据库的HOST地址
DB_PORT = "5432"
DB_USER = 'noise_owner' # 修改为数据库的用户名
DB_PASSWORD = 'example' # 修改为数据库的密码
DB_NAME = 'noise' # 修改为数据库的名称
DB_SSL_MODE = "require"
CGO_ENABLED = "1"
TZ = "Asia/Shanghai"
[service]
internal_port = 1314
protocol = "tcp"
[service.ports]
port = 1314
[vm]
memory = "512mb"
cpu_kind = "shared"
cpus = 1
部署命令:
railway up
注意⚠️
如果你是直接在平台拉取项目部署而不是通过命令部署,你需要拷贝fork本项目并将fly.toml、railway.toml、zeabur.toml文件放入根目录下才能一键部署
数据库连接部分
本地数据库直接docker部署即可
远程数据库服务则可以通过环境变量连接
连接远程 PostgreSQL:
docker run -d \
--name Ech0-Noise \
--platform linux/amd64 \
-p 1314:1314 \
-e DB_TYPE=postgres \
-e DB_HOST=your.postgres.host \
-e DB_PORT=5432 \
-e DB_USER=your_username \
-e DB_PASSWORD=your_password \
-e DB_NAME=noise \
-v /opt/data/images:/app/data/images \
noise233/echo-noise
连接远程 MySQL:
docker run -d \
--name Ech0-Noise \
--platform linux/amd64 \
-p 1314:1314 \
-e DB_TYPE=mysql \
-e DB_HOST=your.mysql.host \
-e DB_PORT=3306 \
-e DB_USER=your_username \
-e DB_PASSWORD=your_password \
-e DB_NAME=noise \
-v /opt/data/images:/app/data/images \
noise233/echo-noise
注意事项:
- 确保远程数据库允许外部连接
- 检查防火墙设置
- 使用正确的数据库连接信息
- 建议使用加密连接
- 注意数据库的字符集设置
对于 Neon PostgreSQL (地址https://console.neon.tech )这样的云数据库服务,需要使用特定的连接参数。以下是连接命令:
docker run -d \
--name Ech0-Noise \
--platform linux/amd64 \
-p 1314:1314 \
-e DB_TYPE=postgres \
-e DB_HOST=your.host \
-e DB_PORT=5432 \
-e DB_USER=user_owner \
-e DB_PASSWORD=password \
-e DB_NAME=yourname \
-e DB_SSL_MODE=require \
-v /opt/data/images:/app/data/images \
noise233/echo-noise
注意事项:
- 添加了
DB_SSL_MODE=require
环境变量,因为 Neon 要求 SSL 连接 - 使用了连接 URL 中提供的主机名、用户名、密码和数据库名
- 保持图片目录的挂载
数据的备份恢复
对于所有数据库类型(SQLite/PostgreSQL/MySQL),点击后台数据库下载按钮后,都会先备份数据库文件
- 然后会将包含数据库备份和图片打包成 zip 文件
- zip 文件中会包含:
- 数据库备份文件(.db/.sql)
- images 目录下的所有图片
备份过程:
本地 -> 执行备份命令 -> 生成备份文件 -> 打包下载
恢复过程:
上传备份文件 -> 解压缩 -> 执行恢复命令 -> 导入到云数据库
恢复要求:
- SQLite本地数据库备份和上传时默认使用的文件名是一致为noise.db
- 非本地数据库PostgreSQL/MySQL请命名为database.sql并放入database.zip来恢复
- 如果备份时zip中有图片文件夹则同时会恢复 images 目录下的所有图片
⚠️ :因PostgreSQL/MySQL云服务会有SSL连接、兼容版本号、数据表格式等要求,后台一键备份恢复不一定能满足你需要连接的远程数据库,请尽量前往服务商处下载备份
API指南🧭
先到后台获取api token,然后可以参考下面的命令运行或使用其它服务(记得将https://your.localhost.com 更改为你自己的服务地址)
# 发送纯文本信息
curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer c721249bd66e1133fba430ea9e3c32f1' \
-d '{
"content": "测试信息",
"type": "text"
}'
# 方式1:使用 Markdown 语法发送文本
curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: c721249bd66e1133fba430ea9e3c32f1' \
-d '{
"content": "# 标题\n这是一段文字\n",
"type": "text"
}'
# 方式2:使用 type: image 发送图片消息
curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: c721249bd66e1133fba430ea9e3c32f1' \
-d '{
"content": "图片描述文字",
"type": "image",
"image": "https://example.com/image.jpg"
}'
如果你想使用session 认证方式
curl -v -X POST 'https://your.localhost.com/api/messages' \
-H 'Content-Type: application/json' \
--cookie "your_session_cookie" \
-d '{
"content": "测试信息",
"type": "text"
}'
对于图文混合消息,可以这样发送:
curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: c721249bd66e1133fba430ea9e3c32f1' \
-d '{
"content": "# 这是标题\n\n这是一段文字说明\n\n\n\n继续写文字内容",
"type": "text"
}'
或者使用 multipart 类型:
curl -X POST 'https://your.localhost.com/api/token/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: c721249bd66e1133fba430ea9e3c32f1' \
-d '{
"content": "# 这是标题\n\n这是一段文字说明",
"type": "multipart",
"image": "https://example.com/image.jpg"
}
API 文档(待增加)
公共接口
1. 获取前端配置
- 路径:
/api/frontend/config
- 方法: GET
- 描述: 获取前端配置信息
- 示例请求:
curl http://localhost:8080/api/frontend/config
2. 用户登录
- 路径:
/api/login
- 方法: POST
- 描述: 用户登录接口
- 请求体:
{
"username": "admin",
"password": "password"
}
- 示例请求:
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}'
3. 用户注册
- 路径:
/api/register
- 方法: POST
- 描述: 用户注册接口
- 请求体:
{
"username": "newuser",
"password": "password",
"email": "user@example.com"
}
- 示例请求:
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"username":"newuser","password":"password","email":"user@example.com"}'
4. 获取系统状态
- 路径:
/api/status
- 方法: GET
- 描述: 获取系统运行状态
- 示例请求:
curl http://localhost:8080/api/status
5. 消息相关公共接口
5.1 获取所有消息
- 路径:
/api/messages
- 方法: GET
- 描述: 获取所有公开消息
- 示例请求:
curl http://localhost:8080/api/messages
5.2 获取单条消息
- 路径:
/api/messages/:id
- 方法: GET
- 描述: 获取指定ID的消息
- 示例请求:
curl http://localhost:8080/api/messages/1
5.3 分页获取消息
- 路径:
/api/messages/page
- 方法: POST或GET
- 描述: 分页获取消息列表
- 请求体:
{
"page": 1,
"pageSize": 10
}
- 示例请求:
curl -X POST http://localhost:8080/api/messages/page \
-H "Content-Type: application/json" \
-d '{"page":1,"pageSize":10}'
5.4 获取消息日历数据
- 路径:
/api/messages/calendar
- 方法: GET
- 描述: 获取消息发布热力图数据
- 示例请求:
curl http://localhost:8080/api/messages/calendar
5.5 搜索消息
- 路径:
/api/messages/search
- 方法: GET
- 参数:
- keyword: 搜索关键词
- page: 页码
- pageSize: 每页数量
- 示例请求:
curl "http://localhost:8080/api/messages/search?keyword=测试&page=1&pageSize=10"
6. RSS 相关接口
6.1 获取 RSS 订阅
- 路径:
/rss
- 方法: GET
- 描述: 获取 RSS 订阅内容
- 示例请求:
curl http://localhost:1314/rss
需要认证的接口
1. 消息操作接口
1.1 发布消息
- 路径:
/api/messages
- 方法: POST
- 描述: 发布新消息
- 请求体:
{
"content": "消息内容",
"private": false,
"imageURL": ""
}
- 示例请求:
curl -X POST http://localhost:8080/api/messages \
-H "Content-Type: application/json" \
-H "Cookie: session=xxx" \
-d '{"content":"测试消息","private":false}'
1.2 更新消息
- 路径:
/api/messages/:id
- 方法: PUT
- 描述: 更新指定消息
- 请求体:
{
"content": "更新后的内容"
}
- 示例请求:
curl -X PUT http://localhost:8080/api/messages/1 \
-H "Content-Type: application/json" \
-H "Cookie: session=xxx" \
-d '{"content":"更新后的内容"}'
1.3 删除消息
- 路径:
/api/messages/:id
- 方法: DELETE
- 描述: 删除指定消息
- 示例请求:
curl -X DELETE http://localhost:8080/api/messages/1 \
-H "Cookie: session=xxx"
2. 用户相关接口
2.1 获取用户信息
- 路径:
/api/user
- 方法: GET
- 描述: 获取当前登录用户信息
- 示例请求:
curl http://localhost:8080/api/user \
-H "Cookie: session=xxx"
2.2 修改密码
- 路径:
/api/user/change_password
- 方法: PUT
- 请求体:
{
"oldPassword": "旧密码",
"newPassword": "新密码"
}
- 示例请求:
curl -X PUT http://localhost:8080/api/user/change_password \
-H "Content-Type: application/json" \
-H "Cookie: session=xxx" \
-d '{"oldPassword":"old","newPassword":"new"}'
2.3 更新用户信息
- 路径:
/api/user/update
- 方法: PUT
- 示例请求:
curl -X PUT http://localhost:8080/api/user/update \
-H "Content-Type: application/json" \
-H "Cookie: session=xxx" \
-d '{"username":"newname"}'
2.4 退出登录
- 路径:
/api/user/logout
- 方法: POST
- 示例请求:
curl -X POST http://localhost:8080/api/user/logout \
-H "Cookie: session=xxx"
3. Token 相关接口
3.1 获取用户 Token
- 路径:
/api/user/token
- 方法: GET
- 示例请求:
curl http://localhost:8080/api/user/token \
-H "Cookie: session=xxx"
3.2 重新生成 Token
- 路径:
/api/user/token/regenerate
- 方法: POST
- 示例请求:
curl -X POST http://localhost:8080/api/user/token/regenerate \
-H "Cookie: session=xxx"
4. 系统设置接口
4.1 更新系统设置
- 路径:
/api/settings
- 方法: PUT
- 请求体:
{
"allowRegistration": true,
"frontendSettings": {
"siteTitle": "网站标题",
"subtitleText": "副标题",
"avatarURL": "头像URL",
"username": "显示用户名",
"description": "描述",
"backgrounds": ["背景图URL"],
"cardFooterTitle": "页脚标题",
"cardFooterLink": "页脚链接",
"pageFooterHTML": "页脚HTML",
"rssTitle": "RSS标题",
"rssDescription": "RSS描述",
"rssAuthorName": "RSS作者",
"rssFaviconURL": "RSS图标URL",
"walineServerURL": "评论系统URL"
}
}
- 示例请求:
curl -X PUT http://localhost:8080/api/settings \
-H "Content-Type: application/json" \
-H "Cookie: session=xxx" \
-d '{"allowRegistration":true,"frontendSettings":{"siteTitle":"我的网站"}}'
5. 备份相关接口
5.1 下载备份
- 路径:
/api/backup/download
- 方法: GET
- 示例请求:
curl http://localhost:8080/api/backup/download \
-H "Cookie: session=xxx" \
--output backup.sql
5.2 恢复备份
- 路径:
/api/backup/restore
- 方法: POST
- 描述: 从备份文件恢复数据
- 示例请求:
curl -X POST http://localhost:8080/api/backup/restore \
-H "Cookie: session=xxx" \
-F "file=@backup.sql"
6. 图片上传接口
6.1 上传图片
- 路径:
/api/images/upload
- 方法: POST
- 描述: 上传图片文件
- 示例请求:
curl -X POST http://localhost:8080/api/images/upload \
-H "Cookie: session=xxx" \
-F "file=@image.jpg"
7.推送配置路由使用说明
获取推送配置
- 路径:
/api/notify/config
- 方法: GET
- 描述: 获取当前推送渠道配置
- 示例请求:
curl -X GET http://localhost:8080/api/notify/config \
-H "Cookie: session=xxx"
保存推送配置
- 路径:
/api/notify/config
- 方法: PUT
- 描述: 更新推送渠道配置
- 请求体示例:
{
"webhookEnabled": true,
"webhookURL": "https://webhook.example.com",
"telegramEnabled": true,
"telegramToken": "bot123:ABC",
"telegramChatID": "-100123456",
"weworkEnabled": false,
"weworkKey": "",
"feishuEnabled": true,
"feishuWebhook": "https://open.feishu.cn/xxx",
"feishuSecret": "signature_key"
}
- 示例请求:
curl -X PUT http://localhost:8080/api/notify/config \
-H "Cookie: session=xxx" \
-H "Content-Type: application/json" \
-d '{
"webhookEnabled": true,
"webhookURL": "https://webhook.example.com"
}'
测试推送
- 路径:
/api/notify/test
- 方法: POST
- 描述: 测试指定推送渠道
- 请求体示例:
{
"type": "telegram"
}
- 示例请求:
curl -X POST http://localhost:8080/api/notify/test \
-H "Cookie: session=xxx" \
-H "Content-Type: application/json" \
-d '{"type": "telegram"}'
发送推送
- 路径:
/api/notify/send
- 方法: POST
- 描述: 手动触发推送(需已配置推送渠道)
- 请求体示例:
{
"content": "测试消息内容",
"images": ["https://example.com/image.jpg"],
"format": "markdown"
}
- 示例请求:
curl -X POST http://localhost:8080/api/notify/send \
-H "Cookie: session=xxx" \
-H "Content-Type: application/json" \
-d '{"content": "紧急通知!"}'
注意事项:
- 所有需要认证的接口都需要在请求头中携带有效的 session cookie
- 部分接口可能需要管理员权限
- 所有请求示例中的域名和端口号需要根据实际部署情况调整
- 文件上传接口需要使用 multipart/form-data 格式
- Token 认证接口可以使用 Token 替代 session 进行认证
发布说明
目前会构建两个版本,
稳定版:latest镜像
实验版:last镜像
如果你需要构建自己的镜像发布-示例:
docker buildx build --platform linux/amd64,linux/arm64 -t noise233/echo-noise:latest --push --no-cache .
Memos数据库迁移示例
其中,你需要设置设置源数据库和目标数据库的路径,源数据库为memos_prod.db(memos数据)目标数据库为database.db(本站数据库),你还需要修改构建插入的数据中的用户名为你自己的用户名,分别迁移了原文本内容、发布时间,可以在noise/memos迁移文件夹中找到该脚本
,运行python3 main.py即可,
迁移结束后将你的数据库文件和原图片文件夹(有的话)打包为zip格式,进入站点后台选择恢复数据上传即可。
Popclip发送扩展
选中后自动识别安装,发送时会自动添加一个popclip开头的标签,token可在后台找到
// #popclip extension for Send to Shuo
// name: 说说笔记
// icon: square filled 说
// language: javascript
// module: true
// entitlements: [network]
// options: [{
// identifier: "siteUrl",
// label: "服务端地址",
// type: "string",
// defaultValue: "https://note.noisework.cn",
// description: "请确保地址正确,不要带末尾斜杠"
// }, {
// identifier: "token",
// label: "API Token",
// type: "string",
// description: "从设置页面获取最新Token"
// }]
async function sendToShuo(input, options) {
try {
// 参数预处理
const siteUrl = (options.siteUrl || "").replace(/\/+$/g, "");
const token = (options.token || "").trim();
const content = (input.text || "").trim();
// 验证参数
if (!/^https:\/\/[\w.-]+(:\d+)?$/.test(siteUrl)) {
throw new Error("地址格式错误,示例: https://note.noisework.cn");
}
if (!token) throw new Error("Token不能为空");
if (!content) throw new Error("选中文本不能为空");
// 发送请求
await sendRequestWithXMLHttpRequest(siteUrl, token, content);
PopClip.showText("✓ 发送成功");
} catch (error) {
handleRequestError(error);
}
}
// 使用 XMLHttpRequest 实现网络请求
function sendRequestWithXMLHttpRequest(siteUrl, token, content) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const url = `${siteUrl}/api/token/messages`;
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
xhr.timeout = 10000; // 设置超时时间(10秒)
// 设置回调函数
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText);
} else {
let errorMsg = `请求失败 (${xhr.status})`;
try {
const data = JSON.parse(xhr.responseText);
errorMsg = data.message || errorMsg;
} catch {}
reject(new Error(errorMsg));
}
}
};
// 处理网络错误
xhr.onerror = () => reject(new Error("网络错误"));
// 处理超时错误
xhr.ontimeout = () => reject(new Error("请求超时"));
try {
// 发送请求
const payload = JSON.stringify({
content: `#Popclip\n${content}`,
type: "text"
});
xhr.send(payload);
} catch (error) {
reject(new Error("请求发送失败: " + error.message));
}
});
}
// 错误处理
function handleRequestError(error) {
console.error("请求错误:", error);
const errorMap = {
"Failed to fetch": "无法连接到服务器",
"aborted": "请求超时",
"网络错误": "网络错误",
"401": "认证失败,请检查Token",
"404": "API地址不存在"
};
const message = Object.entries(errorMap).find(([key]) =>
error.message.includes(key)
)?.[1] || `请求错误: ${error.message.split('\n')[0].slice(0, 50)}`;
PopClip.showText(`❌ ${message}`);
}
exports.actions = [{
title: "发送至说说笔记",
code: sendToShuo,
icon: "square filled 说"
}];
Web组件示例
如果你想将内容作为说说嵌入或结合到你的网站、博客可以参考
说明:host为站点地址,limit为每页内容数量,domId为容器名,下面的代码展示了使用js来请求数据内容到前端并渲染处理的基本框架,其余需要你自己再丰富css样式和你自己的页面
html前端:
<link rel="stylesheet" href=". assets="" css="" note.css"=""></link rel="stylesheet" href=".>
<div id="note" class="note page active"></div id="note" class="note page active">
<div class="note-container"></div class="note-container">
<div class="loading-wrapper" style="text-align: center; padding: 20px;"></div class="loading-wrapper" style="text-align: center; padding: 20px;">
加载中...
<script type="text javascript"=""></script type="text>
var note = {
host: 'https://note.noisework.cn', //请修改为你自己的站点地址
limit: '10',
domId: '#note'
}
<script type="text javascript" src="./assets/js/note.js"></script type="text>
note.js
// Note says content loading script
document.addEventListener('DOMContentLoaded', function() {
// get parameters from global configuration
const config = window.note || {
host: 'https://note.noisework.cn',
limit: '10',
domId: '#note'
};
// 修改容器选择器
const container = document.querySelector('#note .note-container');
let currentPage = 1;
let isLoading = false;
let hasMore = true;
// create load more button
const loadMoreBtn = document.createElement('button');
loadMoreBtn.id = 'load-more-note';
loadMoreBtn.className = 'load-more';
loadMoreBtn.textContent = '加载更多';
loadMoreBtn.style.display = 'none';
loadMoreBtn.addEventListener('click', loadMoreContent);
// create already loaded all prompt
const loadedAll = document.createElement('div');
loadedAll.id = 'loaded-all-note';
loadedAll.className = 'loaded-all';
loadedAll.textContent = '已加载全部';
loadedAll.style.display = 'none';
container.appendChild(loadMoreBtn);
container.appendChild(loadedAll);
// initial load
loadInitialContent();
async function loadInitialContent() {
try {
console.log(`请求URL: ${config.host}/api/messages/page?page=${currentPage}&pageSize=${config.limit}`);
const response = await fetch(`${config.host}/api/messages/page?page=${currentPage}&pageSize=${config.limit}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('API响应数据:', result);
// 修改为检查result.data.items
if (result && result.code === 1 && result.data && result.data.items && Array.isArray(result.data.items)) {
const sortedData = result.data.items.sort((a, b) =>
new Date(b.created_at) - new Date(a.created_at)
);
renderMessages(sortedData);
if (result.data.items.length >= config.limit) {
loadMoreBtn.style.display = 'block';
} else {
loadedAll.style.display = 'block';
hasMore = false;
}
} else {
container.querySelector('.loading-wrapper').textContent = '暂无内容';
hasMore = false;
}
} catch (error) {
console.error('加载内容失败:', error);
container.querySelector('.loading-wrapper').textContent = '加载失败,请刷新重试';
}
}
async function loadMoreContent() {
if (isLoading || !hasMore) return;
isLoading = true;
loadMoreBtn.textContent = '加载中...';
currentPage++;
try {
const response = await fetch(`${config.host}/api/messages/page?page=${currentPage}&pageSize=${config.limit}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// 同样修改为检查result.data.items
if (result && result.code === 1 && result.data && result.data.items && Array.isArray(result.data.items)) {
const sortedData = result.data.items.sort((a, b) =>
new Date(b.created_at) - new Date(a.created_at)
);
renderMessages(sortedData);
if (result.data.items.length < config.limit) {
loadMoreBtn.style.display = 'none';
loadedAll.style.display = 'block';
hasMore = false;
}
} else {
loadMoreBtn.style.display = 'none';
loadedAll.style.display = 'block';
hasMore = false;
}
} catch (error) {
console.error('加载更多内容失败:', error);
currentPage--;
} finally {
isLoading = false;
loadMoreBtn.textContent = '加载更多';
}
}
function renderMessages(messages) {
const loadingWrapper = container.querySelector('.loading-wrapper');
if (loadingWrapper) {
loadingWrapper.style.display = 'none';
}
messages.forEach(message => {
const messageElement = createMessageElement(message);
container.insertBefore(messageElement, loadMoreBtn);
});
}
function createMessageElement(message) {
const messageDiv = document.createElement('div');
messageDiv.className = 'rssmergecard';
const contentDiv = document.createElement('div');
contentDiv.className = 'rssmergecard-content';
const title = document.createElement('h3');
title.className = 'rssmergecard-title';
title.textContent = message.username || '匿名用户';
const description = document.createElement('div');
description.className = 'rssmergecard-description';
// 解析Markdown内容和特殊链接
let processedContent = message.content || '无内容';
processedContent = parseMarkdown(processedContent);
processedContent = parseSpecialLinks(processedContent);
description.innerHTML = processedContent;
// 如果有图片则添加图片
if (message.image_url) {
const img = document.createElement('img');
img.src = message.image_url.startsWith('http') ? message.image_url : config.host + message.image_url;
img.style.maxWidth = '100%';
img.style.borderRadius = '6px';
img.style.margin = '10px 0';
description.appendChild(img);
}
const metaDiv = document.createElement('div');
metaDiv.className = 'rssmergecard-meta';
const timeSpan = document.createElement('span');
timeSpan.className = 'rssmergecard-time';
timeSpan.textContent = formatDate(message.created_at);
metaDiv.appendChild(timeSpan);
contentDiv.appendChild(title);
contentDiv.appendChild(description);
contentDiv.appendChild(metaDiv);
messageDiv.appendChild(contentDiv);
return messageDiv;
}
function parseMarkdown(content) {
// 处理标题
content = content.replace(/^#\s(.+)$/gm, '<h1>$1</h1>');
content = content.replace(/^##\s(.+)$/gm, '<h2>$1</h2>');
content = content.replace(/^###\s(.+)$/gm, '<h3>$1</h3>');
// 处理图片 
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:6px;margin:10px 0;">');
// 处理链接 [text](url)
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// 处理粗体 **text**
content = content.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// 处理斜体 *text*
content = content.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// 处理代码块 `code`
content = content.replace(/`([^`]+)`/g, '<code>$1</code>');
return content;
}
function parseSpecialLinks(content) {
// 定义各种平台的正则表达式
const BILIBILI_REG = /https:\/\/www\.bilibili\.com\/video\/((av[\d]{1,10})|(BV[\w]{10}))\/?/g;
const BILIBILI_A_TAG_REG = /<a\shref="https:\/\/www\.bilibili\.com\/video\/((av[\d]{1,10})|(BV[\w]{10}))\/?">.*<\/a>/g;
const QQMUSIC_REG = /<a\shref="https\:\/\/y\.qq\.com\/.*(\/[0-9a-zA-Z]+)(\.html)?".*?>.*?<\/a>/g;
const QQVIDEO_REG = /<a\shref="https:\/\/v\.qq\.com\/.*\/([a-zA-Z0-9]+)\.html".*?>.*?<\/a>/g;
const SPOTIFY_REG = /<a\shref="https:\/\/open\.spotify\.com\/(track|album)\/([\s\S]+)".*?>.*?<\/a>/g;
const YOUKU_REG = /<a\shref="https:\/\/v\.youku\.com\/.*\/id_([a-zA-Z0-9=]+)\.html".*?>.*<\/a>/g;
const YOUTUBE_REG = /<a\shref="https:\/\/(www\.youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})".*?>.*<\/a>/g;
const NETEASE_MUSIC_REG = /<a\shref="https?:\/\/music\.163\.com\/.*?id=(\d+)<\/a>/g;
// 解析各种链接
return content
.replace(BILIBILI_REG, "<div class='video-wrapper'><iframe src='https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=$1&as_wide=1&high_quality=1&danmaku=0' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen='true' style='position:absolute;height:100%;width:100%;'></iframe></div>")
.replace(YOUTUBE_REG, "<div class='video-wrapper'><iframe src='https://www.youtube.com/embed/$2' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen></iframe></div>")
.replace(NETEASE_MUSIC_REG, "<div class='music-wrapper'><meting-js auto='https://music.163.com/#/song?id=$1'></meting-js></div>")
.replace(QQMUSIC_REG, "<meting-js auto='https://y.qq.com/n/yqq/song$1.html'></meting-js>")
.replace(QQVIDEO_REG, "<div class='video-wrapper'><iframe src='//v.qq.com/iframe/player.html?vid=$1' allowFullScreen='true' frameborder='no'></iframe></div>")
.replace(SPOTIFY_REG, "<div class='spotify-wrapper'><iframe style='border-radius:12px' src='https://open.spotify.com/embed/$1/$2?utm_source=generator&theme=0' width='100%' frameBorder='0' allowfullscreen='' allow='autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture' loading='lazy'></iframe></div>")
.replace(YOUKU_REG, "<div class='video-wrapper'><iframe src='https://player.youku.com/embed/$1' frameborder=0 'allowfullscreen'></iframe></div>");
}
function formatDate(dateString) {
if (!dateString) return '未知时间';
return new Date(dateString).toLocaleString();
}
});
示例note.css
/* 基础卡片样式 */
.rssmergecard {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
padding: 20px;
transition: all 0.3s ease;
}
.rssmergecard:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* 标题样式 */
.rssmergecard-title {
color: #333;
font-size: 18px;
margin: 0 0 10px 0;
}
/* 内容样式 - 支持Markdown渲染 */
.rssmergecard-description {
color: #555;
line-height: 1.6;
font-size: 15px;
}
.rssmergecard-description p {
margin: 10px 0;
}
.rssmergecard-description a {
color: #3498db;
text-decoration: none;
}
.rssmergecard-description a:hover {
text-decoration: underline;
}
.rssmergecard-description img {
max-width: 100%;
height: auto;
border-radius: 4px;
}
/* 元信息样式 */
.rssmergecard-meta {
margin-top: 15px;
font-size: 13px;
color: #999;
}
/* 加载更多按钮样式 */
.load-more {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin: 20px auto;
display: block;
}
.load-more:hover {
background: #2980b9;
}
.loaded-all {
text-align: center;
color: #999;
font-size: 14px;
margin: 20px 0;
}
/* 特殊链接卡片样式 */
.media-card {
background: #f8f9fa;
border-left: 4px solid #3498db;
padding: 15px;
margin: 15px 0;
border-radius: 0 4px 4px 0;
}
.media-card-title {
font-weight: bold;
margin-bottom: 5px;
}
.video-wrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
margin: 15px 0;
}
.video-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 8px;
}
.music-wrapper, .spotify-wrapper {
margin: 15px 0;
border-radius: 8px;
overflow: hidden;
min-height: 86px; /* 确保有足够高度显示播放器 */
}
.music-wrapper meting-js {
width: 100%;
height: 86px;
}
ios快捷指令
使用快捷指令发布内容到站内,获取:https://www.icloud.com/shortcuts/8ba1240ab39d4bf2b4a02b69a5cc12bf
问题🙋
数据库可以直接迁移吗
1、直接上传至部署时挂载的路径中,重新启用,或者在容器文件夹/app/data/noise.db直接替换即可
2、使用后台数据库管理备份功能,支持一键下载、上传
数据库文件下载为zip格式,上传也必须为zip,本地数据库恢复包中必须有noise.db文件
关于魔改指南🌈
👉如何自定义化前端数据后添加到数据库?
需要在setting.go、migrate.go、models.go、controllers.go同时写入前端参数的后端定义,并修改前端参数信息为后端可读取的参数,其中controllers.go为控制器
- database.go 用于数据库连接管理
- migrate.go 用于数据库迁移和数据初始化
👉前端基本在web目录下,目前模版文件为components目录文件,pages下index.vue为父级模版
👉建议:不要和我一样在同一个文件里修改添加,造成一个文件上千行代码…请尽量使用父子层级来添加代码