Compare commits
4 Commits
e1bfa7a712
...
3e7216efb9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e7216efb9 | ||
| d452126069 | |||
| 11d1be285f | |||
| 1975e51960 |
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: 🐛 Bug 报告
|
||||
about: 报告一个让你遇到问题的 bug(前端 / API / 构建 / 素材缺失等)
|
||||
title: '[Bug] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 描述
|
||||
|
||||
简洁清晰地描述这个 bug 是什么。
|
||||
|
||||
## 复现步骤
|
||||
|
||||
1. 打开 `https://meme.smyhub.com/...`
|
||||
2. 点击 `...`
|
||||
3. 选择 `...`
|
||||
4. 看到 `...` 而预期是 `...`
|
||||
|
||||
## 预期行为
|
||||
|
||||
你**期望**发生什么。
|
||||
|
||||
## 实际行为
|
||||
|
||||
实际发生了什么?错误信息、控制台报错、网络请求等。
|
||||
|
||||
## 截图 / 录屏
|
||||
|
||||
如果有的话,附上(拖拽到下面即可)。
|
||||
|
||||
## 环境
|
||||
|
||||
- 浏览器 / 版本:例如 Chrome 125.0 / Safari 17.5 / Firefox 127
|
||||
- 操作系统:例如 Windows 11 / macOS 14 / iOS 17 / Android 14
|
||||
- 设备:例如 PC / iPhone 14 / 小米 14
|
||||
- 页面 URL:例如 https://meme.smyhub.com/#白色小人
|
||||
- 影响的素材路径(如果是某张图坏了):例如 `白色小人/123.webp`
|
||||
|
||||
## 浏览器控制台
|
||||
|
||||
> 打开 DevTools(F12)→ Console 标签,把相关报错贴上来。
|
||||
|
||||
```
|
||||
// 在此粘贴
|
||||
```
|
||||
|
||||
## 额外信息
|
||||
|
||||
任何其他可能有用的上下文。
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 我已经搜索过 [已有 Issue](https://github.com/shumengya/meme-api/issues?q=is%3Aissue) 确认没人提过
|
||||
- [ ] 如果是素材问题,确认是仓库里 / 站点上都看不到,而不是我本地缓存
|
||||
- [ ] 我没有在 Issue 里包含任何个人敏感信息
|
||||
35
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: 💡 功能建议
|
||||
about: 对图库 / API / 构建流程提出改进建议
|
||||
title: '[Feature] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 这个功能解决什么问题?
|
||||
|
||||
简要说明痛点或动机。例如:
|
||||
- "我每次都要手动选 HTML 宽度 240,希望有记忆"
|
||||
- "想在手机端把图片长按保存"
|
||||
|
||||
## 你想要的方案
|
||||
|
||||
描述你心中的实现方式。可以包括:
|
||||
- UI 截图 / 草图
|
||||
- 交互流程
|
||||
- 对哪些 API / 文件有影响
|
||||
|
||||
## 备选方案
|
||||
|
||||
有没有考虑过别的做法?各自的优缺点?
|
||||
|
||||
## 额外信息
|
||||
|
||||
- 影响的页面 / API:例如 `https://meme.smyhub.com/`、`/memes.json`
|
||||
- 是否愿意自己提 PR:是 / 否 / 待定
|
||||
- 关联 Issue(如果有):#123
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 我已经搜索过 [已有 Issue](https://github.com/shumengya/meme-api/issues?q=is%3Aissue) 确认没人提过
|
||||
|
||||
34
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: ❓ 提问 / 求助
|
||||
about: 不知道怎么用 / 需要帮助
|
||||
title: '[Question] '
|
||||
labels: question
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 我想做什么
|
||||
|
||||
简述你想达到的目的。
|
||||
|
||||
## 我已经试过的
|
||||
|
||||
- 看了 README 里的「...」部分
|
||||
- 搜了 Issues,没找到
|
||||
- ...
|
||||
|
||||
## 困惑点
|
||||
|
||||
具体卡在哪?
|
||||
|
||||
## 截图 / 代码
|
||||
|
||||
如果方便,附上相关截图或代码片段。
|
||||
|
||||
## 环境
|
||||
|
||||
- 浏览器 / 设备
|
||||
- 访问的 URL
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 我已经搜索过 [已有 Issue](https://github.com/shumengya/meme-api/issues?q=is%3Aissue) 确认没人提过
|
||||
54
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
## 描述
|
||||
|
||||
<!-- 一两句话说清这个 PR 干了什么。 -->
|
||||
|
||||
## 关联 Issue
|
||||
|
||||
<!-- 用 `Closes #123` / `Fixes #123` 关联 issue;没关联的写 "无"。 -->
|
||||
|
||||
Closes #
|
||||
|
||||
## 变更类型
|
||||
|
||||
请打勾:
|
||||
|
||||
- [ ] 🐛 Bug 修复(fix)
|
||||
- [ ] ✨ 新功能(feat)
|
||||
- [ ] 🎨 样式 / UI 调整(style)
|
||||
- [ ] ♻️ 重构(refactor)
|
||||
- [ ] ⚡ 性能优化(perf)
|
||||
- [ ] 📝 文档(docs)
|
||||
- [ ] 🔧 构建 / 脚本 / 配置(chore)
|
||||
|
||||
## 改动内容
|
||||
|
||||
<!-- 详细列出做了哪些改动。可以按文件 / 模块分点。 -->
|
||||
|
||||
-
|
||||
-
|
||||
-
|
||||
|
||||
## 涉及的文件
|
||||
|
||||
<!-- 方便 reviewer 快速定位 -->
|
||||
|
||||
- [ ] `public/index.html`
|
||||
- [ ] `scripts/generate-manifest.mjs`
|
||||
- [ ] `转化.py`
|
||||
- [ ] `public/memes.json`(清单,由 `npm run build` 自动重生成)
|
||||
- [ ] `dist/memes.json`(构建产物,请确认未手动修改)
|
||||
- [ ] 文档(README / CHANGELOG / 其他)
|
||||
- [ ] GitHub 配置(Issue 模板 / PR 模板 / .github 目录)
|
||||
|
||||
## 自检清单
|
||||
|
||||
- [ ] 本地 `npm run build` 通过,`public/memes.json` 已重生成
|
||||
- [ ] 本地 `npm run dev` 跑过,浏览器实测没回归
|
||||
- [ ] 移动端(DevTools 切到 375px)布局没崩
|
||||
- [ ] 改动不引入新外部依赖
|
||||
- [ ] 没有把 `dist/`、`node_modules/`、`__pycache__/`、`*.pyc`、`原图/` 里的源文件等 commit 进去
|
||||
- [ ] 我已更新 [CHANGELOG.md](./CHANGELOG.md)(如适用)
|
||||
|
||||
## 截图 / 录屏
|
||||
|
||||
<!-- 如有 UI 改动请附图。 -->
|
||||
67
.gitignore
vendored
@@ -1,7 +1,74 @@
|
||||
# ----- 构建产物 -----
|
||||
dist/
|
||||
node_modules/
|
||||
.wrangler/
|
||||
|
||||
# ----- 临时 / 私密 -----
|
||||
.dev.vars
|
||||
.env
|
||||
.env.local
|
||||
*.local
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# ----- 日志 -----
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# ----- Python -----
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.python-version
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
*.egg-info/
|
||||
|
||||
# ----- 操作系统残留 -----
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# ----- IDE / 编辑器 -----
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*.swn
|
||||
*~
|
||||
|
||||
# ----- 备份 -----
|
||||
*.bak
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
# ----- 工具缓存 -----
|
||||
.eslintcache
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
.turbo/
|
||||
|
||||
# ----- 其它 -----
|
||||
# 视频/音频预览(避免误提交原始素材)
|
||||
*.psd
|
||||
*.ai
|
||||
*.sketch
|
||||
*.fig
|
||||
|
||||
75
ASSET_LICENSE.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 素材使用许可(CC BY-NC-SA 4.0)
|
||||
|
||||
本仓库中**所有表情包图片素材**(包括但不限于 `public/meme/`、`原图/`、`原图-已处理/` 目录下的所有 `.webp`、`.gif`、`.png`、`.jpg` 等图像文件)适用本许可。
|
||||
|
||||
> 本文件**仅约束素材**。本仓库的**代码**、**文档**、**配置文件**适用 [LICENSE](./LICENSE) 中规定的 **MIT 协议**。
|
||||
|
||||
---
|
||||
|
||||
## 中文摘要(仅供参考,完整条款以英文原文为准)
|
||||
|
||||
你可以:
|
||||
|
||||
- ✅ **共享** — 复制、转载、在其他网站 / 博客 / 聊天工具中使用
|
||||
- ✅ **改编** — 二次加工、混剪、做自己的表情包
|
||||
- ✅ **署名** — 需要给原作者 / 本仓库署名
|
||||
|
||||
在以下条件下:
|
||||
|
||||
- 📛 **署名(BY)** — 必须标明原作者 / 来源链接,**并标明是否做了修改**
|
||||
- 🚫🚫 **非商用(NC)** — **不得用于任何商业目的**
|
||||
- 🔁 **相同方式共享(SA)** — 如果你改编了,必须用**同样的 CC BY-NC-SA 4.0** 协议发布
|
||||
|
||||
## 完整法律文本
|
||||
|
||||
本作品采用 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans) 协议许可。
|
||||
|
||||
访问以下链接查看完整条款:
|
||||
|
||||
- 🇨🇳 简体中文:<https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans>
|
||||
- 🇺🇸 English:<https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode>
|
||||
|
||||
## 推荐署名格式
|
||||
|
||||
在使用本仓库的表情包时,建议在可被看见的地方附上以下署名之一:
|
||||
|
||||
```
|
||||
素材来自:meme-api (https://github.com/shumengya/meme-api)
|
||||
许可协议:CC BY-NC-SA 4.0
|
||||
```
|
||||
|
||||
如果你转载的是**单张表情**,可以简化为:
|
||||
|
||||
```
|
||||
来源:shumengya/meme-api (CC BY-NC-SA 4.0)
|
||||
```
|
||||
|
||||
## 关于商用
|
||||
|
||||
CC BY-NC-SA 的 **NC(NonCommercial)** 含义:
|
||||
|
||||
- 🚫 不得用于售卖、付费内容、广告变现、商品印刷等任何**直接或间接**产生商业收益的场景
|
||||
- ✅ 私人聊天、个人博客 / 哔哔 / Telegram 频道、**不盈利**的社交账号可放心使用
|
||||
- ❓ 不确定?默认**视为商用**,或单独联系作者获取书面授权
|
||||
|
||||
如确有商用需求(广告、表情商店、付费内容、商品等),请提 [Issue](https://github.com/shumengya/meme-api/issues) 或直接联系作者协商。
|
||||
|
||||
## 侵权处理
|
||||
|
||||
如果你认为本仓库的某张表情包**侵犯了你的合法权益**(版权、肖像权、商标等),请:
|
||||
|
||||
1. 在仓库提一个 Issue 说明情况(推荐,便于追踪)
|
||||
2. 或通过 GitHub 主页 <https://github.com/shumengya> 找到联系方式
|
||||
|
||||
收到通知后,作者会在合理时间内**核实、删除或调整**相关内容,并对历史仓库做必要的回滚。
|
||||
|
||||
## 关于本协议的选择
|
||||
|
||||
- 为什么不是 MIT?表情包是**艺术作品 / 二次创作**,加上 **NC** 可以避免被无差别商用剽窃,加上 **SA** 可以让衍生作品保持开放。
|
||||
- 为什么不是 CC BY-SA?NC 限制可以保护作者署名权益,避免"我做的素材被人用在付费壁纸 APP 里"。
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>Last updated: 2026-06-04</sub>
|
||||
</div>
|
||||
48
CHANGELOG.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 更新日志
|
||||
|
||||
本项目的所有**重要变更**都会记录在此文件。
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
||||
版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
> 表情包素材的新增 / 删除 **不**计入版本号,详情看 Git 提交历史。
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 计划中
|
||||
- 给图库前端加 lightbox 预览(点击放大、左右切换)
|
||||
- 在图库首页显示每个分类的封面图
|
||||
- 素材缩放阈值参数化(支持多档位 CDN 输出)
|
||||
|
||||
## [1.0.0] - 2026-06-04
|
||||
|
||||
### 新增
|
||||
- 仓库首次**公开**整理:补齐 README、LICENSE、CHANGELOG、Issue / PR 模板
|
||||
- 双协议声明:代码 MIT,表情包素材 CC BY-NC-SA 4.0
|
||||
- 收录 **51 个分类、~2600+ 张**表情包(WebP / GIF)
|
||||
- 图库前端(`public/index.html`):分类侧边栏 + 搜索 + 移动端 4 列 / 桌面 12 列网格
|
||||
- 一键复制 URL / Markdown / HTML(HTML 宽度 96–400px 可记忆)
|
||||
- 一键下载原图
|
||||
- 在线 API 说明页:`https://meme.smyhub.com/readme/`
|
||||
- JSON 公开 API:`/memes.json`(兼容 `/api/v1/memes`),已配置 CORS
|
||||
- Python 一键处理脚本 `转化.py`(Pillow,多线程、动图 WebP→GIF、zip 批处理)
|
||||
- Node 构建脚本 `scripts/generate-manifest.mjs`(仅生成清单,素材不重处理)
|
||||
- Cloudflare Pages 部署支持:Git 集成 + wrangler CLI 双通道
|
||||
|
||||
### 安全 / 性能
|
||||
- 静态站点,**无服务端**,无任何用户数据收集
|
||||
- 所有外链静态资源(fonts、analytics 等)**未引入**
|
||||
- CORS 头只对 `memes.json` 与 `/api/v1/memes` 开启,不影响其他资源
|
||||
|
||||
## 历史
|
||||
|
||||
在 1.0.0 之前,仓库作为个人项目维护,无版本号 / 变更日志。
|
||||
|
||||
[Unreleased]: https://github.com/shumengya/meme-api/compare/main...HEAD
|
||||
[1.0.0]: https://github.com/shumengya/meme-api/releases/tag/v1.0.0
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>本文件由维护者手工维护,欢迎在 PR 中追加。</sub>
|
||||
</div>
|
||||
41
LICENSE
Normal file
@@ -0,0 +1,41 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-2026 shumengya and meme-api contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
NOTICE — 本仓库采用 双重许可 (Dual Licensing)
|
||||
================================================================================
|
||||
|
||||
本 LICENSE 文件**仅适用于本仓库中的源代码部分**,包括但不限于:
|
||||
- HTML / CSS / JavaScript 前端代码
|
||||
- Python 脚本(转化.py 等)
|
||||
- Node.js 脚本(scripts/*.mjs)
|
||||
- Cloudflare Pages 配置文件(_headers、_redirects 等)
|
||||
- 文档(README.md、CHANGELOG.md、CONTRIBUTING.md 等 Markdown 文本)
|
||||
|
||||
本仓库中**表情包素材**(`public/meme/` 目录下的所有 .webp / .gif 文件,
|
||||
以及 `原图/`、`原图-已处理/` 下的所有图片)适用单独的协议,请参阅:
|
||||
|
||||
-> ASSET_LICENSE.md (CC BY-NC-SA 4.0)
|
||||
|
||||
简言之:
|
||||
· 代码:MIT,最宽松可用,包括商用
|
||||
· 素材:CC BY-NC-SA 4.0,署名 + 非商用 + 相同方式共享
|
||||
269
README.md
Normal file
@@ -0,0 +1,269 @@
|
||||
<div align="center">
|
||||
|
||||
# 萌芽表情包图库 meme-api
|
||||
|
||||
一个部署在 **Cloudflare Pages** 的纯静态表情包图库 + 公开 API。
|
||||
打开网页就能翻、一键复制链接 / Markdown / HTML,也支持直链外链和 JSON 接口拉清单。
|
||||
|
||||
[](https://meme.smyhub.com)
|
||||
[](https://meme.smyhub.com/memes.json)
|
||||
[](./LICENSE)
|
||||
[](./ASSET_LICENSE.md)
|
||||
[](https://github.com/shumengya/meme-api/stargazers)
|
||||
[](https://github.com/shumengya/meme-api/network)
|
||||
[](https://github.com/shumengya/meme-api/commits/main)
|
||||
|
||||
[在线浏览](https://meme.smyhub.com) · [API 文档](https://meme.smyhub.com/readme/) · [更新日志](./CHANGELOG.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ✨ 这是什么
|
||||
|
||||
| 端 | 说明 |
|
||||
| --- | --- |
|
||||
| 🌐 **网页图库** | 分类侧边栏 + 移动端 4 列 / 桌面 12 列网格,搜索、暗色模式、键盘可达 |
|
||||
| 📋 **一键复制** | 点开任意表情可复制 URL / Markdown / HTML(HTML 宽度 96–400px 可选) |
|
||||
| ⬇️ **原图下载** | 直接下载原图,文件名保持原编号 |
|
||||
| 🔗 **静态直链** | `/meme/<分类>/<编号>.webp` 可被任何网页、博客、聊天工具直接引用 |
|
||||
| 📜 **JSON API** | `/memes.json`(兼容 `/api/v1/memes`)返回完整清单,**已配置 CORS** |
|
||||
|
||||
> 收录 **51 个分类、约 2600+ 张** WebP / GIF 表情包,全部经过统一处理:最长边缩放至 250px、动图统一存为 GIF。
|
||||
|
||||
## 🖼 截图
|
||||
|
||||
> 在线版:<https://meme.smyhub.com>
|
||||
|
||||
<details>
|
||||
<summary>🖥️ 桌面端(点击展开)</summary>
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 萌芽表情包图库ヘ(=^・ω・^=)ノ 说明 · memes.json │
|
||||
├──────────┬───────────────────────────────────────────────────┤
|
||||
│ 分类 │ 白色小人 · 224 滑稽 · 282 金馆长熊猫 · 304 │
|
||||
│ 🔍 搜索… │ 抹茶旦旦 · 227 抹茶旦旦2 · 120 … │
|
||||
│ 全部 │ ┌────┬────┬────┬────┬────┬────┬────┬────┐ │
|
||||
│ 白色小人 │ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ … │ │
|
||||
│ 白色小人2 │ └────┴────┴────┴────┴────┴────┴────┴────┘ │
|
||||
│ … │ (12 列 × 自动行,点击任意缩略图弹出操作面板) │
|
||||
└──────────┴───────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>📱 移动端(点击展开)</summary>
|
||||
|
||||
```
|
||||
┌────────────────────────┐
|
||||
│ 萌芽表情包图库 … │
|
||||
├────────────────────────┤
|
||||
│ ☰ 分类 │
|
||||
│ 白色小人 · 224 │
|
||||
│ 滑稽 · 282 │
|
||||
│ 金馆长熊猫 · 304 │
|
||||
│ … │
|
||||
├────────────────────────┤
|
||||
│ ┌──┬──┬──┬──┐ │
|
||||
│ │1 │2 │3 │4 │ (4 列) │
|
||||
│ ├──┼──┼──┼──┤ │
|
||||
│ │5 │6 │7 │8 │ │
|
||||
│ └──┴──┴──┴──┘ │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 在线使用
|
||||
|
||||
直接打开 <https://meme.smyhub.com>,分类、搜索、复制随便玩。
|
||||
|
||||
### 引用单张表情
|
||||
|
||||
任意 HTML / Markdown / 聊天工具里粘贴:
|
||||
|
||||
```markdown
|
||||
# Markdown
|
||||

|
||||
```
|
||||
|
||||
```html
|
||||
<!-- HTML(宽度 240px,自适应) -->
|
||||
<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" />
|
||||
```
|
||||
|
||||
> 路径里的中文 `分类` 名必须 URL 编码;图库的「复制链接」按钮已帮你编好。
|
||||
|
||||
### 拉取完整清单
|
||||
|
||||
```bash
|
||||
curl -sS "https://meme.smyhub.com/memes.json"
|
||||
# 老接口仍兼容:
|
||||
curl -sS "https://meme.smyhub.com/api/v1/memes"
|
||||
```
|
||||
|
||||
返回结构(节选):
|
||||
|
||||
```json
|
||||
{
|
||||
"generatedAt": "2026-05-19T13:01:45.229Z",
|
||||
"categories": [
|
||||
{
|
||||
"id": "白色小人",
|
||||
"name": "白色小人",
|
||||
"items": [
|
||||
{ "file": "1.webp", "path": "白色小人/1.webp" },
|
||||
{ "file": "2.webp", "path": "白色小人/2.webp" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
拼接规则:`{BASE}/meme/{category.id}/{item.file}`。
|
||||
|
||||
## 🛠 本地开发
|
||||
|
||||
> 需要 **Node.js ≥ 18** 和 **Python ≥ 3.8**(仅当你需要重新处理图片时才用得到 Python)。
|
||||
|
||||
```bash
|
||||
# 1. 克隆
|
||||
git clone https://github.com/shumengya/meme-api.git
|
||||
cd meme-api
|
||||
|
||||
# 2. 安装依赖(只需要 wrangler,体积很小)
|
||||
npm install
|
||||
|
||||
# 3. 构建(生成 dist/,含 memes.json)
|
||||
npm run build
|
||||
|
||||
# 4. 本地预览(http://localhost:8788)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
可选的构建变体:
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `npm run build` | 默认构建。清单优先扫描 `public/meme/`(已拷入的成品),否则回退 `原图-已处理/` / `原图/` |
|
||||
| `npm run build:manifest-from-source` | **强制**按 `原图-已处理/`(无则 `原图/`)生成清单,不看 `public/meme/` |
|
||||
| `npm run dev` | 构建 + 起 `serve` 静态服务在 8788 端口 |
|
||||
| `npm run deploy:preview` | 用 `wrangler` 部署到 Cloudflare Pages **预览**分支 |
|
||||
| `npm run deploy:upload` | 用 `wrangler` 部署到 Cloudflare Pages **生产**分支(仅直连 CLI 的项目用) |
|
||||
|
||||
## 📦 添加 / 处理新表情
|
||||
|
||||
`public/meme/` 里已经是**可直接部署的成品**(已编号、已缩放、动图已是 GIF)。
|
||||
当你拿到新的原图想加入,按下面流程走:
|
||||
|
||||
```bash
|
||||
# 1. 把未编号的源素材丢进 原图/<分类名>/
|
||||
mkdir -p 原图/我的新分类
|
||||
cp ~/Downloads/xxx.png 原图/我的新分类/
|
||||
|
||||
# 2. 装依赖(一次性)
|
||||
python -m pip install pillow
|
||||
|
||||
# 3. 转换:自动编号、统一 webp / gif、最长边缩放到 250px
|
||||
python 转化.py
|
||||
# 产物落到 原图-已处理/,原图默认保留;想清源可加 --delete-sources
|
||||
|
||||
# 4. 拷入发布目录
|
||||
cp -r 原图-已处理/我的新分类 public/meme/
|
||||
|
||||
# 5. 重新生成清单
|
||||
npm run build
|
||||
```
|
||||
|
||||
> `转化.py` 的细节(多线程、zip 批处理、WebP→GIF 动图等)见脚本顶部 docstring。
|
||||
|
||||
## 🚢 部署到 Cloudflare Pages
|
||||
|
||||
### 方式 A:连接 Git(推荐)
|
||||
|
||||
1. 在 Cloudflare 控制台创建 Pages 项目,连接本仓库 `main` 分支
|
||||
2. 构建设置:
|
||||
- **构建命令**:`npm run build`
|
||||
- **构建输出目录**:`dist`
|
||||
3. 每次 `git push` 都会自动触发部署
|
||||
|
||||
### 方式 B:CLI 直传
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npx wrangler login # 第一次需要登录
|
||||
npm run deploy:upload # 部署到生产
|
||||
# 或
|
||||
npm run deploy:preview # 部署到 preview 分支
|
||||
```
|
||||
|
||||
> Git 项目里 `wrangler pages deploy` 通常会落到**预览**环境,属正常。
|
||||
|
||||
## 🗂 项目结构
|
||||
|
||||
```
|
||||
meme-api/
|
||||
├── 原图/ # 源素材(未编号、可能未处理)
|
||||
├── 转化.py # Python 一键处理脚本(Pillow)
|
||||
├── scripts/
|
||||
│ └── generate-manifest.mjs # 生成 memes.json + 复制 public/ -> dist/
|
||||
├── public/ # 构建源(直接作为站点根被复制)
|
||||
│ ├── index.html # 图库前端(单文件,零依赖)
|
||||
│ ├── _headers # CORS 等响应头
|
||||
│ ├── _redirects # 兼容旧路径(/memes -> /)
|
||||
│ ├── memes.json # 由脚本生成的清单
|
||||
│ ├── meme/ # 处理后的素材(已编号 .webp / .gif)
|
||||
│ └── readme/index.html # 在线 API 说明页
|
||||
├── dist/ # 构建产物(git 忽略)
|
||||
├── package.json
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
## ⚙️ 配置
|
||||
|
||||
| 位置 | 作用 |
|
||||
| --- | --- |
|
||||
| `public/_headers` | `memes.json` 和 `/api/v1/memes` 的 CORS 头(已开启 `*`) |
|
||||
| `public/_redirects` | `/api/v1/memes` → `/memes.json`;`/memes{,/}` → `/` |
|
||||
| `scripts/generate-manifest.mjs` | 清单生成策略、最大文件格式(`.webp/.gif`)、输出目录 |
|
||||
|
||||
如果想换 CDN 域名,可以在前端 JS 里给 `memes.json` 加 `baseUrl` 字段(详见脚本注释)。
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
见 [CHANGELOG.md](./CHANGELOG.md)。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本仓库**双重许可**,请分别阅读:
|
||||
|
||||
| 内容 | 协议 | 链接 |
|
||||
| --- | --- | --- |
|
||||
| **代码**(HTML / JS / Python / Node 脚本、`_headers`、`_redirects` 等) | MIT | [LICENSE](./LICENSE) |
|
||||
| **表情包素材**(`public/meme/` 下的所有 `.webp` / `.gif` 文件) | CC BY-NC-SA 4.0 | [ASSET_LICENSE.md](./ASSET_LICENSE.md) |
|
||||
|
||||
简言之:代码随便用、商用随便;素材**署名 + 非商用 + 相同方式共享**。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
- [Cloudflare Pages](https://pages.cloudflare.com/) 提供静态托管
|
||||
- [Pillow](https://python-pillow.org/) 提供图片处理能力
|
||||
- [Cloudflare Pages](https://pages.cloudflare.com/) 提供静态托管
|
||||
- [Pillow](https://python-pillow.org/) 提供图片处理能力
|
||||
|
||||
## 📬 联系
|
||||
|
||||
- Issue / PR:<https://github.com/shumengya/meme-api/issues>
|
||||
- 主页:<https://github.com/shumengya>
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>本项目仅作表情包整理与分享;如素材涉及版权或肖像权问题,请提 Issue 或邮件联系,会第一时间处理。</sub>
|
||||
</div>
|
||||
1030
Typora_Hook_Log.txt
Normal file
186
blog-post.md
Normal 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
|
||||

|
||||
```
|
||||
|
||||
```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/)
|
||||
@@ -1 +1,3 @@
|
||||
/api/v1/memes /memes.json 200
|
||||
/memes / 301
|
||||
/memes/ / 301
|
||||
|
||||
@@ -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 / HTML,51 个分类共 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/<分类>/</code>,对应 URL:</p>
|
||||
<p><code>GET /meme/<分类>/<文件名>.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>原图/<分类>/</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, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<");
|
||||
}
|
||||
|
||||
function markdownImage(url, alt) {
|
||||
var a = String(alt || "表情").replace(/\]/g, "");
|
||||
return "";
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
BIN
public/meme/QQ表情包/1.gif
Normal file
|
After Width: | Height: | Size: 420 KiB |
BIN
public/meme/QQ表情包/2.gif
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
BIN
public/meme/emoji/1765198807110.gif
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/meme/emoji/1765198816044.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 476 KiB After Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
BIN
public/meme/互联网厂/5.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/meme/互联网厂/6.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/meme/互联网厂/7.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/meme/人机验证/0.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/meme/人机验证/1.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/meme/人机验证/10.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/meme/人机验证/11.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/meme/人机验证/12.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/meme/人机验证/13.webp
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/meme/人机验证/14.webp
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/meme/人机验证/15.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/meme/人机验证/16.webp
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/meme/人机验证/17.webp
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/meme/人机验证/2.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/meme/人机验证/3.webp
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/meme/人机验证/4.webp
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/meme/人机验证/5.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/meme/人机验证/6.webp
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/meme/人机验证/7.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/meme/人机验证/8.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/meme/人机验证/9.webp
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
public/meme/初音猫猫/0.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/meme/初音猫猫/1.webp
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
public/meme/初音猫猫/10.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/meme/初音猫猫/11.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/meme/初音猫猫/12.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/meme/初音猫猫/13.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/meme/初音猫猫/14.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/meme/初音猫猫/15.webp
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
public/meme/初音猫猫/16.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/meme/初音猫猫/17.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/meme/初音猫猫/18.webp
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/meme/初音猫猫/19.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/meme/初音猫猫/2.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/meme/初音猫猫/20.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/meme/初音猫猫/21.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/meme/初音猫猫/22.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/meme/初音猫猫/23.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/meme/初音猫猫/24.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/meme/初音猫猫/25.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/meme/初音猫猫/26.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/meme/初音猫猫/27.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/meme/初音猫猫/28.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/meme/初音猫猫/29.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/meme/初音猫猫/3.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/meme/初音猫猫/30.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/meme/初音猫猫/31.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/meme/初音猫猫/32.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/meme/初音猫猫/33.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/meme/初音猫猫/34.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/meme/初音猫猫/35.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/meme/初音猫猫/36.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/meme/初音猫猫/37.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/meme/初音猫猫/38.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/meme/初音猫猫/39.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/meme/初音猫猫/4.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |