chore: 移除投稿相关文档与说明

- 删除 CONTRIBUTING.md

- README / blog-post / CHANGELOG / ASSET_LICENSE:移除所有投稿、贡献者、素材来源声明等表述

- .github/ISSUE_TEMPLATE / PULL_REQUEST_TEMPLATE:移除素材投稿选项、CONTRIBUTING.md 引用、素材来源声明

- public/readme/index.html:移除投稿指南链接与 CONTRIBUTING.md 引用

- public/index.html:补充 favicon、og:meta、GitHub 顶栏链接(上次未推送的改进)
This commit is contained in:
2026-06-06 16:57:25 +08:00
parent 1975e51960
commit 11d1be285f
9 changed files with 1324 additions and 306 deletions

View File

@@ -32,4 +32,4 @@ assignees: ''
## 检查清单
- [ ] 我已经搜索过 [已有 Issue](https://github.com/shumengya/meme-api/issues?q=is%3Aissue) 确认没人提过
- [ ] 我愿意按 [CONTRIBUTING.md](../../CONTRIBUTING.md) 提一个 PR可选

View File

@@ -19,7 +19,6 @@ Closes #
- [ ] ⚡ 性能优化perf
- [ ] 📝 文档docs
- [ ] 🔧 构建 / 脚本 / 配置chore
- [ ] 🖼 素材投稿(新表情包 / 新分类)
## 改动内容
@@ -34,32 +33,22 @@ Closes #
<!-- 方便 reviewer 快速定位 -->
- [ ] `public/index.html`
- [ ] `public/meme/...`(素材)
- [ ] `scripts/generate-manifest.mjs`
- [ ] `转化.py`
- [ ] `public/memes.json`(清单,由 `npm run build` 自动重生成)
- [ ] `dist/memes.json`(构建产物,请确认未手动修改)
- [ ] 文档README / CONTRIBUTING / CHANGELOG / 其他)
- [ ] 文档README / CHANGELOG / 其他)
- [ ] GitHub 配置Issue 模板 / PR 模板 / .github 目录)
## 素材来源声明(仅素材投稿必填)
- **来源**:原创 / 转载自 `xxx` / 协议 `CC BY 4.0` / ...
- **是否商用素材**:否
- **是否涉及他人作品**:否 / 是,已附原始协议与作者
- **协议确认**:我同意按 [CC BY-NC-SA 4.0](./ASSET_LICENSE.md) 发布
## 自检清单
- [ ] 我已经读了 [CONTRIBUTING.md](./CONTRIBUTING.md)
- [ ] 本地 `npm run build` 通过,`public/memes.json` 已重生成
- [ ] 本地 `npm run dev` 跑过,浏览器实测没回归
- [ ] 移动端DevTools 切到 375px布局没崩
- [ ] 改动不引入新外部依赖
- [ ] 没有把 `dist/``node_modules/``__pycache__/``*.pyc``原图/` 里的源文件等 commit 进去
- [ ] 素材若涉他人,已在 PR 描述里写明出处与原始协议
- [ ] 我已更新 [CHANGELOG.md](./CHANGELOG.md)(如适用)
## 截图 / 录屏
<!-- UI 改动请务必附图;素材投稿可以附几张代表图。 -->
<!-- 如有 UI 改动请图。 -->

View File

@@ -54,16 +54,6 @@ CC BY-NC-SA 的 **NCNonCommercial** 含义:
如确有商用需求(广告、表情商店、付费内容、商品等),请提 [Issue](https://github.com/shumengya/meme-api/issues) 或直接联系作者协商。
## 关于投稿
向本仓库提交 PR 投稿素材,即表示你:
1. 确认你对所投稿素材拥有合法分发权利(自己原创 / 协议允许 / 来源已授权)
2. 同意本仓库按 **CC BY-NC-SA 4.0** 协议发布你的投稿
3. 同意其他人在遵守上述协议的前提下使用、改编、再分发你的投稿
如果你的素材使用了**他人的原始素材**,请在 PR 描述里写明出处与原始协议。
## 侵权处理
如果你认为本仓库的某张表情包**侵犯了你的合法权益**(版权、肖像权、商标等),请:

View File

@@ -17,7 +17,7 @@
## [1.0.0] - 2026-06-04
### 新增
- 仓库首次**公开**整理:补齐 README、LICENSE、CONTRIBUTING、CHANGELOG、Issue / PR 模板
- 仓库首次**公开**整理:补齐 README、LICENSE、CHANGELOG、Issue / PR 模板
- 双协议声明:代码 MIT表情包素材 CC BY-NC-SA 4.0
- 收录 **51 个分类、~2600+ 张**表情包WebP / GIF
- 图库前端(`public/index.html`):分类侧边栏 + 搜索 + 移动端 4 列 / 桌面 12 列网格

View File

@@ -1,232 +0,0 @@
# 投稿指南 🤝
非常欢迎你一起完善这个表情包图库!这份文档说明**怎么提交素材 / 修复 / 功能**才能又快又稳被合入。
> 阅读前请先看一眼 [README](./README.md) 和 [ASSET_LICENSE](./ASSET_LICENSE.md),了解项目结构与协议。
## 目录
- [我能贡献什么?](#-我能贡献什么)
- [我该 fork 哪个分支?](#-我该-fork-哪个分支)
- [提 Issue](#-提-issue)
- [素材投稿流程](#-素材投稿流程)
- [代码 / 文档投稿](#-代码--文档投稿)
- [约定与规范](#-约定与规范)
- [本地自检](#-本地自检)
- [PR 流程](#-pr-流程)
- [审核标准](#-审核标准)
- [行为准则](#-行为准则)
## 🎁 我能贡献什么?
| 类型 | 说明 |
| --- | --- |
| 🖼 新表情 / 新分类 | 投到 `public/meme/<新分类>/` 下 |
| 🐛 修坏图 / 错图 | 替换、删除或重新分类 |
| 🌐 图库前端改进 | 修改 `public/index.html`(单文件,无构建) |
| ⚙️ 构建脚本改进 | 改 `scripts/generate-manifest.mjs``转化.py` |
| 📝 文档 | 改进 `README.md`、本文件、API 说明页 |
| 🧪 Bug 报告 / 体验建议 | 提 Issue |
## 🌿 我该 fork 哪个分支?
**默认 `main` 分支。** 任何 PR 都基于 `main`
预览分支 `preview` 是 wrangler 部署的临时产物,**不要**直接 PR 到那里。
## 🐞 提 Issue
- **Bug 报告**:用 [Bug 报告模板](.github/ISSUE_TEMPLATE/bug_report.md),按字段填好
- **新功能 / 改进建议**:用 [功能建议模板](.github/ISSUE_TEMPLATE/feature_request.md)
- **侵权 / 删除请求**:标题加 `[takedown]`,并附素材路径和说明
## 🖼 素材投稿流程
### 0. 先确认你有分发权
只投**以下任一**的素材:
- ✅ 你自己原创 / 自己画的表情包
- ✅ 协议允许二次分发的素材(如 CC BY、CC BY-SA、公共领域
- ✅ 你已经获得作者明确授权
**别投**:商用素材、明显盗图、官方美术资源(除非协议明确允许)。
### 1. 处理素材(推荐)
> `转化.py` 会帮你自动编号、缩放、转格式;**不要**手动起 `1.webp` 这样的名字,编号是脚本加的。
```bash
# 拿一个新分类举例
mkdir -p 原图/我的新分类
cp ~/Downloads/表情.png 原图/我的新分类/
# 装 Pillow一次性
python -m pip install pillow
# 转换:自动编号、统一 webp/gif、最长边缩放到 250px
python 转化.py
# 把生成结果拷入发布目录
cp -r 原图-已处理/我的新分类 public/meme/
```
参数说明:
| 参数 | 作用 |
| --- | --- |
| `--delete-sources` | 处理完删除 `原图/` 里的源文件(默认保留) |
| `--max-side 250` | 最长边像素,默认 250px |
| `--jobs 10` | 并行线程数 |
### 2. 直接投到 `public/meme/`
如果你的素材**已经**是 `N.webp` / `N.gif` 命名,且最长边 ≤ 250px可以**直接**放到 `public/meme/<分类>/`,不跑 `转化.py`。脚本在生成清单时会自动识别已编号的素材。
### 3. 重新生成清单
```bash
npm run build
```
这一步会重写 `public/memes.json``dist/memes.json`**请把这两个文件一起 commit**。
### 4. 提交 PR
- **PR 标题**`feat(meme): 新增 xxx 分类``fix(meme): 删除损坏的 白色小人/123.webp`
- **PR 描述**
- 素材来源(自己画 / 转载自 xxx / 协议 xxxx
- 大致数量和分类
- 截图(可选)
## 💻 代码 / 文档投稿
### 前端
- `public/index.html` 是**单文件**前端HTML + 内联 CSS + 内联 JS**无任何依赖**
- 改完**直接刷新本地预览**就能看效果:
```bash
npm run dev
# 浏览器打开 http://localhost:8788
```
- 尽量保持:
- 移动端优先 → 桌面端
- 暗色模式自适应
- 键盘可达(`Tab`、`Enter`、`Esc`
- 不引入外部依赖
### 构建脚本
- `scripts/generate-manifest.mjs` — Node 18+ ESM
- `转化.py` — Python 3.8+,只依赖 Pillow
- 改完跑 `npm run build` 验证 `memes.json` 生成正确
### 文档
- 写完后检查一下渲染效果:本地打开 `README.md` 看看排版
- 文档用 **中文**变量名、函数名、commit 标题用**英文**
- 引用外部链接优先官方文档
## 📐 约定与规范
### Commit 消息
遵循 [Conventional Commits](https://www.conventionalcommits.org/)
```
feat(meme): 新增 黄仁勋 分类
fix(meme): 替换损坏的 滑稽/3.webp
docs: 完善 README 的 API 文档
style(frontend): 调整移动端栅格断点
refactor(scripts): 拆分 generate-manifest.mjs
chore: 升级 wrangler 到 4.7
```
允许类型:`feat` / `fix` / `docs` / `style` / `refactor` / `perf` / `test` / `chore` / `ci`
### 分支名
- `feat/<短描述>`:新功能 / 新分类
- `fix/<短描述>`:修 bug
- `docs/<短描述>`:文档
- `chore/<短描述>`:杂项
例:`feat/meme-add-minecraft-pack`、`fix/meme-broken-gifs`
### 分类目录命名
- 用**清晰的中文**,避免过长的字串
- 不要带空格(用空格的话在 URL 里得编码,体验差)
- 不要带特殊符号 `/`、`\`、`#``?`
- 想拆同主题的子分类就再加一个目录,例如 `抹茶旦旦2`
### 素材命名
- **不要**自己命名;让 `转化.py` 续号
- 已经手动起好的也 OK`1.webp` / `2.gif` 这种),脚本会从已有最大编号续起
### 文件格式
- 静态图:`.webp`(默认 lossless质量 + 体积兼顾)
- 动图:`.gif`Pillow 转码WebP 动图也会被转成 GIF 提升兼容性)
- 缩放:最长边 **250px**
- 单张建议 ≤ **30KB**;动图 ≤ **100KB**
## ✅ 本地自检
提交前请跑一遍:
```bash
# 1. 构建通过、清单生成正常
npm run build
# 2. 本地预览,检查图库显示、复制/下载、侧边栏等
npm run dev
# 3. (可选)检查素材大小
du -sh public/meme/<新分类>/
ls -la public/meme/<新分类>/ | head
```
确认:
- [ ] 新分类在侧边栏出现,编号连续无跳号
- [ ] 点开任意一张能正常复制 URL / Markdown / HTML / 下载
- [ ] 移动端DevTools 切到 375px布局没崩
- [ ] 没有把 `dist/``node_modules/``__pycache__/``*.pyc` 等 commit 进去
## 🔁 PR 流程
1. Fork → 新分支 → 改代码 / 投素材
2. `npm run build` 自检
3. 提 PR 到 `main`
4. 描述清楚改了什么;附截图(如有 UI 变化)
5. 等 CI / 维护者 review
6. 必要的话按 review 改一轮
7. 合并后 Cloudflare Pages 会自动部署
## 🧐 审核标准
PR 被合的优先级:
- ✅ 修复真实 bug
- ✅ 引入来源清晰的新素材
- ✅ 改进图库体验(无障碍 / 性能 / 暗色)
- ✅ 改进文档可读性
- ⚠️ 大批量素材(> 200 张)建议先开 Issue 沟通
- ❌ 协议不清晰的素材
- ❌ 引入外部依赖CDN 库、构建工具)的"重"改动
- ❌ 与项目无关的"小改"(拼写错误除外)
## 🫶 行为准则
- 友善、尊重、就事论事
- 拒绝任何形式的骚扰、歧视、人身攻击
- 提 Issue / 评论前先搜一下避免重复
- 不确定时先问,错了就改
违反行为准则的评论 / PR 会被关闭,屡次违反会被封禁。
---
有任何问题,欢迎在 [Issue](https://github.com/shumengya/meme-api/issues) 里沟通。谢谢你的贡献!🌱

View File

@@ -13,7 +13,7 @@
[![GitHub forks](https://img.shields.io/github/forks/shumengya/meme-api?style=flat-square)](https://github.com/shumengya/meme-api/network)
[![Last commit](https://img.shields.io/github/last-commit/shumengya/meme-api?style=flat-square)](https://github.com/shumengya/meme-api/commits/main)
[在线浏览](https://meme.smyhub.com) · [API 文档](https://meme.smyhub.com/readme/) · [投稿](#-投稿指南) · [更新日志](./CHANGELOG.md)
[在线浏览](https://meme.smyhub.com) · [API 文档](https://meme.smyhub.com/readme/) · [更新日志](./CHANGELOG.md)
</div>
@@ -235,15 +235,6 @@ meme-api/
如果想换 CDN 域名,可以在前端 JS 里给 `memes.json``baseUrl` 字段(详见脚本注释)。
## 🤝 投稿指南
欢迎补充新分类、修复坏图、改善图库体验!开始前请先看 **[CONTRIBUTING.md](./CONTRIBUTING.md)**,重点:
- 素材需是 **你自己有权分发** 的,或来源已明确允许
- 分类目录用清晰中文名;编号脚本会自动续号
- 一张图只发一个分类;动图尽量保留动画
- 提交 PR 前跑一次 `npm run build` 自检
## 📝 更新日志
见 [CHANGELOG.md](./CHANGELOG.md)。
@@ -258,12 +249,11 @@ meme-api/
| **表情包素材**`public/meme/` 下的所有 `.webp` / `.gif` 文件) | CC BY-NC-SA 4.0 | [ASSET_LICENSE.md](./ASSET_LICENSE.md) |
简言之:代码随便用、商用随便;素材**署名 + 非商用 + 相同方式共享**。
如果你投稿了素材,默认视为同意按 CC BY-NC-SA 4.0 发布;商用授权请单独联系。
## 🙏 致谢
- 所有为这个图库添砖加瓦的[贡献者](https://github.com/shumengya/meme-api/graphs/contributors)
- 各个表情包原图的原作者 / 出处方
- [Cloudflare Pages](https://pages.cloudflare.com/) 提供静态托管
- [Pillow](https://python-pillow.org/) 提供图片处理能力
- [Cloudflare Pages](https://pages.cloudflare.com/) 提供静态托管
- [Pillow](https://python-pillow.org/) 提供图片处理能力

186
blog-post.md Normal file
View File

@@ -0,0 +1,186 @@
# [萌芽妙妙工具] 萌芽表情包图库 —— 一个纯静态的表情包 API + 图库
> 项目地址:[github.com/shumengya/meme-api](https://github.com/shumengya/meme-api)
> 在线体验:[meme.smyhub.com](https://meme.smyhub.com)
> 协议MIT协议
---
## 这是做什么的
平时在群里聊天、写博客、做项目文档的时候,经常需要找表情包。网上搜到的图要么带水印,要么链接失效,要么格式不统一,用起来很不顺手。
所以我整理了一个表情包图库,做成纯静态站点部署在 Cloudflare Pages 上。打开网页就能翻、能搜、能一键复制链接,也对外提供 JSON API方便在任何地方调用。
目前一共收录了 **51 个分类、2600 多张** WebP / GIF 表情包,全部经过统一处理(最长边缩放到 250px动图统一为 GIF体积和兼容性都控制得比较好。
---
## 打开网页就能用
图库本身是一个单文件 HTML 页面(`public/index.html`),没有任何前端框架、没有任何外部依赖,打开就是下面这样:
- 左侧分类侧边栏,支持搜索过滤
- 手机端自动变成 4 列网格,电脑端最多 12 列
- 点击任意表情弹出操作面板,可以:
- 复制直链 URL
- 复制 Markdown 格式
- 复制 HTML 嵌入代码(宽度 96~400px 可选,会记住你的偏好)
- 直接下载原图
整个页面加载完之后所有交互都是本地的,不需要后端服务。
---
## 也能当 API 用
如果你不想打开网页,直接用接口拉数据也可以:
```bash
curl -sS "https://meme.smyhub.com/memes.json"
```
返回结构大概长这样:
```json
{
"generatedAt": "2026-05-19T13:01:45.229Z",
"categories": [
{
"id": "白色小人",
"name": "白色小人",
"items": [
{ "file": "1.webp", "path": "白色小人/1.webp" },
{ "file": "2.webp", "path": "白色小人/2.webp" }
]
}
]
}
```
拿到清单之后,拼接一下就是直链:
```
https://meme.smyhub.com/meme/白色小人/1.webp
```
中文路径记得 URL 编码,图库里的「复制链接」按钮已经帮你编好了。
接口还做了 CORS任何网页、博客、脚本、聊天机器人都可以直接跨域调用。
---
## 引用单张表情
在 Markdown 或 HTML 里直接贴:
```markdown
![哈哈](https://meme.smyhub.com/meme/%E7%99%BD%E8%89%B2%E5%B0%8F%E4%BA%BA/1.webp)
```
```html
<img src="https://meme.smyhub.com/meme/%E7%99%BD%E8%89%B2%E5%B0%8F%E4%BA%BA/1.webp"
alt="哈哈" loading="lazy" width="240"
style="max-width:240px;width:100%;height:auto" />
```
因为托管在 Cloudflare Pages全球边缘节点都有缓存国内访问速度也还可以。
---
## 技术结构
整个项目非常轻,没有复杂架构:
```
meme-api/
├── public/
│ ├── index.html ← 图库前端(单文件,零依赖)
│ ├── meme/ ← 处理后的表情包素材
│ ├── memes.json ← 自动生成的清单
│ ├── _headers ← CORS 响应头配置
│ ├── _redirects ← 旧接口兼容
│ └── readme/index.html ← API 说明页
├── scripts/
│ └── generate-manifest.mjs ← 生成 memes.json + 复制到 dist/
├── 原图/ ← 原始素材
├── 转化.py ← Python 一键处理脚本
└── package.json
```
### 纯静态,没有后端
所有内容都是静态文件。分类列表、搜索、复制、下载,全部在前端用原生 JS 完成。部署只需要把 `dist/` 丢到任意静态托管就行Cloudflare Pages、Vercel、GitHub Pages、对象存储都可以
### 零依赖前端
`index.html` 是一个完整的单页面应用,内联了所有 CSS 和 JavaScript没有引用任何外部 CDN。这意味着
- 内网环境、离线环境也能打开
- 不用担心 CDN 挂掉导致页面崩
- 审计和安全层面很干净
### Python 处理脚本
新素材丢进 `原图/<分类>/`,运行:
```bash
python 转化.py
```
会自动完成:
- 未编号素材自动续号
- 统一转成 WebP静态或 GIF动图
- 最长边缩放到 250px
- 多线程并行处理
处理完的东西会输出到 `原图-已处理/`,再手动拷进 `public/meme/` 就可以上线。
---
## 协议说明
这个仓库用了**双重许可**,代码和素材分开:
| 内容 | 协议 |
|------|------|
| 代码HTML / JS / Python / Node / 配置) | MIT随便用商用也行 |
| 表情包素材(`public/meme/` 下的所有图片) | CC BY-NC-SA 4.0,署名 + 非商用 + 相同方式共享 |
---
## 部署方式
### 方式一:连接 Git推荐
在 Cloudflare Pages 创建项目,连接 GitHub 仓库的 `main` 分支:
- 构建命令:`npm run build`
- 构建输出目录:`dist`
每次 push 会自动触发部署。
### 方式二CLI 直传
```bash
npm install
npm run build
npx wrangler pages deploy dist
```
---
## 总结
这个项目本质上就是一个**整理好的表情包文件夹 + 一个能用的前端 + 一个能读的 JSON 接口**。没有花里胡哨的功能,但刚好解决了"找表情、发链接、统一管理"这几个常见痛点。
如果你平时也需要大量表情包,或者想给自己的项目搭一个简单的素材 API可以试着部署一下这个项目。表情包内容会不定期整理更新。
---
**相关链接**
- 🌐 在线图库:[meme.smyhub.com](https://meme.smyhub.com)
- 📦 GitHub 仓库:[github.com/shumengya/meme-api](https://github.com/shumengya/meme-api)
- 📖 API 文档:[meme.smyhub.com/readme/](https://meme.smyhub.com/readme/)

View File

@@ -3,48 +3,952 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>meme-api</title>
<title>萌芽表情包图库(=・ω・=)</title>
<meta name="description" content="一个公开的表情包图库与 API分类浏览、一键复制链接 / Markdown / HTML51 个分类共 2600+ 张 WebP / GIF。" />
<meta name="theme-color" content="#FFCC4D" />
<meta property="og:title" content="萌芽表情包图库" />
<meta property="og:description" content="一个公开的表情包图库与 API分类浏览、一键复制链接 / Markdown / HTML。" />
<meta property="og:type" content="website" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%23FFCC4D'/><text x='50' y='74' text-anchor='middle' font-size='72' font-family='sans-serif' fill='%23000'>萌</text></svg>" />
<style>
:root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "PingFang SC","Microsoft YaHei", sans-serif; line-height: 1.55; }
body { max-width: 52rem; margin: 2rem auto; padding: 0 1.25rem; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; }
pre { overflow: auto; padding: 0.9rem 1rem; border-radius: 8px; background: color-mix(in oklab, Canvas 92%, CanvasText 8%); }
a { color: inherit; }
h1 { font-size: 1.35rem; }
h2 { font-size: 1.05rem; margin-top: 1.75rem; }
.muted { opacity: 0.78; font-size: 0.92rem; }
:root {
color-scheme: light dark;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
"PingFang SC", "Microsoft YaHei", sans-serif;
line-height: 1.45;
--bg: Canvas;
--fg: CanvasText;
--card: color-mix(in oklab, Canvas 94%, CanvasText 6%);
--border: color-mix(in oklab, Canvas 82%, CanvasText 18%);
--accent: color-mix(in oklab, CanvasText 12%, Canvas 88%);
--sidebar-w: 15rem;
--sidebar-w-collapsed: 2.75rem;
--topbar-h: 4.25rem;
}
body {
margin: 0;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
display: flex;
flex-direction: column;
}
body.sidebar-collapsed {
--sidebar-w: var(--sidebar-w-collapsed);
}
.top-bar {
flex-shrink: 0;
padding: 0.85rem clamp(0.75rem, 2vw, 1.25rem);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
background: color-mix(in oklab, var(--bg) 88%, transparent);
backdrop-filter: blur(10px);
z-index: 4;
}
.top-bar h1 {
font-size: 1.2rem;
margin: 0 0 0.25rem;
font-weight: 650;
}
.sub {
margin: 0;
font-size: 0.9rem;
opacity: 0.78;
}
.sub a {
color: inherit;
}
.layout {
display: flex;
flex: 1;
min-height: 0;
align-items: stretch;
}
.sidebar {
flex: 0 0 var(--sidebar-w);
width: var(--sidebar-w);
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
background: color-mix(in oklab, var(--bg) 96%, CanvasText 4%);
position: sticky;
top: var(--topbar-h);
height: calc(100vh - var(--topbar-h));
transition: width 0.22s ease, flex-basis 0.22s ease;
overflow: hidden;
z-index: 3;
}
.sidebar-head {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.45rem;
border-bottom: 1px solid var(--border);
}
.sidebar-toggle {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
font-size: 1.1rem;
line-height: 1;
-webkit-tap-highlight-color: transparent;
}
.sidebar-toggle:hover {
background: color-mix(in oklab, var(--accent) 65%, transparent);
}
.sidebar-toggle:focus-visible {
outline: 2px solid color-mix(in oklab, CanvasText 40%, transparent);
outline-offset: 2px;
}
.sidebar-title {
font-size: 0.82rem;
font-weight: 600;
opacity: 0.72;
white-space: nowrap;
overflow: hidden;
transition: opacity 0.15s ease;
}
body.sidebar-collapsed .sidebar-title {
opacity: 0;
width: 0;
overflow: hidden;
}
.sidebar-search {
flex-shrink: 0;
padding: 0 0.45rem 0.4rem;
}
body.sidebar-collapsed .sidebar-search {
display: none;
}
.cat-search-input {
width: 100%;
box-sizing: border-box;
font: inherit;
font-size: 0.82rem;
padding: 0.38rem 0.55rem;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg);
color: inherit;
}
.cat-search-input::placeholder {
opacity: 0.55;
}
.cat-search-input:focus {
outline: 2px solid color-mix(in oklab, CanvasText 28%, transparent);
outline-offset: 1px;
}
.cat-tabs-empty {
margin: 0.25rem 0.5rem;
font-size: 0.82rem;
opacity: 0.65;
}
.cat-tabs {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
overflow-x: hidden;
overflow-y: auto;
padding: 0.45rem;
margin: 0;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.cat-tab {
flex: 0 0 auto;
width: 100%;
box-sizing: border-box;
font: inherit;
font-size: 0.84rem;
padding: 0.42rem 0.55rem;
border-radius: 8px;
border: 1px solid transparent;
background: transparent;
color: inherit;
cursor: pointer;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background 0.12s ease, border-color 0.12s ease, opacity 0.15s ease;
-webkit-tap-highlight-color: transparent;
}
body.sidebar-collapsed .cat-tab {
opacity: 0;
pointer-events: none;
height: 0;
padding: 0;
margin: 0;
border: none;
overflow: hidden;
}
.cat-tab:hover {
background: color-mix(in oklab, var(--accent) 65%, transparent);
}
.cat-tab[aria-selected="true"] {
font-weight: 650;
border-color: color-mix(in oklab, CanvasText 22%, var(--border));
background: color-mix(in oklab, var(--accent) 100%, transparent);
}
.cat-tab:focus-visible {
outline: 2px solid color-mix(in oklab, CanvasText 40%, transparent);
outline-offset: 2px;
}
.content {
flex: 1;
min-width: 0;
}
main {
padding: 1.25rem clamp(0.5rem, 2vw, 1rem) 3rem;
max-width: none;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.sidebar-overlay {
display: none;
position: fixed;
inset: var(--topbar-h) 0 0 0;
z-index: 2;
border: none;
padding: 0;
margin: 0;
width: 100%;
background: color-mix(in oklab, CanvasText 32%, transparent);
cursor: pointer;
}
@media (max-width: 640px) {
:root {
--sidebar-w: min(78vw, 15rem);
}
.layout {
display: block;
}
.sidebar {
position: fixed;
left: 0;
top: var(--topbar-h);
flex: none;
}
body.sidebar-collapsed .sidebar {
box-shadow: none;
}
body:not(.sidebar-collapsed) .sidebar {
box-shadow: 4px 0 24px color-mix(in oklab, CanvasText 18%, transparent);
}
body:not(.sidebar-collapsed) .sidebar-overlay {
display: block;
}
.content {
width: 100%;
}
}
/* 手机:每行固定 4 列 */
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.4rem 0.35rem;
}
/* 电脑:每行最多 12 列,占满可用宽度 */
@media (min-width: 769px) {
.grid {
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 0.55rem 0.5rem;
}
}
.tile {
display: block;
width: 100%;
border: none;
margin: 0;
padding: 0;
border-radius: 0;
background: transparent;
cursor: pointer;
text-align: center;
transition: opacity 0.15s ease;
-webkit-tap-highlight-color: transparent;
}
.tile:hover {
opacity: 0.88;
}
.tile:focus-visible {
outline: 2px solid color-mix(in oklab, CanvasText 40%, transparent);
outline-offset: 3px;
}
.tile img {
display: block;
width: 100%;
aspect-ratio: 1;
height: auto;
object-fit: contain;
vertical-align: middle;
border-radius: 0;
background: transparent;
}
.tile .label {
margin-top: 0.2rem;
font-size: 0.68rem;
opacity: 0.55;
word-break: break-all;
line-height: 1.2;
}
.toast {
position: fixed;
left: 50%;
bottom: 1.25rem;
transform: translateX(-50%) translateY(120%);
opacity: 0;
padding: 0.6rem 1rem;
border-radius: 10px;
background: color-mix(in oklab, CanvasText 88%, Canvas 12%);
color: Canvas;
font-size: 0.9rem;
transition: transform 0.22s ease, opacity 0.22s ease;
z-index: 9;
pointer-events: none;
max-width: min(92vw, 420px);
text-align: center;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.error {
padding: 1rem;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--card);
}
.empty {
opacity: 0.8;
padding: 1rem 0;
}
.sheet-root {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: flex-end;
justify-content: center;
padding: env(safe-area-inset-bottom, 0) 0.75rem 0.75rem;
box-sizing: border-box;
}
.sheet-root[hidden] {
display: none !important;
}
.sheet-backdrop {
position: absolute;
inset: 0;
background: color-mix(in oklab, CanvasText 38%, transparent);
border: none;
padding: 0;
cursor: pointer;
}
.sheet-panel {
position: relative;
width: min(22rem, 100%);
max-height: min(85vh, 28rem);
overflow: auto;
border-radius: 14px 14px 0 0;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
box-shadow: 0 -8px 32px color-mix(in oklab, CanvasText 22%, transparent);
padding: 0.65rem 0;
margin-bottom: 0;
}
@media (min-width: 480px) {
.sheet-root {
align-items: center;
padding: 1rem;
}
.sheet-panel {
border-radius: 14px;
margin-bottom: 0;
}
}
.sheet-title {
margin: 0;
padding: 0.35rem 1rem 0.65rem;
font-size: 0.82rem;
font-weight: 600;
opacity: 0.85;
word-break: break-all;
border-bottom: 1px solid var(--border);
}
.sheet-btn {
display: block;
width: 100%;
text-align: left;
font: inherit;
font-size: 0.95rem;
padding: 0.75rem 1rem;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
transition: background 0.12s ease;
-webkit-tap-highlight-color: transparent;
}
.sheet-btn:hover,
.sheet-btn:focus-visible {
background: color-mix(in oklab, var(--accent) 85%, transparent);
}
.sheet-btn:active {
opacity: 0.92;
}
.sheet-cancel {
margin-top: 0.25rem;
border-top: 1px solid var(--border);
text-align: center;
font-weight: 500;
opacity: 0.78;
}
.sheet-size-bar {
padding: 0.5rem 1rem 0.65rem;
border-bottom: 1px solid var(--border);
}
.sheet-size-label {
display: block;
font-size: 0.72rem;
opacity: 0.68;
margin-bottom: 0.45rem;
}
.sheet-size-btns {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.sheet-size-btn {
font: inherit;
font-size: 0.8rem;
font-weight: 600;
padding: 0.35rem 0.55rem;
min-width: 2.75rem;
border-radius: 8px;
border: 1px solid var(--border);
background: transparent;
color: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.sheet-size-btn.is-active {
border-color: color-mix(in oklab, CanvasText 28%, var(--border));
background: color-mix(in oklab, var(--accent) 100%, transparent);
}
.sheet-size-btn:focus-visible {
outline: 2px solid color-mix(in oklab, CanvasText 40%, transparent);
outline-offset: 2px;
}
</style>
</head>
<body>
<h1>meme-api</h1>
<p class="muted">静态站点(<a href="https://pages.cloudflare.com/">Cloudflare Pages</a>)。在 <code>原图/</code> 放未编号素材后运行 <code>python 转化.py</code>,输出到 <code>原图-已处理/</code>。将成品拷入 <code>public/meme/</code> 后运行 <code>npm run build</code>:只生成 <code>memes.json</code> 并把 <code>public/</code> 复制到 <code>dist/</code><strong>不会</strong>改写 <code>public/meme/</code>。清单默认按 <code>public/meme/</code> 扫描;若尚未拷素材则回退扫描 <code>原图-已处理/</code>(无则 <code>原图/</code>)。需强制按源目录生成清单时用 <code>npm run build:manifest-from-source</code>。部署目录为 <code>dist/</code>。直链可外部引用JSON 已配置 CORS。</p>
<p><a href="/memes/">浏览图库(分页、按文件夹分类、点击复制链接)</a></p>
<h2>获取表情包列表</h2>
<p><code>GET /memes.json</code>(与旧版兼容:<code>GET /api/v1/memes</code>,内部映射为同一份 JSON</p>
<p>返回所有分类及文件相对路径;前端与脚本可用其拼接直链。</p>
<pre id="curl-memes">curl -sS ""</pre>
<h2>直链访问单张表情包</h2>
<p>站点根目录对应构建产物 <code>dist/</code>(本地从 <code>public/</code> 同步而来);表情文件在 <code>/meme/&lt;分类&gt;/</code>,对应 URL</p>
<p><code>GET /meme/&lt;分类&gt;/&lt;文件名&gt;.webp</code><code>.gif</code></p>
<pre id="curl-example">https://example.com/meme/%E7%99%BD%E8%89%B2%E5%B0%8F%E4%BA%BA/1.webp</pre>
<p class="muted">中文或多段路径请使用编码后的 URL浏览器会自动处理图库「复制链接」亦为已编码地址</p>
<h2>维护流程</h2>
<ol>
<li><code>原图/&lt;分类&gt;/</code> 放入未编号素材;运行 <code>python 转化.py</code>,在 <code>原图-已处理/</code> 生成编号 <code>.webp</code>/<code>.gif</code> 并限最长边(默认 300px默认<strong>保留</strong> <code>原图/</code> 源文件,清理可加 <code>--delete-sources</code></li>
<li>动图 WebP 会在 <code>python 转化.py</code> 转换流程中转为 GIF。</li>
<li><code>npm run build</code>:生成 <code>dist/</code>(含 <code>dist/memes.json</code>)。请先把 <code>原图-已处理/</code> 拷入 <code>public/meme/</code> 再构建;构建脚本不会覆盖 <code>public/meme/</code></li>
<li><strong>上线(生产)</strong>:项目在 Cloudflare 已连接 Git 时,由推送生产分支触发云端构建:<code>npm run build</code><code>git add/commit/push</code>;仪表盘「构建命令」填 <code>npm run build</code>,「构建输出目录」填 <code>dist</code><code>wrangler pages deploy dist</code> 在 Git 项目里常显示为<strong>预览</strong>,属正常。</li>
</ol>
<header class="top-bar">
<h1>萌芽表情包图库ヘ(=^・ω・^=)ノ</h1>
<p class="sub">
<a href="/readme/">说明</a>
·
<a href="/memes.json"><code>memes.json</code></a>
·
<a href="https://github.com/shumengya/meme-api" rel="noopener" target="_blank">GitHub</a>
</p>
</header>
<button type="button" class="sidebar-overlay" id="sidebar-overlay" hidden aria-label="关闭分类"></button>
<div class="layout">
<aside class="sidebar" id="sidebar">
<div class="sidebar-head">
<button
type="button"
class="sidebar-toggle"
id="sidebar-toggle"
aria-expanded="true"
aria-controls="cat-tabs"
title="收起分类"
>
<span aria-hidden="true"></span>
</button>
<span class="sidebar-title">分类</span>
</div>
<div class="sidebar-search">
<input
type="search"
class="cat-search-input"
id="cat-search"
placeholder="搜索分类…"
autocomplete="off"
enterkeyhint="search"
/>
</div>
<nav class="cat-tabs" id="cat-tabs" role="tablist" aria-label="表情包分类"></nav>
</aside>
<div class="content">
<main id="app">
<p class="empty">加载中…</p>
</main>
</div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<div id="sheet-root" class="sheet-root" hidden>
<button type="button" class="sheet-backdrop" id="sheet-backdrop" aria-label="关闭"></button>
<div class="sheet-panel" role="dialog" aria-modal="true" aria-labelledby="sheet-title">
<p id="sheet-title" class="sheet-title"></p>
<button type="button" class="sheet-btn" id="act-url">复制 url链接</button>
<button type="button" class="sheet-btn" id="act-md">复制 Markdown</button>
<div class="sheet-size-bar" id="sheet-size-bar" role="group" aria-label="HTML 宽度">
<span class="sheet-size-label">HTML 宽度</span>
<div class="sheet-size-btns">
<button type="button" class="sheet-size-btn" data-w="96" aria-pressed="false">96</button>
<button type="button" class="sheet-size-btn" data-w="128" aria-pressed="false">128</button>
<button type="button" class="sheet-size-btn" data-w="160" aria-pressed="false">160</button>
<button type="button" class="sheet-size-btn" data-w="200" aria-pressed="false">200</button>
<button type="button" class="sheet-size-btn" data-w="240" aria-pressed="false">240</button>
<button type="button" class="sheet-size-btn" data-w="280" aria-pressed="false">280</button>
<button type="button" class="sheet-size-btn" data-w="320" aria-pressed="false">320</button>
<button type="button" class="sheet-size-btn" data-w="400" aria-pressed="false">400</button>
<button type="button" class="sheet-size-btn" data-w="original" aria-pressed="false">原图</button>
</div>
</div>
<button type="button" class="sheet-btn" id="act-html">复制 HTML</button>
<button type="button" class="sheet-btn" id="act-download">下载</button>
<button type="button" class="sheet-btn sheet-cancel" id="act-close">取消</button>
</div>
</div>
<script>
(function () {
var o = window.location.origin;
document.title = "meme-api · " + o.replace(/^https?:\/\//, "");
document.getElementById("curl-memes").textContent = 'curl -sS "' + o + '/memes.json"';
document.getElementById("curl-example").textContent =
o + "/meme/" + encodeURIComponent("白色小人") + "/" + encodeURIComponent("1.webp");
const app = document.getElementById("app");
const tabsEl = document.getElementById("cat-tabs");
const sidebarToggle = document.getElementById("sidebar-toggle");
const sidebarOverlay = document.getElementById("sidebar-overlay");
const toastEl = document.getElementById("toast");
var SIDEBAR_KEY = "meme_sidebar_collapsed";
var categoryFilter = "";
function toAbsoluteUrl(url) {
if (!url) return url;
if (/^https?:\/\//i.test(url)) return url;
try {
return new URL(url, location.href).href;
} catch (e) {
return url;
}
}
function isSidebarCollapsed() {
return document.body.classList.contains("sidebar-collapsed");
}
function syncSidebarUI() {
if (!sidebarToggle) return;
var collapsed = isSidebarCollapsed();
sidebarToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
sidebarToggle.title = collapsed ? "展开分类" : "收起分类";
var icon = sidebarToggle.querySelector("span");
if (icon) icon.textContent = collapsed ? "" : "";
if (sidebarOverlay) sidebarOverlay.hidden = collapsed;
}
function setSidebarCollapsed(collapsed) {
document.body.classList.toggle("sidebar-collapsed", collapsed);
syncSidebarUI();
try {
localStorage.setItem(SIDEBAR_KEY, collapsed ? "1" : "0");
} catch (e) {}
}
function loadSidebar() {
var collapsed = false;
try {
if (window.matchMedia("(max-width: 640px)").matches) {
collapsed = localStorage.getItem(SIDEBAR_KEY) !== "0";
} else {
collapsed = localStorage.getItem(SIDEBAR_KEY) === "1";
}
} catch (e) {
collapsed = window.matchMedia("(max-width: 640px)").matches;
}
setSidebarCollapsed(collapsed);
}
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
setSidebarCollapsed(!isSidebarCollapsed());
});
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener("click", function () {
setSidebarCollapsed(true);
});
}
loadSidebar();
const sheetRoot = document.getElementById("sheet-root");
const sheetTitle = document.getElementById("sheet-title");
const sheetBackdrop = document.getElementById("sheet-backdrop");
/** @type {{ url: string; file: string; category: string } | null} */
var sheetCtx = null;
/** @type {number | null} null = 原图尺寸 */
var selectedWidthPx = 240;
var WIDTH_STORAGE = "meme_copy_img_width";
var VALID_HTML_WIDTHS = [96, 128, 160, 200, 240, 280, 320, 400];
function loadSavedWidth() {
try {
var v = localStorage.getItem(WIDTH_STORAGE);
if (v === "original") selectedWidthPx = null;
else {
var n = parseInt(v, 10);
selectedWidthPx =
VALID_HTML_WIDTHS.indexOf(n) >= 0 ? n : 240;
}
} catch (e) {
selectedWidthPx = 240;
}
}
function persistWidth() {
try {
localStorage.setItem(
WIDTH_STORAGE,
selectedWidthPx == null ? "original" : String(selectedWidthPx)
);
} catch (e) {}
}
function syncSizeBarUI() {
var bar = document.getElementById("sheet-size-bar");
if (!bar) return;
bar.querySelectorAll(".sheet-size-btn").forEach(function (btn) {
var w = btn.getAttribute("data-w");
var active =
(w === "original" && selectedWidthPx == null) ||
(w !== "original" && parseInt(w, 10) === selectedWidthPx);
btn.classList.toggle("is-active", active);
btn.setAttribute("aria-pressed", active ? "true" : "false");
});
}
function wireSizeBar() {
document.querySelectorAll("#sheet-size-bar .sheet-size-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
var w = btn.getAttribute("data-w");
selectedWidthPx = w === "original" ? null : parseInt(w, 10);
persistWidth();
syncSizeBarUI();
});
});
}
loadSavedWidth();
wireSizeBar();
syncSizeBarUI();
function showToast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(showToast._t);
showToast._t = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
async function copyText(text, toastMsg) {
try {
await navigator.clipboard.writeText(text);
showToast(toastMsg || "已复制到剪贴板");
} catch {
window.prompt("请手动复制:", text);
}
}
function escapeAttr(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;");
}
function markdownImage(url, alt) {
var a = String(alt || "表情").replace(/\]/g, "");
return "![" + a + "](" + url + ")";
}
function htmlEmbed(url, alt, widthPx) {
var parts =
'<img src="' +
escapeAttr(url) +
'" alt="' +
escapeAttr(alt || "") +
'" loading="lazy" draggable="false"';
if (widthPx != null) {
parts +=
' width="' +
widthPx +
'" style="max-width:' +
widthPx +
'px;width:100%;height:auto"';
}
return parts + " />";
}
function openSheet(url, file, category) {
sheetCtx = {
url: toAbsoluteUrl(url),
file: file,
category: category,
};
sheetTitle.textContent = (category ? category + " / " : "") + file;
syncSizeBarUI();
sheetRoot.hidden = false;
document.body.style.overflow = "hidden";
}
function closeSheet() {
sheetRoot.hidden = true;
document.body.style.overflow = "";
sheetCtx = null;
}
function wireSheetActions() {
sheetBackdrop.addEventListener("click", closeSheet);
document.getElementById("act-close").addEventListener("click", closeSheet);
document.getElementById("act-url").addEventListener("click", function () {
if (!sheetCtx) return;
copyText(sheetCtx.url, "已复制链接");
closeSheet();
});
document.getElementById("act-md").addEventListener("click", function () {
if (!sheetCtx) return;
var alt = sheetCtx.category + "/" + sheetCtx.file;
copyText(markdownImage(sheetCtx.url, alt), "已复制 Markdown");
closeSheet();
});
document.getElementById("act-html").addEventListener("click", function () {
if (!sheetCtx) return;
var alt = sheetCtx.category + "/" + sheetCtx.file;
var tip =
selectedWidthPx == null ? "已复制 HTML" : "已复制 HTML" + selectedWidthPx + "px";
copyText(htmlEmbed(sheetCtx.url, alt, selectedWidthPx), tip);
closeSheet();
});
document.getElementById("act-download").addEventListener("click", function () {
if (!sheetCtx) return;
var u = sheetCtx.url;
var name = sheetCtx.file;
closeSheet();
(async function () {
try {
var res = await fetch(u);
if (!res.ok) throw new Error("HTTP " + res.status);
var blob = await res.blob();
var objectUrl = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = objectUrl;
a.download = name;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(objectUrl);
showToast("已开始下载");
} catch (e) {
var link = document.createElement("a");
link.href = u;
link.download = name;
link.rel = "noopener";
link.target = "_blank";
document.body.appendChild(link);
link.click();
link.remove();
showToast("已尝试下载");
}
})();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && !sheetRoot.hidden) closeSheet();
});
}
wireSheetActions();
function readHashCategory() {
var raw = location.hash.replace(/^#/, "");
if (!raw) return null;
try {
return decodeURIComponent(raw);
} catch (e) {
return raw;
}
}
function setHashCategory(id) {
var enc = encodeURIComponent(id);
if (location.hash !== "#" + enc) {
history.replaceState(null, "", "#" + enc);
}
}
function render(data) {
/** 可选 CDN 根;未设置时清单与 /meme 相对当前页面解析,支持子路径部署与本机 file:// 打开 */
var assetBase = data.baseUrl ? String(data.baseUrl).replace(/\/?$/, "") : "";
function memeAssetUrl(catId, fileName) {
var path =
"/meme/" +
encodeURIComponent(catId) +
"/" +
encodeURIComponent(fileName);
return toAbsoluteUrl(assetBase ? assetBase + path : path);
}
var categories = data.categories || [];
if (!categories.length) {
tabsEl.innerHTML = "";
app.innerHTML =
'<div class="empty">暂无数据。构建并部署后可在此查看。</div>';
return;
}
var selectedId = readHashCategory();
var validIds = {};
for (var i = 0; i < categories.length; i++) {
validIds[categories[i].id] = true;
}
if (!selectedId || !validIds[selectedId]) {
selectedId = categories[0].id;
setHashCategory(selectedId);
}
function findCategory(id) {
for (var j = 0; j < categories.length; j++) {
if (categories[j].id === id) return categories[j];
}
return categories[0];
}
var catSearchEl = document.getElementById("cat-search");
function renderTabs() {
tabsEl.innerHTML = "";
var q = categoryFilter.trim().toLowerCase();
var visible = 0;
for (var k = 0; k < categories.length; k++) {
var cat = categories[k];
if (q) {
var hay = ((cat.name || "") + " " + cat.id).toLowerCase();
if (hay.indexOf(q) < 0) continue;
}
visible++;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "cat-tab";
btn.setAttribute("role", "tab");
btn.setAttribute("aria-selected", cat.id === selectedId ? "true" : "false");
btn.textContent = (cat.name || cat.id) + " · " + (cat.items || []).length;
btn.addEventListener(
"click",
(function (id) {
return function () {
if (selectedId === id) return;
selectedId = id;
setHashCategory(id);
renderTabs();
renderGrid();
if (window.matchMedia("(max-width: 640px)").matches) {
setSidebarCollapsed(true);
}
};
})(cat.id)
);
tabsEl.appendChild(btn);
}
if (q && visible === 0) {
var empty = document.createElement("p");
empty.className = "cat-tabs-empty";
empty.textContent = "无匹配分类";
tabsEl.appendChild(empty);
}
var selectedBtn = tabsEl.querySelector('[aria-selected="true"]');
if (selectedBtn) {
selectedBtn.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
}
if (catSearchEl && !catSearchEl._wired) {
catSearchEl._wired = true;
catSearchEl.addEventListener("input", function () {
categoryFilter = catSearchEl.value;
if (categoryFilter.trim() && isSidebarCollapsed()) {
setSidebarCollapsed(false);
}
renderTabs();
});
}
function renderGrid() {
var cat = findCategory(selectedId);
var items = cat.items || [];
app.innerHTML = "";
var grid = document.createElement("div");
grid.className = "grid";
for (let n = 0; n < items.length; n++) {
const it = items[n];
const url = toAbsoluteUrl(it.url || memeAssetUrl(cat.id, it.file));
const tile = document.createElement("button");
tile.type = "button";
tile.className = "tile";
tile.title = it.file;
const img = document.createElement("img");
img.loading = "lazy";
img.alt = it.file;
img.src = url;
tile.appendChild(img);
const label = document.createElement("div");
label.className = "label";
label.textContent = it.file;
tile.appendChild(label);
tile.addEventListener("click", function () {
openSheet(url, it.file, cat.name || cat.id);
});
grid.appendChild(tile);
}
app.appendChild(grid);
}
renderTabs();
renderGrid();
window.addEventListener("hashchange", function () {
var hid = readHashCategory();
if (hid && validIds[hid] && hid !== selectedId) {
selectedId = hid;
renderTabs();
renderGrid();
}
});
}
fetch("/memes.json")
.then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then(render)
.catch(function (e) {
app.innerHTML =
'<div class="error">加载失败:<code>' +
String(e.message || e) +
"</code></div>";
});
})();
</script>
</body>

191
public/readme/index.html Normal file
View File

@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>API 说明 · 萌芽表情包图库</title>
<meta name="description" content="萌芽表情包图库的公开 API 文档清单接口、单图直链、CORS 与维护流程。" />
<style>
:root {
color-scheme: light dark;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"PingFang SC", "Microsoft YaHei", sans-serif;
line-height: 1.6;
}
body {
max-width: 52rem;
margin: 2rem auto;
padding: 0 1.25rem 4rem;
word-break: break-word;
}
h1 { font-size: 1.55rem; margin: 0 0 0.25rem; }
h2 { font-size: 1.15rem; margin-top: 2rem; border-bottom: 1px solid color-mix(in oklab, CanvasText 18%, transparent); padding-bottom: 0.3rem; }
h3 { font-size: 1rem; margin-top: 1.25rem; }
code, pre, kbd { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace; font-size: 0.92em; }
code { padding: 0.12em 0.36em; border-radius: 4px; background: color-mix(in oklab, Canvas 92%, CanvasText 8%); }
pre {
overflow: auto;
padding: 0.9rem 1rem;
border-radius: 8px;
background: color-mix(in oklab, Canvas 92%, CanvasText 8%);
line-height: 1.45;
}
pre code { padding: 0; background: transparent; }
a { color: inherit; }
a:hover { text-decoration: underline; }
.muted { opacity: 0.75; font-size: 0.92rem; }
.top-nav { display: flex; flex-wrap: wrap; gap: 0.25rem 0.85rem; font-size: 0.92rem; opacity: 0.82; margin-bottom: 0.5rem; }
.top-nav a { text-decoration: none; }
.top-nav a:hover { text-decoration: underline; }
table { border-collapse: collapse; width: 100%; font-size: 0.92rem; }
th, td { text-align: left; padding: 0.4rem 0.6rem; border-bottom: 1px solid color-mix(in oklab, CanvasText 14%, transparent); }
th { opacity: 0.78; }
.pill { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.78rem; background: color-mix(in oklab, Canvas 92%, CanvasText 8%); }
.pill.get { background: color-mix(in oklab, #16a34a 22%, Canvas); }
.badge { display: inline-block; padding: 0.05rem 0.4rem; border-radius: 4px; font-size: 0.78rem; background: color-mix(in oklab, Canvas 92%, CanvasText 8%); }
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; font-size: 0.92rem; }
.kv dt { font-weight: 600; }
.kv dd { margin: 0; opacity: 0.92; }
.toc { font-size: 0.9rem; padding: 0.6rem 0.9rem; border: 1px solid color-mix(in oklab, CanvasText 14%, transparent); border-radius: 8px; }
.toc ul { margin: 0.2rem 0 0; padding-left: 1.2rem; }
</style>
</head>
<body>
<nav class="top-nav">
<a href="/">← 返回图库</a>
<a href="https://github.com/shumengya/meme-api" rel="noopener">GitHub 仓库</a>
<a href="https://github.com/shumengya/meme-api/blob/main/README.md" rel="noopener">README</a>
<a href="https://github.com/shumengya/meme-api/blob/main/CHANGELOG.md" rel="noopener">更新日志</a>
</nav>
<h1>萌芽表情包图库 · API 说明</h1>
<p class="muted">
纯静态站点(<a href="https://pages.cloudflare.com/" rel="noopener">Cloudflare Pages</a>)。
所有接口都是 <strong>GET</strong>JSON 接口已开启 CORS可被任意网页 / 脚本调用。
</p>
<div class="toc">
<strong>目录</strong>
<ul>
<li><a href="#memes-json">1. 获取表情包清单</a></li>
<li><a href="#direct-link">2. 直链访问单张表情包</a></li>
<li><a href="#cors">3. CORS</a></li>
<li><a href="#workflow">4. 维护流程(仅项目维护者)</a></li>
<li><a href="#examples">5. 客户端示例</a></li>
<li><a href="#links">6. 相关链接</a></li>
</ul>
</div>
<h2 id="memes-json">1. 获取表情包清单</h2>
<p>
<span class="pill get">GET</span>
<code>/memes.json</code>
(旧版兼容:<code>/api/v1/memes</code>,内部 <code>_redirects</code> 映射为同一份 JSON
</p>
<p>返回所有分类及文件相对路径;前端与脚本可用其拼接直链。</p>
<pre><code id="curl-memes">curl -sS ""</code></pre>
<h3>返回结构</h3>
<pre><code>{
"generatedAt": "2026-05-19T13:01:45.229Z",
"categories": [
{
"id": "白色小人",
"name": "白色小人",
"items": [
{ "file": "1.webp", "path": "白色小人/1.webp" },
{ "file": "2.webp", "path": "白色小人/2.webp" }
]
}
]
}</code></pre>
<h3>字段说明</h3>
<dl class="kv">
<dt>generatedAt</dt><dd>清单生成时间ISO 8601 UTC</dd>
<dt>categories[].id</dt><dd>分类目录名URL 中需编码</dd>
<dt>categories[].name</dt><dd>展示名(当前与 id 相同)</dd>
<dt>items[].file</dt><dd>文件名,含扩展名 <code>.webp</code> / <code>.gif</code></dd>
<dt>items[].path</dt><dd><code>{分类}/{文件名}</code>,拼接成 <code>/meme/{path}</code> 即直链</dd>
</dl>
<h2 id="direct-link">2. 直链访问单张表情包</h2>
<p>站点根目录对应构建产物 <code>dist/</code>(本地从 <code>public/</code> 同步而来);表情文件在 <code>/meme/&lt;分类&gt;/</code>,对应 URL</p>
<p>
<span class="pill get">GET</span>
<code>/meme/&lt;分类&gt;/&lt;文件名&gt;.webp</code>
<code>.gif</code>
</p>
<pre><code id="curl-example">https://meme.smyhub.com/meme/%E7%99%BD%E8%89%B2%E5%B0%8F%E4%BA%BA/1.webp</code></pre>
<p class="muted">中文或多段路径请使用编码后的 URL浏览器会自动处理图库「复制链接」按钮已帮你编好</p>
<h3>响应头</h3>
<p>由 Cloudflare 边缘缓存,<code>Cache-Control: public, max-age=...</code>;按需可直接 <code>?v=&lt;时间戳&gt;</code> 绕过缓存。</p>
<h2 id="cors">3. CORS</h2>
<p><code>/memes.json</code><code>/api/v1/memes</code> 已配置:</p>
<pre><code>Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Allow-Headers: Content-Type</code></pre>
<p class="muted">图片资源 <code>/meme/...</code> 无 CORS 限制(普通 <code>&lt;img&gt;</code> / <code>&lt;a&gt;</code> 引用即可)。</p>
<h2 id="workflow">4. 维护流程(仅项目维护者)</h2>
<ol>
<li><code>原图/&lt;分类&gt;/</code> 放入未编号素材;运行 <code>python 转化.py</code>,在 <code>原图-已处理/</code> 生成编号 <code>.webp</code> / <code>.gif</code> 并限最长边(默认 250px。默认<strong>保留</strong> <code>原图/</code> 源文件,清理可加 <code>--delete-sources</code></li>
<li>动图 WebP 会在 <code>python 转化.py</code> 转换流程中转为 GIF。</li>
<li><code>npm run build</code>:生成 <code>dist/</code>(含 <code>dist/memes.json</code>)。请先把 <code>原图-已处理/</code> 拷入 <code>public/meme/</code> 再构建;构建脚本<strong>不会</strong>覆盖 <code>public/meme/</code></li>
<li><strong>上线(生产)</strong>:项目在 Cloudflare 已连接 Git 时,由推送生产分支触发云端构建:<code>npm run build</code><code>git add/commit/push</code>;仪表盘「构建命令」填 <code>npm run build</code>,「构建输出目录」填 <code>dist</code><code>wrangler pages deploy dist</code> 在 Git 项目里常显示为<strong>预览</strong>,属正常。</li>
</ol>
<h2 id="examples">5. 客户端示例</h2>
<h3>JavaScript (浏览器 / Node 18+)</h3>
<pre><code>// 拉清单
const r = await fetch("https://meme.smyhub.com/memes.json");
const { categories } = await r.json();
// 随机抽一张
const cat = categories[Math.floor(Math.random() * categories.length)];
const it = cat.items[Math.floor(Math.random() * cat.items.length)];
const url = `https://meme.smyhub.com/meme/${encodeURIComponent(cat.id)}/${encodeURIComponent(it.file)}`;
console.log(url);</code></pre>
<h3>Python</h3>
<pre><code>import json, random, urllib.parse, urllib.request
with urllib.request.urlopen("https://meme.smyhub.com/memes.json") as r:
data = json.load(r)
cat = random.choice(data["categories"])
it = random.choice(cat["items"])
url = f"https://meme.smyhub.com/meme/{urllib.parse.quote(cat['id'])}/{urllib.parse.quote(it['file'])}"
print(url)</code></pre>
<h3>Markdown / HTML 嵌入</h3>
<pre><code>![哈哈](https://meme.smyhub.com/meme/%E7%99%BD%E8%89%B2%E5%B0%8F%E4%BA%BA/1.webp)
&lt;img src="https://meme.smyhub.com/meme/%E7%99%BD%E8%89%B2%E5%B0%8F%E4%BA%BA/1.webp"
alt="哈哈" loading="lazy" width="240"
style="max-width:240px;width:100%;height:auto" /&gt;</code></pre>
<h2 id="links">6. 相关链接</h2>
<ul>
<li>🌐 在线图库:<a href="https://meme.smyhub.com/" rel="noopener">meme.smyhub.com</a></li>
<li>📦 仓库:<a href="https://github.com/shumengya/meme-api" rel="noopener">github.com/shumengya/meme-api</a></li>
<li>📝 README<a href="https://github.com/shumengya/meme-api/blob/main/README.md" rel="noopener">README.md</a></li>
<li>🪧 协议:代码 <a href="https://github.com/shumengya/meme-api/blob/main/LICENSE" rel="noopener">MIT</a>,素材 <a href="https://github.com/shumengya/meme-api/blob/main/ASSET_LICENSE.md" rel="noopener">CC BY-NC-SA 4.0</a></li>
</ul>
<script>
(function () {
var o = window.location.origin;
document.title = "API 说明 · " + o.replace(/^https?:\/\//, "");
var base = o || "https://meme.smyhub.com";
var memesEl = document.getElementById("curl-memes");
var exEl = document.getElementById("curl-example");
if (memesEl) memesEl.textContent = 'curl -sS "' + base + '/memes.json"';
if (exEl)
exEl.textContent =
base + "/meme/" + encodeURIComponent("白色小人") + "/" + encodeURIComponent("1.webp");
})();
</script>
</body>
</html>