├── .vscode └── settings.json ├── README.md ├── _routes.json ├── css ├── style.css └── 未压缩-style.css ├── functions ├── _middleware.js ├── _telegram.js ├── _vars │ └── [name].js ├── _wecom.js ├── clear-all.js ├── contents.js ├── contents │ └── [id].js ├── files.js ├── files │ ├── [filename].js │ ├── [name].js │ └── upload.js ├── images.js └── images │ └── [name].js ├── git_delete.bat ├── git_upload.bat ├── index.html ├── js ├── main.js ├── theme.js ├── 当前的js.js ├── 未压缩-main.js └── 未压缩-theme.js ├── logo.jpg ├── package.json ├── postcss.config.js ├── schema.sql └── wrangler.toml /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drop中转站 2 | 3 | Drop中转站是一个基于 Cloudflare Pages 的多功能内容分享平台,支持文本、代码、诗歌、图片和文件的在线分享和存储。 4 | 5 | 部署教程:看博客[https://git-blog-share.1143520.xyz/posts/cf-drop-paste.pdf](https://git-blog-share.1143520.xyz/posts/cf-drop-paste.pdf) 6 | 7 | 密码验证和Telegram通知功能:看下面设置变量 8 | 9 | ## 功能特点 10 | 11 | - 多种内容类型支持: 12 | - 普通文本:支持markdown格式 13 | - 代码:自动语法高亮 14 | - 诗歌:优雅排版展示 15 | - 图片:支持预览和下载 16 | - 文件:支持所有类型文件的上传和下载 17 | - 美观的界面设计: 18 | - 响应式布局,完美适配手机和电脑 19 | - 优雅的中文字体渲染 20 | - 简洁现代的界面风格 21 | - 支持深色/浅色主题切换 22 | - 便捷的操作体验: 23 | - 一键添加新内容 24 | - 拖拽上传文件 25 | - 实时预览效果 26 | - 快速编辑和删除 27 | - 安全特性: 28 | - 可选的密码保护 29 | - 密码验证有效期15天 30 | - 未验证时内容模糊显示 31 | - Telegram 通知: 32 | - 可选的 Telegram 群组通知 33 | - 支持文本、图片、文件等多种类型 34 | - 美观的 HTML 格式消息 35 | - 自动同步所有新上传内容 36 | 37 | ## Markdown 支持 38 | 39 | 本站支持完整的 Markdown 语法,包括: 40 | 41 | 1. **基础语法**: 42 | - 标题(H1-H6) 43 | - 粗体、斜体 44 | - 有序/无序列表 45 | - 引用块 46 | - 代码块和行内代码 47 | - 链接和图片 48 | - 水平分割线 49 | 50 | 2. **扩展语法**: 51 | - 表格 52 | - 任务列表 53 | - 删除线 54 | - 脚注 55 | - 上标和下标 56 | - Emoji 表情 :smile: 57 | 58 | 3. **代码高亮**: 59 | ```javascript 60 | console.log('支持多种编程语言的语法高亮'); 61 | ``` 62 | 63 | 4. **数学公式**: 64 | - 行内公式:$E=mc^2$ 65 | - 块级公式: 66 | $$ 67 | \frac{n!}{k!(n-k)!} = \binom{n}{k} 68 | $$ 69 | 70 | ## 密码保护功能 71 | 72 | ### 设置密码保护 73 | 74 | 1. 在 Cloudflare Pages 的环境变量中设置: 75 | - 变量名:`ACCESS_PASSWORD` 76 | - 变量值:你想设置的密码 77 | - 设置位置:Cloudflare Dashboard > Pages > 你的项目名 > Settings > Environment variables 78 | 79 | 2. 密码保护特性: 80 | - 未设置 `ACCESS_PASSWORD` 时默认无密码保护 81 | - 密码验证成功后有效期为15天 82 | - 验证有效期存储在浏览器本地存储中 83 | - 切换浏览器或清除浏览器数据需重新验证 84 | 85 | 3. 安全提示: 86 | - 密码验证在前端进行,适用于简单的访问控制 87 | - 不建议存储高度敏感的信息 88 | - 如需更高安全性,建议使用专业的加密存储服务 89 | 90 | ### Telegram 通知功能 91 | 92 | 1. 配置方法: 93 | 在 Cloudflare Pages 的环境变量中设置: 94 | - `TG_BOT_TOKEN`: 你的 Telegram Bot Token 95 | - `TG_CHAT_ID`: 你的 Telegram 群组 ID 96 | - 设置位置:同上 97 | 98 | 2. 功能特性: 99 | - 未设置变量时默认不发送通知 100 | - 支持 HTML 格式的富文本消息 101 | - 自动处理消息长度限制(最大4096字符) 102 | - 代码内容保持格式化显示 103 | 104 | 3. 通知类型: 105 | 106 | a. 新内容上传: 107 | ``` 108 | 新[类型]上传 109 | 110 | 标题: xxx 111 | 内容/链接: xxx 112 | ``` 113 | 114 | b. 内容更新: 115 | ``` 116 | 内容已更新 117 | 118 | 标题: xxx 119 | 内容/链接: xxx 120 | 121 | 此内容已被编辑 122 | ``` 123 | 124 | c. 内容删除: 125 | ``` 126 | 🗑 内容已删除 127 | 128 | 类型: xxx 129 | 标题: xxx 130 | 131 | 此内容已被永久删除 132 | ``` 133 | 134 | d. 清空所有内容: 135 | ``` 136 | 🗑 内容已全部清空 137 | 138 | 清空内容: 139 | - 数据库记录: xx 条 140 | - 图片文件: xx 个 141 | - 其他文件: xx 个 142 | 143 | 所有内容已被永久删除 144 | ``` 145 | 146 | 4. 内容展示特点: 147 | - 文本内容:直接显示文本 148 | - 代码:使用 `
` 格式化显示
149 |    - 图片:显示可访问的链接
150 |    - 文件:显示下载链接
151 |    - 超长内容自动截断并添加提示
152 | 
153 | 5. 使用提示:
154 |    - Bot 需要已被添加到目标群组
155 |    - Bot 需要具有发送消息的权限
156 |    - 群组 ID 必须为数字格式
157 |    - 建议先在测试群组中验证功能
158 | 
159 | 6. 安全提示:
160 |    - 不要在消息中包含敏感信息
161 |    - Bot Token 不要泄露给他人
162 |    - 可以限制 Bot 的权限
163 |    - 建议使用私有群组
164 | 
165 | 7. 消息格式说明:
166 |    - 标题使用粗体 `` 标签
167 |    - 说明文字使用斜体 `` 标签
168 |    - 代码使用 `
` 标签
169 |    - 支持基本的 HTML 格式化
170 |    - 支持 emoji 表情符号
171 | 
172 | 8. 调试建议:
173 |    - 检查环境变量是否正确设置
174 |    - 确认 Bot 是否在群组中
175 |    - 查看 Cloudflare Pages 的日志
176 |    - 测试不同类型内容的通知效果
177 | 
178 | ### 企业微信机器人通知功能
179 | 
180 | 1. 配置方法:
181 |    在 Cloudflare Pages 的环境变量中设置:
182 |    - `WECOM_BOT_URL`: 企业微信机器人的完整 Webhook 地址
183 |    - 设置位置:Cloudflare Dashboard > Pages > 你的项目名 > Settings > Environment variables
184 | 
185 | 2. 功能特性:
186 |    - 未设置变量时默认不发送通知
187 |    - 使用纯文本格式,适合在手机端查看
188 |    - 自动处理消息长度限制(最大2048字符)
189 |    - 使用 emoji 让消息更生动
190 | 
191 | 3. 通知类型:
192 | 
193 |    新内容上传:
194 |    ```
195 |    📝 新内容上传
196 | 
197 |    📌 标题: xxx
198 | 
199 |    💬 内容:
200 | 
201 |    这是内容文本
202 |    ```
203 | 
204 |    或者对于图片/文件:
205 |    ```
206 |    🖼️ 新图片上传
207 | 
208 |    📌 标题: xxx
209 | 
210 |    🔗 链接: https://example.com/image.jpg
211 |    ```
212 | 
213 | 4. 内容展示特点:
214 |    - 文本内容:直接显示文本
215 |    - 代码:作为纯文本显示
216 |    - 图片:显示可访问的链接
217 |    - 文件:显示下载链接
218 |    - 超长内容自动截断并添加省略号
219 | 
220 | 5. 使用提示:
221 |    - 需要先在企业微信群中添加机器人
222 |    - 复制机器人的 Webhook 地址
223 |    - 建议先在测试群组中验证功能
224 | 
225 | 6. 安全提示:
226 |    - Webhook 地址不要泄露给他人
227 |    - 建议使用私有群组
228 |    - 定期更新 Webhook 地址
229 | 
230 | 7. 消息格式说明:
231 |    - 使用 emoji 标识不同类型的内容
232 |    - 每个部分之间有空行分隔
233 |    - 保持简洁清晰的格式
234 | 
235 | 8. 调试建议:
236 |    - 检查环境变量是否正确设置
237 |    - 确认 Webhook 地址是否有效
238 |    - 查看 Cloudflare Pages 的日志
239 |    - 测试不同类型内容的通知效果
240 | 
241 | ## 技术架构
242 | 
243 | - 前端:
244 |   - HTML5 + CSS3 + JavaScript
245 |   - Prism.js 用于代码高亮
246 |   - Markdown-it 用于 Markdown 渲染
247 |   - Medium-zoom 用于图片预览
248 | - 后端:
249 |   - Cloudflare Pages 托管静态资源
250 |   - Cloudflare Workers 处理动态请求
251 |   - Cloudflare D1 SQLite 数据库存储内容
252 |   - Cloudflare KV 存储图片和文件
253 | - 核心功能:
254 |   - 数据库表结构自动创建和管理
255 |   - 文件和图片的 KV 存储管理
256 |   - CORS 跨域支持
257 |   - 可选的密码保护功能
258 |   - 可选的 Telegram 通知集成
259 |   - 高效的缓存和内存管理机制
260 | 
261 | ## 缓存和内存管理
262 | 
263 | 1. **内容缓存机制**:
264 |    - 使用 localStorage 存储内容缓存
265 |    - 缓存有效期为15天
266 |    - 缓存包含完整的内容数据
267 |    - 自动检测内容更新并刷新缓存
268 | 
269 | 2. **同步间隔设置**:
270 |    - 默认同步间隔为30秒
271 |    - 同步间隔配置缓存在 localStorage
272 |    - 同步间隔缓存有效期为7天
273 |    - 支持服务端配置自定义同步间隔
274 | 
275 | 3. **密码验证缓存**:
276 |    - 密码验证状态缓存15天
277 |    - 使用 localStorage 存储验证状态
278 |    - 包含验证时间和过期时间
279 |    - 自动检查验证状态有效性
280 | 
281 | 4. **内存管理优化**:
282 |    - 维护内存中的内容缓存数组
283 |    - 记录最后更新时间戳
284 |    - 定期检查内容更新
285 |    - 页面不可见时暂停同步
286 |    - 页面可见时恢复同步
287 |    - 浏览器关闭前自动清理
288 | 
289 | 5. **性能优化策略**:
290 |    - 避免频繁的服务器请求
291 |    - 优先使用本地缓存数据
292 |    - 增量更新变化的内容
293 |    - 智能的缓存过期处理
294 |    - 按需加载和更新数据
295 | 
296 | ## 项目结构
297 | 
298 | ```
299 | .
300 | ├── index.html                    # 主页面
301 | ├── css/                         # 样式文件
302 | │   └── style.css               # 主样式文件
303 | ├── js/                         # JavaScript文件
304 | │   ├── main.js                # 主逻辑文件
305 | │   └── theme.js               # 主题切换功能
306 | ├── functions/                  # Cloudflare Functions
307 | │   ├── _middleware.js         # 中间件:数据库初始化和CORS
308 | │   ├── _telegram.js          # Telegram 通知功能
309 | │   ├── _vars/               # 环境变量访问
310 | │   │   └── [name].js       # 环境变量获取API
311 | │   ├── contents.js          # 内容列表管理
312 | │   ├── contents/           # 内容详细操作
313 | │   │   └── [id].js        # 单个内容的CRUD
314 | │   ├── images.js          # 图片上传和列表
315 | │   ├── images/           # 图片详细操作
316 | │   │   └── [name].js    # 单个图片的处理
317 | │   ├── files.js         # 文件管理
318 | │   ├── files/          # 文件详细操作
319 | │   │   ├── upload.js  # 文件上传处理
320 | │   │   └── [name].js  # 单个文件的处理
321 | │   └── clear-all.js   # 清空所有数据
322 | ├── _routes.json       # API路由配置
323 | └── package.json      # 项目配置和依赖
324 | ```
325 | 
326 | ### 关键文件说明
327 | 
328 | 1. **核心配置文件**:
329 |    - `_routes.json`: 定义所有API路由和访问权限
330 |    - `package.json`: 项目依赖和命令配置
331 | 
332 | 2. **前端文件**:
333 |    - `index.html`: 主页面和UI组件
334 |    - `style.css`: 样式定义,包括深色模式
335 |    - `main.js`: 主要业务逻辑
336 |    - `theme.js`: 主题切换功能
337 | 
338 | 3. **后端功能**:
339 |    - `_middleware.js`: 
340 |      - 数据库初始化
341 |      - 表结构创建
342 |      - CORS 处理
343 |    - `_telegram.js`:
344 |      - Telegram 消息格式化
345 |      - 通知发送逻辑
346 |    - `contents.js` 和 `contents/[id].js`:
347 |      - 内容的增删改查
348 |      - 数据库操作
349 |    - `images.js` 和 `files.js`:
350 |      - 文件上传处理
351 |      - KV 存储管理
352 |    - `clear-all.js`:
353 |      - 数据清理
354 |      - 存储空间管理
355 | 
356 | ## 使用教程
357 | 
358 | ### 1. 添加新内容
359 | 
360 | 1. 点击页面顶部的"添加新内容"按钮
361 | 2. 在弹出的对话框中选择内容类型:
362 |    - 普通文本:直接输入或粘贴文本内容
363 |    - 代码:输入代码,支持自动语法高亮
364 |    - 诗歌:按诗歌格式排版输入
365 |    - 图片:点击上传或拖拽图片文件
366 |    - 文件:点击上传或拖拽任意类型文件
367 | 3. 填写标题
368 | 4. 点击"保存"按钮完成添加
369 | 
370 | ### 2. 编辑内容
371 | 
372 | 1. 找到要编辑的内容卡片
373 | 2. 点击右上角的编辑图标
374 | 3. 在弹出的对话框中修改内容
375 | 4. 点击"保存"保存修改
376 | 
377 | ### 3. 删除内容
378 | 
379 | 1. 找到要删除的内容卡片
380 | 2. 点击右上角的删除图标
381 | 3. 确认删除操作
382 | 
383 | ### 4. 下载文件/图片
384 | 
385 | 1. 找到要下载的文件/图片卡片
386 | 2. 点击下载图标或文件名即可下载
387 | 
388 | ## 部署教程
389 | 
390 | ### 1. 准备工作
391 | 
392 | 1. 注册 Cloudflare 账号
393 | 2. 安装 Node.js 和 npm
394 | 3. 安装 Wrangler CLI:
395 |    ```bash
396 |    npm install -g wrangler
397 |    ```
398 | 
399 | ### 2. 本地开发
400 | 
401 | 1. 克隆项目:
402 |    ```bash
403 |    git clone <项目地址>
404 |    cd dropbox
405 |    ```
406 | 
407 | 2. 安装依赖:
408 |    ```bash
409 |    npm install
410 |    ```
411 | 
412 | 3. 配置环境:
413 |    ```bash
414 |    # 登录到 Cloudflare
415 |    wrangler login
416 |    
417 |    # 创建 D1 数据库
418 |    wrangler d1 create drop-db
419 |    
420 |    # 创建 KV 命名空间
421 |    wrangler kv:namespace create IMAGES
422 |    wrangler kv:namespace create FILES
423 |    ```
424 | 
425 | 4. 初始化数据库:
426 |    ```bash
427 |    wrangler d1 execute drop-db --file=./schema.sql
428 |    ```
429 | 
430 | 5. 启动开发服务器:
431 |    ```bash
432 |    npm run dev
433 |    ```
434 | 
435 | ### 3. 部署到生产环境
436 | 
437 | 1. 在 Cloudflare Pages 中创建新项目
438 | 2. 连接 GitHub 仓库
439 | 3. 配置构建设置:
440 |    - 构建命令:`npm run build`
441 |    - 输出目录:`/`
442 | 4. 配置环境变量:
443 |    - `DB`: D1 数据库绑定
444 |    - `IMAGES`: KV 命名空间绑定
445 |    - `FILES`: KV 命名空间绑定
446 |    - `SYNC_INTERVAL`: 内容同步间隔(毫秒),例如:
447 |      - 30秒:设置为 `30000`
448 |      - 1分钟:设置为 `60000`
449 |      - 5分钟:设置为 `300000`
450 |      - 注意:最小值为5000(5秒)
451 | 5. 部署:
452 |    ```bash
453 |    npm run deploy
454 |    ```
455 | 
456 | ### 4. 配置说明
457 | 
458 | #### 同步间隔设置
459 | - 默认值:30秒(30000毫秒)
460 | - 修改方法:在 Cloudflare Pages 的环境变量中设置 `SYNC_INTERVAL`
461 | - 设置位置:Cloudflare Dashboard > Pages > 你的项目 > Settings > Environment variables
462 | - 生效时间:修改后用户刷新页面即可生效
463 | - 注意事项:
464 |   - 值必须大于等于5000(5秒)
465 |   - 较短的同步间隔会增加API请求频率
466 |   - 建议根据实际需求和免费额度(10万次/天)合理设置
467 |   - 计算公式:`(24小时 * 3600秒) / (同步间隔数) = 每天请求次数/用户`
468 | 
469 | ## 常见问题
470 | 
471 | 1. **Q: 上传文件大小有限制吗?**
472 |    A: 是的,单个文件最大支持 25MB。
473 | 
474 | 2. **Q: 支持哪些代码语言的高亮?**
475 |    A: 支持所有主流编程语言,包括但不限于:JavaScript、Python、Java、C++、Go等。
476 | 
477 | 3. **Q: 如何备份数据?**
478 |    A: 可以通过 Cloudflare D1 的导出功能备份数据库,KV 存储的文件需要单独下载备份。
479 | 
480 | ## 技术支持
481 | 
482 | 如果遇到问题或需要帮助,可以:
483 | 1. 提交 GitHub Issue
484 | 2. 查看 Cloudflare 官方文档
485 | 3. 参考代码注释
486 | 
487 | ## 开源协议
488 | 
489 | 本项目采用 MIT 协议开源。
490 | 
491 | ## CDN 配置说明
492 | 
493 | 本项目使用了多个 CDN 来加载外部资源,主要包括以下几个:
494 | 
495 | ### 1. JSD 镜像(jsd.cdn.zzko.cn)
496 | 主要用于加载 Prism.js 核心文件:
497 | ```html
498 | 
499 | ```
500 | 
501 | ### 2. Zstatic CDN(s4.zstatic.net)
502 | 用于加载大部分 JavaScript 库和 CSS 文件:
503 | 
504 | ```html
505 | 
506 | 
507 | 
508 | 
509 | 
510 | 
511 | 
512 | 
513 | 
514 | 
515 | 
516 | 
517 | 
518 | 
519 | 
520 | 
521 | 
522 | 
523 | 
524 | 
525 | 
526 | 
527 | 
528 | 
529 | 
530 | 
531 | 
532 | 
533 | 
534 | ```
535 | 
536 | ### CDN 切换说明
537 | 
538 | 如果需要切换 CDN 源,可以考虑以下备选方案:
539 | 
540 | 1. **jsDelivr**
541 | ```html
542 | 
543 | 
544 | ```
545 | 
546 | 2. **UNPKG**
547 | ```html
548 | 
549 | 
550 | ```
551 | 
552 | 3. **cdnjs**
553 | ```html
554 | 
555 | 
556 | ```
557 | 
558 | ### CDN 选择建议
559 | 
560 | 1. **国内访问**:
561 |    - 优先使用南科大镜像或 Zstatic
562 |    - 备选:字节跳动静态资源公共库
563 | 
564 | 2. **国外访问**:
565 |    - 优先使用 jsDelivr 或 cdnjs
566 |    - 备选:UNPKG
567 | 
568 | 3. **自定义域名**:
569 |    - 可以使用 Cloudflare Workers 自建 CDN
570 |    - 支持自定义缓存策略和访问控制
571 | 
572 | 4. **注意事项**:
573 |    - 建议保留多个备用 CDN 源
574 |    - 可以通过 JavaScript 动态检测 CDN 可用性
575 |    - 关键资源建议本地部署备份


--------------------------------------------------------------------------------
/_routes.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "version": 1,
 3 |   "include": ["/*"],
 4 |   "exclude": [],
 5 |   "functions": {
 6 |     "/contents": {
 7 |       "function": "contents",
 8 |       "method": ["GET", "POST", "OPTIONS"]
 9 |     },
10 |     "/contents/*": {
11 |       "function": "contents/[id]",
12 |       "method": ["GET", "PUT", "DELETE", "OPTIONS"]
13 |     },
14 |     "/images": {
15 |       "function": "images",
16 |       "method": ["POST", "OPTIONS"]
17 |     },
18 |     "/images/*": {
19 |       "function": "images",
20 |       "method": ["GET", "OPTIONS"]
21 |     },
22 |     "/_vars/*": {
23 |       "function": "_vars/[name]",
24 |       "method": ["GET", "OPTIONS"]
25 |     },
26 |     "/files/upload": {
27 |       "function": "files/upload",
28 |       "method": ["POST", "OPTIONS"]
29 |     },
30 |     "/files/*": {
31 |       "function": "files",
32 |       "method": ["GET", "OPTIONS"]
33 |     },
34 |     "/clear-all": {
35 |       "function": "clear-all",
36 |       "method": ["POST", "OPTIONS"]
37 |     }
38 |   }
39 | } 


--------------------------------------------------------------------------------
/css/style.css:
--------------------------------------------------------------------------------
1 | .image img,img{height:auto}.empty,.image,.image-preview,.poetry,.poetry p,header{text-align:center}header,pre{padding:1rem}.loading,.modal{position:fixed;top:0;left:0;height:100%;width:100%}.prose p,body{line-height:1.6}#editImage,.btn,.theme-toggle{cursor:pointer}.image img,img{max-width:100%}*{margin:0;padding:0;box-sizing:border-box;scrollbar-width:none;-ms-overflow-style:none;transition:background-color .3s,color .1s,border-color .3s,box-shadow .3s}::-webkit-scrollbar{display:none}.add-new-btn,.add-new-content,.btn,.btn-add,button[type=submit]{display:inline-flex;align-items:center;justify-content:center;padding:.5rem 1rem;border-radius:6px;font-size:.875rem;font-weight:500;min-width:4rem;border:none;cursor:pointer;color:#fff!important;background-color:#333!important;transition:.2s;opacity:1!important}.add-new-content,.btn-primary,.btn-save,button[type=submit]{background-color:#333!important;color:#fff!important;font-weight:600;border:1px solid #613e7f;padding:.625rem 1.25rem;box-shadow:0 2px 4px rgba(0,0,0,.1);opacity:1!important}.loading{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:9999}body.loaded .loading{display:none}.btn-copy{background-color:#9333ea!important}.btn-download{background-color:#0284c7!important}.btn-edit{background-color:#059669!important}.btn-delete{background-color:#dc2626!important}.btn-cancel,.btn-secondary{background-color:#6b7280!important}.add-new-content:hover,.btn-primary:hover,.btn-save:hover,.btn:hover,button[type=submit]:hover{transform:translateY(-1px);filter:brightness(1.1);box-shadow:0 4px 6px rgba(0,0,0,.15)}.btn:disabled{background-color:#bdc3c7!important;transform:none;cursor:not-allowed}:root{--font-sans:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif;--font-serif:"Noto Serif","Noto Serif CJK SC","Source Han Serif SC","Source Han Serif CN","Songti SC",STSong,"AR PL New Sung","AR PL SungtiL GB",NSimSun,SimSun,"TW\-Sung","WenQuanYi Bitmap Song","AR PL UMing CN","AR PL UMing HK","AR PL UMing TW","AR PL UMing TW MBE",serif;--font-mono:"Fira Code","Noto Sans Mono CJK SC","Source Code Pro",Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace;--bg-color:#f5f5f5;--text-color:#333;--grid-color:rgba(0, 0, 0, 0.84);--card-bg:#ffffff;--card-inner-bg:#f8f8f8;--card-shadow:rgba(0, 0, 0, 0.08);--heading-color:#2c3e50;--border-color:#e8e8e8;--input-bg:#ffffff;--input-border:#d0d0d0;--input-focus-border:#3498db;--input-text:#333;--input-placeholder:#999;--file-bg:var(--card-inner-bg);--file-border:var(--border-color);--file-text:#2c3e50;--file-subtext:#7f8c8d;--code-bg:var(--card-inner-bg);--modal-bg:#ffffff;--modal-overlay:rgba(0, 0, 0, 0.5);--skeleton-start:#f0f0f0;--skeleton-middle:#e0e0e0;--skeleton-start:#f0f0f0;--skeleton-middle:#e0e0e0}[data-theme=dark]{--bg-color:#1a1a1a;--text-color:#e0e0e0;--grid-color:rgba(255, 255, 255, 0.1);--card-bg:#2d2d2d;--card-inner-bg:#1e1e1e;--card-border:#404040;--card-shadow:rgba(0, 0, 0, 0.3);--heading-color:#ffffff;--border-color:#404040;--file-bg:#2d2d2d;--file-border:#404040;--file-text:#e0e0e0;--file-subtext:#a0a0a0;--code-bg:#1e1e1e;--code-block-bg:#1e1e1e;--code-border:#2d2d2d;--code-text:#e0e0e0;--code-comment:#6a9955;--code-string:#ce9178;--code-number:#b5cea8;--code-keyword:#569cd6;--code-function:#dcdcaa;--code-property:#9cdcfe;--modal-bg:#2d2d2d;--modal-overlay:rgba(0, 0, 0, 0.7);--btn-bg:#3d3d3d;--btn-text:#ffffff;--btn-hover:#4a4a4a;--input-bg:#2d2d2d;--input-border:#404040;--input-text:#e0e0e0;--toast-bg:rgba(45, 45, 45, 0.9);--skeleton-start:#2d2d2d;--skeleton-middle:#3d3d3d;--skeleton-start:#2d2d2d;--skeleton-middle:#3d3d3d}body{font-family:var(--font-sans);color:var(--text-color);background-color:var(--bg-color);background-image:linear-gradient(var(--grid-color) 1px,transparent 1px),linear-gradient(90deg,var(--grid-color) 1px,transparent 1px);background-size:35px 35px;background-position:calc(50% + .5px) calc(50% + .5px);background-attachment:fixed;transition:background-color .3s,color .1s;overflow-x:hidden}.container{max-width:800px;margin:0 auto;padding:2rem 1rem}header{margin-bottom:2rem}header h1{margin-bottom:1rem}.header-buttons{display:flex;align-items:center;justify-content:center;margin:0 auto}.text-block:hover{box-shadow:0 6px 12px var(--card-shadow);box-shadow:0 4px 8px rgba(0,0,0,.15)}.text-block h2,h2{color:var(--heading-color);font-size:1.5rem;font-weight:600;margin-bottom:1.5rem}.poetry p,.prose p{margin:.5em 0}.form-group label,.modal-content,[data-theme=dark] .confirm-dialog-message,[data-theme=dark] .empty-hint,[data-theme=dark] .token.punctuation{color:var(--text-color)}.poetry{font-family:var(--font-serif);font-size:1.2rem;line-height:2}pre{margin:0;border-radius:4px;overflow-x:auto}code{font-family:var(--font-mono);font-size:.9rem;line-height:1.5}.prose{font-size:1rem;line-height:1.8;text-align:justify}.toast{font-size:14px;font-weight:500;box-shadow:0 4px 12px rgba(0,0,0,.15);display:flex;align-items:center;gap:8px}.empty,.error,.image img,.image-preview img,.loading,.text-block{box-shadow:0 2px 4px rgba(0,0,0,.1)}.toast.success{background-color:rgba(46,204,113,.9)}.toast.error{background-color:rgba(231,76,60,.9)}.toast::before{content:'';width:16px;height:16px;background-position:center;background-repeat:no-repeat;background-size:contain}.toast.success::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E")}.toast.error::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z'/%3E%3C/svg%3E")}.text-block{background:#fff;opacity:1}.text-block.fade-out,.toast{opacity:0}.text-block-actions{display:flex;justify-content:flex-end;gap:8px;border-top:1px solid #eee}.modal{display:none;background-color:var(--modal-overlay);z-index:1000;overflow-y:auto;padding:20px}.modal-content{background-color:var(--modal-bg);margin:20px auto;padding:2rem;width:90%;max-width:600px;border-radius:8px;position:relative;max-height:calc(100vh - 40px);overflow-y:auto}.form-group input,.form-group select,.form-group textarea{width:100%;padding:.5rem;border:1px solid var(--file-border);border-radius:4px;font-size:1rem;font-family:var(--font-sans);background-color:var(--file-bg);color:var(--text-color)}.form-group textarea{min-height:150px;resize:vertical}.form-buttons{display:flex;justify-content:flex-end;gap:1rem;margin-top:2rem;padding-top:1rem;border-top:1px solid var(--border-color)}@media (max-width:600px){.container{padding:1rem}h1{font-size:2rem}.text-block{padding:1.5rem}.poetry{font-size:1.1rem}.modal{padding:10px}.modal-content{margin:10px auto;padding:1rem;width:95%}.form-buttons{flex-direction:column;gap:.5rem}.btn{width:100%}.image-preview img{max-height:200px}}.confirm-dialog-overlay,.file{align-items:center;display:flex}.empty,.error,.loading{text-align:center;padding:2rem;background:#fff;border-radius:8px;margin:1rem 0}#editImage,.image-preview{padding:1rem;background-color:#f8f9fa}.code,.image img,.image-preview{border-radius:4px}.loading{color:#3498db}.error{color:#e74c3c}.empty{color:#95a5a6}.text-block.fade-in{animation:.3s ease-in-out fadeIn}.loading-spinner,.loading::before{animation:1s linear infinite spin}@keyframes fadeIn{from{opacity:0}to{opacity:1}}.image-preview{margin-top:1rem}.image-preview img{max-width:100%;max-height:300px;border-radius:4px}.poetry p{line-height:1.8}.code{background:#f8f9fa;padding:1rem}.code pre{margin:0}.code code{font-family:var(--font-mono);font-size:.9rem}#editImage{border:2px dashed #bdc3c7;transition:border-color .3s}#editImage:hover,[data-theme=dark] #editImage:hover{border-color:#3498db}.loading-spinner{border:2px solid #f3f3f3;border-top:2px solid #3498db}.toast{position:fixed;bottom:20px;left:50%;transform:translate(-50%,100%);background-color:rgba(0,0,0,.8);color:#fff;padding:.8rem 1.5rem;border-radius:4px;z-index:2000;transition:.3s ease-in-out}.toast.show{transform:translate(-50%,0);opacity:1}.toast.fade-out{opacity:0;transform:translate(-50%,20px)}.confirm-dialog-overlay{backdrop-filter:blur(4px);justify-content:center}@keyframes dialogShow{from{opacity:0;transform:translate(-50%,-45%)}to{opacity:1;transform:translate(-50%,-50%)}}.confirm-dialog h3{margin:0 0 16px;color:#333;font-size:1.25rem;font-weight:600}.confirm-dialog p{margin:8px 0;color:#666;line-height:1.5}.confirm-dialog ul{margin:12px 0;padding-left:20px;color:#666}.confirm-dialog ul li{margin:6px 0;line-height:1.4}.confirm-dialog .warning-text{color:#dc3545;font-weight:500;padding:12px 16px;background:rgba(220,53,69,.1);border-radius:6px;margin:16px 0}[data-theme=dark] .confirm-dialog-content{box-shadow:none}[data-theme=dark] .confirm-dialog .warning-text{background:rgba(220,53,69,.15)}.file{background:var(--file-bg);border:1px solid var(--file-border)}.file:hover{box-shadow:0 4px 8px var(--card-shadow)}.file-icon{align-items:center;justify-content:center;border-radius:8px}.file-name{color:var(--file-text);font-weight:500;font-size:1rem;margin-bottom:.25rem}.btn,.file-type,.text-block-meta{font-size:.875rem}.file-type,[data-theme=dark] .file-size,[data-theme=dark] .file-type{color:var(--file-subtext)}.file-icon.code{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%233498db' d='M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z'/%3E%3C/svg%3E")}.file-icon.archive{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ffa000' d='M20 6h-4V4c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-6 0h-4V4h4v2z'/%3E%3C/svg%3E")}.file-icon.video{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23e74c3c' d='M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z'/%3E%3C/svg%3E")}.file-icon.audio{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%239b59b6' d='M12 3v9.28c-.47-.17-.97-.28-1.5-.28C8.01 12 6 14.01 6 16.5S8.01 21 10.5 21c2.31 0 4.2-1.75 4.45-4H15V6h4V3h-7z'/%3E%3C/svg%3E")}.file-icon.generic{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2395a5a6' d='M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z'/%3E%3C/svg%3E")}.theme-toggle{position:fixed;top:20px;right:-45px;width:48px;height:24px;border-radius:12px;background-color:#e0e0e0;display:flex;align-items:center;padding:2px;transition:.3s;border:none;z-index:1000;opacity:.6}.theme-toggle-hover-area{position:fixed;top:0;right:0;width:80px;height:60px;z-index:999}.theme-toggle-hover-area:hover .theme-toggle,.theme-toggle:hover{right:20px;opacity:1}.theme-toggle::before{content:'';width:20px;height:20px;border-radius:50%;background-color:#fff;box-shadow:0 2px 4px rgba(0,0,0,.2);transition:transform .3s}[data-theme=dark] .theme-toggle{background-color:#4d3067}[data-theme=dark] .theme-toggle::before{transform:translateX(24px)}.theme-toggle:hover{opacity:.9}[data-theme=dark] .btn{background-color:var(--btn-bg);color:var(--btn-text);border:1px solid var(--border-color)}[data-theme=dark] .btn:hover{background-color:var(--btn-hover)}[data-theme=dark] input,[data-theme=dark] select,[data-theme=dark] textarea{background-color:var(--input-bg);border-color:var(--input-border);color:var(--input-text)}[data-theme=dark] input:focus,[data-theme=dark] select:focus,[data-theme=dark] textarea:focus{border-color:#3498db;outline:0}[data-theme=dark] .file{background:var(--file-bg);border-color:var(--file-border)}[data-theme=dark] .file:hover{background:var(--btn-hover);border-color:var(--border-color)}[data-theme=dark] .modal-content{background-color:var(--modal-bg);border:1px solid var(--border-color)}[data-theme=dark] .toast{background-color:var(--toast-bg)}[data-theme=dark] .empty,[data-theme=dark] .error,[data-theme=dark] .loading{background:var(--loading-bg);border:1px solid var(--border-color)}[data-theme=dark] .loading{color:var(--loading-text)}[data-theme=dark] .error{color:var(--error-text)}[data-theme=dark] .empty{color:var(--empty-text)}[data-theme=dark] .confirm-dialog-content{background:0 0;color:var(--dialog-text);border:none}[data-theme=dark] .confirm-dialog-title,[data-theme=dark] .empty-text{color:var(--heading-color)}[data-theme=dark] .loading-spinner{border:2px solid var(--spinner-bg);border-top:2px solid var(--spinner-border)}[data-theme=dark] #editImage,[data-theme=dark] #imagePreview,[data-theme=dark] .file-preview{background-color:var(--file-bg);border-color:var(--file-border)}[data-theme=dark] .file-name{color:var(--file-text)}[data-theme=dark] .text-block{background:var(--card-bg);border:1px solid var(--card-border);box-shadow:0 2px 4px var(--card-shadow)}[data-theme=dark] .text-block>div{background:var(--card-inner-bg);border-radius:12px;padding:1.5rem;margin:.5rem}[data-theme=dark] .text-block pre{background:var(--card-inner-bg);margin:0;border:none}[data-theme=dark] .text-block .code{background:var(--card-inner-bg);padding:0;border-radius:4px;overflow:hidden}[data-theme=dark] .text-block code{background:0 0;color:var(--code-text)}[data-theme=dark] pre{background-color:var(--code-block-bg);border:none;box-shadow:0 2px 4px rgba(0,0,0,.2)}[data-theme=dark] pre code{background-color:transparent;color:var(--code-text);border:none}[data-theme=dark] .text-block pre{background-color:var(--code-block-bg);border:1px solid var(--code-border)}[data-theme=dark] pre *{background-color:transparent!important}[data-theme=dark] .language-javascript{color:var(--code-text)}.btn,.btn-copy,.btn-delete,.btn-download,.btn-edit,[data-theme=dark] .confirm-dialog-title{color:#fff}[data-theme=dark] .token.cdata,[data-theme=dark] .token.comment,[data-theme=dark] .token.doctype,[data-theme=dark] .token.prolog{color:var(--code-comment)}[data-theme=dark] .token.attr-value,[data-theme=dark] .token.string{color:var(--code-string)}[data-theme=dark] .token.boolean,[data-theme=dark] .token.number{color:var(--code-number)}[data-theme=dark] .token.keyword,[data-theme=dark] .token.operator{color:var(--code-keyword)}[data-theme=dark] .token.function{color:var(--code-function)}[data-theme=dark] .token.attr-name,[data-theme=dark] .token.property{color:var(--code-property)}.text-block{background:var(--card-bg);border-radius:16px;padding:2rem;margin-bottom:2rem;box-shadow:0 1px 2px var(--card-shadow),0 2px 4px var(--card-shadow),0 4px 8px var(--card-shadow);transition:.3s;border:1px solid var(--border-color);overflow:hidden}.btn,.file{transition:.2s}.file{background:var(--file-bg);display:flex;align-items:center;padding:1.5rem;border-radius:12px;border:1px solid var(--file-border);margin:1rem}.form-group,.text-block-header{margin-bottom:1.5rem}[data-theme=dark] .file{background:var(--card-inner-bg);border:1px solid var(--card-border);margin:1.5rem;padding:2rem}.file-details{margin-left:1.5rem}[data-theme=dark] .text-block-actions{background:var(--card-bg);margin-top:1.5rem;padding:1.5rem;border-top:1px solid var(--border-color);border-radius:0 0 12px 12px}.text-block-actions{display:flex;gap:1rem;justify-content:flex-end}.btn{padding:.5rem 1rem;border-radius:6px;font-weight:500;border:none;display:inline-flex;align-items:center;justify-content:center;min-width:4rem}.btn:hover{transform:translateY(-1px);filter:brightness(1.1)}.btn:active{transform:translateY(0)}input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],select,textarea{width:100%;padding:.75rem 1rem;font-size:1rem;line-height:1.5;color:var(--input-text);background-color:var(--input-bg);border:1px solid var(--input-border);border-radius:6px;transition:.2s ease-in-out;margin-bottom:1rem}input::placeholder,textarea::placeholder{color:var(--input-placeholder)}input:focus,select:focus,textarea:focus{outline:0;border-color:var(--input-focus-border);box-shadow:0 0 0 3px rgba(52,152,219,.1)}select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23666' viewBox='0 0 16 16'%3E%3Cpath d='M8 10.5l-4-4h8l-4 4z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 1rem center;padding-right:2.5rem}.form-group label{display:block;margin-bottom:.5rem;color:var(--text-color);font-weight:500}.text-block-actions{margin-top:1.5rem;padding-top:1.5rem;border-top:1px solid var(--border-color);background:var(--card-bg)}.text-block-header{display:flex;justify-content:space-between;align-items:flex-start}.text-block-header h2{margin:0;font-size:1.5rem;font-weight:600;color:var(--heading-color);flex:1}.text-block-meta{color:#666;margin-left:1rem}.modified-date{display:inline-flex;align-items:center;color:#666}.empty,.loading{flex-direction:column}.modified-date::before{content:'';display:inline-block;width:14px;height:14px;margin-right:4px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'%3E%3C/path%3E%3C/svg%3E");background-size:contain;background-repeat:no-repeat;opacity:.7}[data-theme=dark] .loading-text,[data-theme=dark] .modified-date,[data-theme=dark] .text-block-meta{color:#999}[data-theme=dark] .modified-date::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23999999'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'%3E%3C/path%3E%3C/svg%3E")}.edit-form{scrollbar-width:thin;scrollbar-color:rgba(0,0,0,0.2) transparent}.edit-form::-webkit-scrollbar{width:6px}.edit-form::-webkit-scrollbar-track{background:0 0}.edit-form::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,.2);border-radius:3px;border:none}.edit-form::-webkit-scrollbar-thumb:hover{background-color:rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){.edit-form{scrollbar-color:rgba(255,255,255,0.2) transparent}.edit-form::-webkit-scrollbar-thumb{background-color:rgba(255,255,255,.2)}.edit-form::-webkit-scrollbar-thumb:hover{background-color:rgba(255,255,255,.3)}}.edit-modal{position:fixed;top:0;right:0;bottom:0;width:500px;background:var(--background-color);box-shadow:-2px 0 5px rgba(0,0,0,.1);padding:20px;border-radius:12px 0 0 12px;overflow:hidden;z-index:1000}.empty,.skeleton{box-shadow:0 1px 2px var(--card-shadow),0 2px 4px var(--card-shadow),0 4px 8px var(--card-shadow)}.edit-form{height:calc(100% - 60px);overflow-y:auto;padding-right:20px;margin-right:-20px;scrollbar-width:thin;scrollbar-color:rgba(0,0,0,0.2) transparent}.loading{display:flex;align-items:center;justify-content:center;color:var(--text-color-secondary)}.loading::before{content:"";margin-bottom:15px;border:3px solid var(--text-color-secondary)}.loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--text-color-secondary);border-radius:50%;border-top-color:transparent;margin-left:8px;vertical-align:middle}.empty{display:flex;align-items:center;justify-content:center;padding:4rem 2rem;background:var(--card-bg);border-radius:16px;margin:2rem 0;border:1px solid var(--border-color)}.empty-icon{font-size:4rem;margin-bottom:1.5rem;animation:2s infinite pulse}.empty-text{font-size:1.5rem;color:var(--heading-color);margin-bottom:1rem;font-weight:600}.empty-hint{font-size:1rem;color:var(--text-color);opacity:.8}@keyframes pulse{0%,100%{opacity:.5;transform:scale(1)}50%{opacity:.8;transform:scale(1.1)}}[data-theme=dark] .empty{background:var(--card-bg);border-color:var(--border-color)}@keyframes spin{100%,to{transform:rotate(360deg)}0%{transform:rotate(0)}}.loading{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100px;color:var(--text-color-secondary)}.loading::before{content:"";display:block;width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%}.loading-text{margin-top:15px;font-size:14px;color:#666}[data-theme=dark] .loading::before{border-color:#3498db #333 #333}.file-icon{width:32px;height:32px;display:inline-block;background-size:contain;background-repeat:no-repeat;background-position:center;margin-right:10px}.file-icon.markdown{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%234CAF50' d='M14.85 3H5.15A2.15 2.15 0 0 0 3 5.15v13.7A2.15 2.15 0 0 0 5.15 21h13.7A2.15 2.15 0 0 0 21 18.85V9.15L14.85 3Z'/%3E%3Cpath fill='white' d='M14.5 3v6.5H21'/%3E%3Cpath fill='white' d='M7 14.5v-5h2l2 2.5 2-2.5h2v5h-2v-3l-2 2.5-2-2.5v3H7z'/%3E%3C/svg%3E")}.file-icon.image{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23FF9800' d='M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'/%3E%3C/svg%3E")}.file-icon.pdf{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23F44336' d='M20 2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8.5 7.5c0 .83-.67 1.5-1.5 1.5H9v2H7.5V7H10c.83 0 1.5.67 1.5 1.5v1zm5 2c0 .83-.67 1.5-1.5 1.5h-2.5V7H15c.83 0 1.5.67 1.5 1.5v3zm4-3H19v1h1.5V11H19v2h-1.5V7h3v1.5zM9 9.5h1v-1H9v1zM4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm10 5.5h1v-3h-1v3z'/%3E%3C/svg%3E")}.file-icon.word{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%232196F3' d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-3.5 14H14l-2-7.5-2 7.5H8.5L6.1 7h1.7l1.54 7.5L11.3 7h1.4l1.97 7.5L16.2 7h1.7l-2.4 10z'/%3E%3C/svg%3E")}.file-icon.excel{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%234CAF50' d='M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z'/%3E%3C/svg%3E")}.file-icon.powerpoint{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect fill='%23d24726' x='2' y='2' width='20' height='20' rx='4'/%3E%3Cpath fill='white' d='M8 17V7h5c1.1 0 2 .9 2 2v2.5c0 1.1-.9 2-2 2H9.5v3.5H8zm1.5-5h3c.3 0 .5-.2.5-.5v-2c0-.3-.2-.5-.5-.5h-3v3z'/%3E%3C/svg%3E")}.file-icon.text{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%239E9E9E' d='M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 14H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z'/%3E%3C/svg%3E")}.file-icon.windows{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzAwNzhENiIgZD0iTTMgNS41NTdsNy4zNTctLjk2N3Y3LjExNkwzIDExLjY3MVY1LjU1N3ptMCA2LjY4Nmw3LjM1Ny0uOTY3djcuMTE2TDMgMTcuMzI4di01LjA4NXpNMTEuNSA0LjQ3N2w5LjUtMS4zMTJ2OC41NDFsLTkuNS0uOTY3VjQuNDc3em0wIDguMTU2bDkuNS0uOTY3djguNTQxbC05LjUtMS4zMTJ2LTYuMjYyeiIvPjwvc3ZnPg==")}.file-icon.windows-installer{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI0Y0NDMzNiIgZD0iTTE3IDJIN2MtMS4xIDAtMiAuOS0yIDJ2MTZjMCAxLjEuOSAyIDIgMmgxMGMxLjEgMCAyLS45IDItMlY0YzAtMS4xLS45LTItMi0yem0tMSAxMWgtMnYyaC0ydi0ySDh2LTJoNFY5aC00VjdoNFY1aDJ2MmgydjJoLTJ2MmgydjJ6Ii8+PC9zdmc+")}.file-icon.android{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzNEREMxNCIgZD0iTTE3LjYgOS40OGwxLjg0LTMuMThjLjE2LS4zMS4wNC0uNjktLjI2LS44NS0uMjktLjE1LS42NS0uMDYtLjgzLjIybC0xLjg4IDMuMjRjLTIuODYtMS4yMS02LjA4LTEuMjEtOC45NCAwTDUuNjUgNS42N2MtLjE5LS4yOS0uNTgtLjM4LS44Ny0uMi0uMjguMTgtLjM3LjU0LS4yMi44M0w2LjQgOS40OEMzLjMgMTEuMjUgMS4yOCAxNC40NCAxIDE4aDIyYy0uMjgtMy41Ni0yLjMtNi43NS01LjQtOC41MnpNNyAxNS4yNWMtLjY5IDAtMS4yNS0uNTYtMS4yNS0xLjI1cy41Ni0xLjI1IDEuMjUtMS4yNSAxLjI1LjU2IDEuMjUgMS4yNS0uNTYgMS4yNS0xLjI1IDEuMjV6bTEwIDBjLS42OSAwLTEuMjUtLjU2LTEuMjUtMS4yNXMuNTYtMS4yNSAxLjI1LTEuMjUgMS4yNS41NiAxLjI1IDEuMjUtLjU2IDEuMjUtMS4yNSAxLjI1eiIvPjwvc3ZnPg==")}.file-icon.ios,.file-icon.macos{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzk5OTk5OSIgZD0iTTE4LjcxIDE5LjVjLS44MyAxLjI0LTEuNzEgMi40NS0zLjA1IDIuNDctMS4zNC4wMy0xLjc3LS43OS0zLjI9LS43OS0xLjUzIDAtMiAuNzctMy4yNy44Mi0xLjMxLjA1LTIuMy0xLjMyLTMuMTQtMi41M0M0LjI1IDE3IDIuOTQgMTIuNDUgNC43IDkuMzljLjg3LTEuNTIgMi40My0yLjQ4IDQuMTItMi41MSAxLjI4LS4wMiAyLjUuODcgMy4yOS44Ny43OCAwIDIuMjYtMS4wNyAzLjgxLS45MS42NS4wMyAyLjQ3LjI2IDMuNjQgMS45OC0uMDkuMDYtMi4xNyAxLjI4LTIuMTUgMy44MS4wMyAzLjAyIDIuNjUgNC4wMyAyLjY4IDQuMDQtLjAzLjA3LS40MiAxLjQ0LTEuMzggMi44M00xMyAzLjVjLjczLS44MyAxLjk0LTEuNDYgMi45NC0xLjUuMTMgMS4xNy0uMzQgMi4zNS0xLjA0IDMuMTktLjY5Ljg1LTEuODMgMS41MS0yLjk1IDEuNDItLjE1LTEuMTUuNDEtMi4zNSAxLjA1LTMuMTF6Ii8+PC9zdmc+")}.file-icon.linux{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI0ZDQzYyNCIgZD0iTTEyLjUwNCAwYy0uMTU1IDAtLjMxNS4wMDgtLjQ4LjAyMS00LjIyNi4zMzMtMy4xMDUgNC44MDctMy4xNyA2LjI9OC0uMDc2IDEuMDkyLS4zIDEuOTUzLTEuMDUgMy4wMi0uODg1IDEuMDUxLTIuMTI3IDIuNzUtMi43MTYgNC41MjEtLjI3OC44MzItLjQxIDEuNjg0LS4yODcgMi40ODlhLjQyNC40MjQgMCAwMC0uMTEuMTM1Yy0uMjYuMjY4LS40NS42LS42NjMuODM5LS4xOTkuMTk5LS40ODUuMjY3LS43OTcuNC0uMzEzLjEzNi0uNjU4LjI6OS0uODY0LjY4LS4wOS4xODktLjEzNi4zOTQtLjEzMi42MDIgMCAuMTk5LjAyNy40LjA1NS41MzYuMDU4LjM5OS4xMTYuNzI4LjA0Ljk3LS4yNDkuNjgtLjI4IDEuMTQ1LS4xMDYgMS40ODQuMTc0LjMzNC41MzUuNDcuOTQuNjAxLjgxLjIgMS45MS4xMzUgMi43NzQuNi45MjYuNDY2IDEuODY2LjY3IDIuNjE2LjQ3LjUyNi0uMTE2Ljk3LS40NjQgMS4yMDgtLjk0Ni41ODctLjAwMyAxLjIzLS4yNjkgMi4yNi0uMzM0LjY5OS0uMDU4IDEuNTc0LjI2NyAyLjU3Ny4yLjAyNS4xMzQuMDYzLjE5OC4xMTQuMzMzbC4wMDMuMDAzYy4zOTEuNzc4IDEuMTEzIDEuMTMyIDEuODg0IDEuMDcxLjc3MS0uMDYgMS41OTItLjUzNiAyLjI1Ny0xLjMwNi42MzEtLjc2NSAxLjY4My0xLjA4NCAyLjM3OC0xLjUwMy4zNDgtLjE5OS42MjktLjQ2OS42NDktLjg1My4wMjMtLjQtLjItLjgxMS0uNzE0LTEuMzc2di0uMDk3bC0uMDAzLS4wMDNjLS4xNy0uMi0uMjUtLjUzNS0uMzM4LS45MjYtLjA4NS0uNDAxLS4xODItLjc4Ni0uNDkyLTEuMDQ2aC0uMDAzYy0uMDU5LS4wNTQtLjEyMy0uMDY3LS4xODgtLjEzNWEuMzU3LjM1NyAwIDAwLS4xOS0uMDY0Yy40MzEtMS4yNzguMjY0LTIuNTUtLjE3My0zLjY5NC0uNTMzLTEuNDEtMS40NjUtMi42MzgtMi4xNzUtMy40ODMtLjc5Ni0xLjAwNS0xLjU3Ni0xLjk1Ny0xLjU2LTMuMzY4LjAyNi0yLjE1Mi4yMzYtNi4xMzMtMy41NDQtNi4xMzl6bS41MjkgMy40MDVoLjAxM2MuMjEzIDAgLjM5Ni4wNjIuNTg0LjE5OC4xOS4xMzUuMzMuMzMyLjQzOC41MzMuMTA1LjI1OS4xNTguNDU5LjE2Ni43MjQgMC0uMDIuMDA2LS4wNC4wMDYtLjA2di4xMDVhLjA4Ni4wODYgMCAwMS0uMDA0LS4wMjFsLS4wMDQtLjAyNGExLjgwNyAxLjgwNyAwIDAxLS4xNS43MDYuOTUzLjk1MyAwIDAxLS4yMTMuMzM1LjcxLjcxIDAgMDAtLjA4OC0uMDQyYy0uMTA0LS4wNDUtLjE5OC0uMDY0LS4yODQtLjEzM2ExLjMxMiAxLjMxMiAwIDAwLS4yMi0uMDY2Yy4wNS0uMDYuMTQ2LS4xMzMuMTgzLS4xOTguMDUzLS4xMjguMDgyLS4yNjQuMDg4LS40MDJ2LS4wMmExLjIxIDEuMjEgMCAwMC0uMDYxLS40Yy0uMDQ1LS4xMzQtLjEwMS0uMi0uMTgzLS4zMzMtLjA4NC0uMDY2LS4xNjctLjEzMi0uMjY3LS4xMzJoLS4wMTZjLS4wOTMgMC0uMTc2LjAzLS4yNjIuMTMyYS44LjggMCAwMC0uMjA1LjMzNCAxLjE4IDEuMTggMCAwMC0uMDkuNHYuMDE5Yy4wMDIuMDg5LjAwOC4xNzkuMDIuMjY3LS4xOTMtLjA2Ny0uNDM4LS4xMzUtLjYwNy0uMjAyYTEuNjM1IDEuNjM1IDAgMDEtLjAxOC0uMnYtLjAyYTEuNzcyIDEuNzcyIDAgMDEuMTUtLjc2OGMuMDgyLS4yMi4yMzItLjQwNi40My0uNTMzYS45ODUuOTg1IDAgMDEuNTk0LS4yem0tMi45NjIuMDU5aC4wMzZjLjE0MiAwIC4yNy4wNDguMzk5LjEzNS4xNDYuMTI9LjI6NC4yODguMzQ0LjQ2NS4wOS4xOTkuMTQuNC4xNTMuNjY3di4wMDRjLjAwNy4xMzQuMDA2LjItLjAwMi4yNjZ2LjA4Yy0uMDMuMDA3LS4wNTYuMDE4LS4wODMuMDI0LS4xNTIuMDU1LS4yNzQuMTM1LS4zOTMuMi4wMTItLjA5LjAxMy0uMTguMDAzLS4yNjd2LS4wMTVjLS4wMTItLjEzMy0uMDQtLjItLjA4Mi0uMzMzYS42MTMuNjEzIDAgMDAtLjE2Ni0uMjY3LjI0OC4yNDggMCAwMC0uMTgzLS4wNjRoLS4wMjFjLS4wNzEuMDA2LS4xMy4wNC0uMTg2LjEzMmEuNTUyLjU1MiAwIDAwLS4xMi4yNy45NDQuOTQ0IDAgMDAtLjAyMy4zM3YuMDE1Yy4wMTIuMTM1LjAzNy4yLjA4LjMzNC4wNDYuMTM0LjA5OC4yLjE2Ni4yNjguMDEuMDA5LjAyLjAxOC4wMzQuMDI0LS4wNy4wNTctLjExNy4wNy0uMTc2LjEzNmEuMzA0LjMwNCAwIDAxLS4xMzEuMDY4IDIuNjIgMi42MiAwIDAxLS4yNzUtLjQwMiAxLjc3MiAxLjc3MiAwIDAxLS4xNTUtLjY2NyAxLjc1OSAxLjc1OSAwIDAxLjA4LS42NjggMS40MyAxLjQzIDAgMDEuMjgzLS41MzVjLjEyOC0uMTMzLjI2LS4yLjQxOC0uMnptMS4zNyAxLjcwNmMuMzMyIDAgLjczMy4wNjUgMS4yMTYuMzk5LjI5My4yLjUyMy4yNjkgMS4wNTIuNDY4aC4wMDNjLjI1NS4xMzYuNDA1LjI2Ni40NzguMzk5di0uMTMxYS41NzEuNTcxIDAgMDEuMDE2LjQ3Yy0uMTIzLjMxLS41MTYuNjQzLTEuMDYzLjg0MnYuMDAyYy0uMjY4LjEzNS0uNTAxLjMzMy0uNzc1LjQ2NS0uMjc2LjEzNS0uNTg4LjI9Mi0xLjAxMi4yNjdhMS4xMzkgMS4xMzkgMCAwMS0uNDQ4LS4wNjcgMy41NjYgMy41NjYgMCAwMS0uMzIyLS4xOThjLS4xOTUtLjEzNS0uMzYzLS4zMzItLjYxMi0uNDY1di0uMDA1aC0uMDA1Yy0uNC0uMjQ2LS42MTYtLjUxMi0uNjg2LS43MS0uMDctLjI2OC0uMDA1LS40Ny4xOTMtLjYuMjI0LS4xMzUuMzgtLjI3MS40ODMtLjMzNi4xMDQtLjA3NC4xNDMtLjEwMi4xNzYtLjEzMWguMDAydi0uMDAzYy4xNjktLjIwMi40MzYtLjQ3LjgzOS0uNjAxLjEzOS0uMDM2LjI5NC0uMDY1LjQ2Ni0uMDY1em0yLjggMi4xNDJjLjM1OCAxLjQxNyAxLjE5NiAzLjQ3NSAxLjczNSA0LjQ3My4yODYuNTM0Ljg1NSAxLjY1OSAxLjEwMiAzLjAyNC4xNTYtLjAwNS4zMy4wMTguNTEzLjA2NC42NDYtMS42NzEtLjU0Ni0zLjQ2Ny0xLjA4OS0zLjk2Ni0uMjItLjItLjIzMi0uMzM1LS4xMjMtLjMzNS41OS41MzQgMS4zNjUgMS41NzIgMS42NDYgMi43NTcuMTMuNTM1LjE2IDEuMTA0LjAyMSAxLjY3LjA2Ny4wMjguMTM1LjA2LjIwNS4wNjcgMS4wMzIuNTM0IDEuNDEzLjkzOCAxLjIzIDEuNTM3di0uMDQzYy0uMDYtLjAwMy0uMTIgMC0uMTggMGgtLjAxNmMuMTUxLS40NjctLjE4Mi0uODI1LTEuMDY1LTEuMjI0LS45MTUtLjQtMS42NDYtLjMzNi0xLjc3LjQ2NS0uMDA4LjA0My0uMDEzLjA2Ni0uMDE4LjEzNS0uMDY4LjAyMy0uMTM5LjA1My0uMjA5LjA2NC0uNDMuMjY4LS42NjIuNjY5LS43OTMgMS4xODctLjEzLjUzMy0uMTcgMS4xNTYtLjIwNSAxLjg2OXYuMDAzYy0uMDIuMzM0LS4xNy44MzgtLjMxOSAxLjM1LTEuNSAxLjA3Mi0zLjU4IDEuNTM4LTUuMzQ4LjMzNGEyLjY0NSAyLjY0NSAwIDAwLS40MDItLjUzMyAxLjQ1IDEuNDUgMCAwMC0uMjc1LS4zMzNjLjE4MiAwIC4zMzgtLjAzLjQ2Ii8+PC9zdmc+")}@keyframes shimmer{0%{background-position:-1000px 0}100%{background-position:1000px 0}}.skeleton{background:var(--card-bg);border-radius:16px;padding:2rem;margin-bottom:2rem;border:1px solid var(--border-color);overflow:hidden}.skeleton-meta,.skeleton-title{background:linear-gradient(90deg,var(--skeleton-start) 25%,var(--skeleton-middle) 50%,var(--skeleton-start) 75%)}.skeleton-title{height:32px;background-size:1000px 100%;animation:2s linear infinite shimmer;margin-bottom:1rem;border-radius:6px;width:60%}.skeleton-line,.skeleton-meta{height:20px;animation:2s linear infinite shimmer;border-radius:4px}.skeleton-meta{background-size:1000px 100%;margin-bottom:2rem;width:40%}.skeleton-line{background:linear-gradient(90deg,var(--skeleton-start) 25%,var(--skeleton-middle) 50%,var(--skeleton-start) 75%);background-size:1000px 100%;margin-bottom:1rem}.skeleton-line:first-child{width:100%}.skeleton-line:nth-child(2){width:92%}.skeleton-line:nth-child(3){width:85%}.header-buttons{display:flex;gap:10px;align-items:center}.btn-danger{background-color:#dc3545;color:#fff;border:1px solid #613e7f}.btn-danger:hover,[data-theme=dark] .btn-danger:hover{background-color:#c82333;border-color:#bd2130}[data-theme=dark] .btn-danger{background-color:#dc3545;border-color:#613e7f}.btn-icon{vertical-align:middle;display:inline-block}.confirm-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.2);z-index:1001;max-width:90%;width:420px;border:1px solid #e0e0e0;animation:.3s dialogShow;padding:24px}[data-theme=dark] .confirm-dialog{background:#1e1e1e;border-color:#333}.confirm-dialog-title{font-size:18px;font-weight:600;color:#333;margin-bottom:16px}.confirm-dialog-message{font-size:14px;color:#666;margin-bottom:24px;line-height:1.5}[data-theme=dark] .confirm-dialog-message{color:#ccc}.confirm-dialog-buttons{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}.confirm-dialog-buttons .btn{min-width:80px;padding:8px 16px}.confirm-dialog-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:1000}@media (max-width:768px){.form-group,.text-block,h1{margin-bottom:1rem}.form-group input,.form-group select,.form-group textarea,.header-buttons .btn{padding:8px;font-size:14px}.container,.text-block{padding:1rem}h1{font-size:1.75rem}.header-buttons{flex-direction:row;width:100%;padding:0;margin-top:1rem}.header-buttons .btn{flex:1;min-width:unset;white-space:nowrap;height:36px}.header-buttons .btn-icon{width:14px;height:14px;margin-right:4px}}.video-container,.video-container.bilibili,.video-container.youtube{padding-bottom:56.25%;height:0}.video-container iframe{position:absolute;top:0;left:0;width:100%;height:100%}.error-message{padding:15px;color:#856404;background-color:#fff3cd;border:1px solid #ffeeba;border-radius:4px;margin:10px 0}.error-message .error-title{font-weight:700;margin-bottom:10px;color:#dc3545}.error-message .error-details{font-size:14px}.error-message pre{background:#f8f9fa;padding:10px;border-radius:4px;margin:5px 0;white-space:pre-wrap;word-break:break-all}img{border-radius:8px;display:block;margin:1rem auto}.zoomable-image{cursor:zoom-in;transition:.3s ease-out}.medium-zoom-image,.medium-zoom-overlay{transition:.4s cubic-bezier(.4, 0, .2, 1)!important}.zoomable-image:hover{transform:scale(1.01)}.medium-zoom-overlay{z-index:1000;background:rgba(0,0,0,.9)!important}.medium-zoom-image{z-index:1001;filter:none!important;will-change:transform}.medium-zoom-image--opened{border-radius:0}[data-theme=dark] .medium-zoom-overlay{background:rgba(0,0,0,.95)!important}.video-container{position:relative;width:100%;max-width:800px;margin:1rem auto;border-radius:8px;overflow:hidden;background:#000}.video-container video.native-video{width:100%;height:auto;display:block;border-radius:8px}.video-container.bilibili iframe,.video-container.youtube iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}[data-theme=dark] .video-container{background:#1a1a1a}
2 | 


--------------------------------------------------------------------------------
/functions/_middleware.js:
--------------------------------------------------------------------------------
 1 | async function initializeDatabase(env) {
 2 |   if (!env.DB) {
 3 |     console.error('Database binding not found in environment');
 4 |     throw new Error('Database binding not found - 请在 Cloudflare Pages 设置中绑定 D1 数据库');
 5 |   }
 6 | 
 7 |   try {
 8 |     // 检查数据库连接
 9 |     const testQuery = await env.DB.prepare('SELECT 1').first();
10 |     if (!testQuery) {
11 |       throw new Error('数据库连接测试失败');
12 |     }
13 | 
14 |     // 创建表
15 |     await env.DB.prepare(`
16 |       CREATE TABLE IF NOT EXISTS content_blocks (
17 |         id INTEGER PRIMARY KEY AUTOINCREMENT,
18 |         type TEXT NOT NULL,
19 |         title TEXT NOT NULL,
20 |         content TEXT NOT NULL,
21 |         created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
22 |         updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
23 |       )
24 |     `).run();
25 |     
26 |     console.log('Database initialized successfully');
27 |   } catch (error) {
28 |     console.error('Failed to initialize database:', error);
29 |     throw new Error(`数据库初始化失败: ${error.message}`);
30 |   }
31 | }
32 | 
33 | export async function onRequest(context) {
34 |   try {
35 |     // 检查环境变量
36 |     if (!context.env.DB) {
37 |       throw new Error('Database binding not found - 请在 Cloudflare Pages 设置中绑定 D1 数据库');
38 |     }
39 | 
40 |     // 初始化数据库
41 |     await initializeDatabase(context.env);
42 |     
43 |     // 处理请求
44 |     const response = await context.next();
45 |     
46 |     // 添加 CORS 头
47 |     const headers = new Headers(response.headers);
48 |     headers.set('Access-Control-Allow-Origin', '*');
49 |     headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
50 |     headers.set('Access-Control-Allow-Headers', 'Content-Type');
51 |     
52 |     return new Response(response.body, {
53 |       status: response.status,
54 |       headers
55 |     });
56 |   } catch (error) {
57 |     console.error('Middleware error:', error);
58 |     
59 |     // 如果是 OPTIONS 请求,返回 CORS 头
60 |     if (context.request.method === 'OPTIONS') {
61 |       return new Response(null, {
62 |         headers: {
63 |           'Access-Control-Allow-Origin': '*',
64 |           'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
65 |           'Access-Control-Allow-Headers': 'Content-Type',
66 |           'Access-Control-Max-Age': '86400',
67 |         },
68 |       });
69 |     }
70 |     
71 |     // 返回详细的错误信息
72 |     return new Response(
73 |       JSON.stringify({
74 |         error: 'Internal Server Error',
75 |         details: error.message,
76 |         timestamp: new Date().toISOString()
77 |       }),
78 |       {
79 |         status: 500,
80 |         headers: {
81 |           'Content-Type': 'application/json',
82 |           'Access-Control-Allow-Origin': '*',
83 |           'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
84 |           'Access-Control-Allow-Headers': 'Content-Type',
85 |         },
86 |       }
87 |     );
88 |   }
89 | } 


--------------------------------------------------------------------------------
/functions/_telegram.js:
--------------------------------------------------------------------------------
  1 | // 发送消息到 Telegram
  2 | async function sendToTelegram(env, message, parseMode = 'HTML') {
  3 |   // 如果没有配置 Telegram,直接返回
  4 |   if (!env.TG_BOT_TOKEN || !env.TG_CHAT_ID) {
  5 |     return null;
  6 |   }
  7 | 
  8 |   try {
  9 |     // 确保消息不超过限制
 10 |     const truncatedMessage = truncateMessage(message);
 11 | 
 12 |     const response = await fetch(`https://api.telegram.org/bot${env.TG_BOT_TOKEN}/sendMessage`, {
 13 |       method: 'POST',
 14 |       headers: {
 15 |         'Content-Type': 'application/json',
 16 |       },
 17 |       body: JSON.stringify({
 18 |         chat_id: env.TG_CHAT_ID,
 19 |         text: truncatedMessage,
 20 |         parse_mode: parseMode,
 21 |         disable_web_page_preview: false
 22 |       }),
 23 |     });
 24 | 
 25 |     const result = await response.json();
 26 |     if (!result.ok) {
 27 |       console.error(`Telegram API error: ${result.description}`);
 28 |       return null;
 29 |     }
 30 | 
 31 |     return result;
 32 |   } catch (error) {
 33 |     console.error('Failed to send message to Telegram:', error);
 34 |     return null;
 35 |   }
 36 | }
 37 | 
 38 | // 格式化内容为 Telegram 消息
 39 | function formatContentForTelegram(type, title, content, url = null, isEdit = false) {
 40 |   let message = `${isEdit ? '内容已更新' : '新' + (type === 'file' ? '文件' : type === 'image' ? '图片' : '内容') + '上传'}\n\n`;
 41 |   message += `标题: ${escapeHtml(title)}\n`;
 42 |   
 43 |   if (type === 'text' || type === 'code' || type === 'poetry') {
 44 |     message += `内容:\n`;
 45 |     // 对于代码类型,使用代码格式
 46 |     if (type === 'code') {
 47 |       message += `
${escapeHtml(content)}
`; 48 | } else { 49 | message += escapeHtml(content); 50 | } 51 | } else if (type === 'file' || type === 'image') { 52 | message += `链接: ${url}`; 53 | } 54 | 55 | if (isEdit) { 56 | message += '\n\n此内容已被编辑'; 57 | } 58 | 59 | return message; 60 | } 61 | 62 | // 格式化删除通知 63 | function formatDeleteNotification(type, title) { 64 | return `🗑 内容已删除\n\n` + 65 | `类型: ${type === 'file' ? '文件' : type === 'image' ? '图片' : '内容'}\n` + 66 | `标题: ${escapeHtml(title)}\n\n` + 67 | `此内容已被永久删除`; 68 | } 69 | 70 | // 截断消息以符合 Telegram 限制 71 | function truncateMessage(message) { 72 | const MAX_LENGTH = 4000; // 留一些余地给可能的格式化字符 73 | 74 | if (message.length <= MAX_LENGTH) { 75 | return message; 76 | } 77 | 78 | // 检查是否包含代码块 79 | const codeBlockMatch = message.match(/
([\s\S]*?)<\/code><\/pre>/);
 80 |   if (codeBlockMatch) {
 81 |     const beforeCode = message.substring(0, codeBlockMatch.index);
 82 |     const afterCode = message.substring(codeBlockMatch.index + codeBlockMatch[0].length);
 83 |     const code = codeBlockMatch[1];
 84 |     
 85 |     // 如果代码太长,截断代码
 86 |     if (code.length > MAX_LENGTH - 200) { // 预留200字符给其他内容
 87 |       const truncatedCode = code.substring(0, MAX_LENGTH - 200) + '...(已截断)';
 88 |       return beforeCode + '
' + truncatedCode + '
' + afterCode; 89 | } 90 | } 91 | 92 | // 普通文本的截断 93 | return message.substring(0, MAX_LENGTH - 3) + '...'; 94 | } 95 | 96 | // HTML 转义 97 | function escapeHtml(text) { 98 | if (!text) return ''; 99 | return text 100 | .replace(/&/g, "&") 101 | .replace(//g, ">") 103 | .replace(/"/g, """) 104 | .replace(/'/g, "'"); 105 | } 106 | 107 | export { sendToTelegram, formatContentForTelegram, formatDeleteNotification }; -------------------------------------------------------------------------------- /functions/_vars/[name].js: -------------------------------------------------------------------------------- 1 | export async function onRequestGet({ params, env }) { 2 | const varName = params.name; 3 | 4 | // 只允许访问特定的环境变量 5 | const allowedVars = ['SYNC_INTERVAL', 'ACCESS_PASSWORD']; 6 | 7 | if (!allowedVars.includes(varName)) { 8 | return new Response('Forbidden', { 9 | status: 403, 10 | headers: { 11 | 'Access-Control-Allow-Origin': '*' 12 | } 13 | }); 14 | } 15 | 16 | // 获取环境变量值 17 | const value = env[varName]; 18 | 19 | // 如果是访问 ACCESS_PASSWORD 且未设置,返回特殊状态码 204 20 | if (varName === 'ACCESS_PASSWORD' && value === undefined) { 21 | return new Response(null, { 22 | status: 204, // 204 表示成功但无内容 23 | headers: { 24 | 'Access-Control-Allow-Origin': '*', 25 | 'Cache-Control': 'no-cache' 26 | } 27 | }); 28 | } 29 | 30 | if (value === undefined) { 31 | return new Response('Not Found', { 32 | status: 404, 33 | headers: { 34 | 'Access-Control-Allow-Origin': '*' 35 | } 36 | }); 37 | } 38 | 39 | return new Response(value, { 40 | headers: { 41 | 'Access-Control-Allow-Origin': '*', 42 | 'Cache-Control': 'no-cache' 43 | } 44 | }); 45 | } 46 | 47 | export function onRequestOptions() { 48 | return new Response(null, { 49 | headers: { 50 | 'Access-Control-Allow-Origin': '*', 51 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 52 | 'Access-Control-Allow-Headers': 'Content-Type', 53 | 'Access-Control-Max-Age': '86400', 54 | }, 55 | }); 56 | } -------------------------------------------------------------------------------- /functions/_wecom.js: -------------------------------------------------------------------------------- 1 | // 发送消息到企业微信机器人 2 | async function sendToWecom(env, message) { 3 | // 如果没有配置企业微信机器人,直接返回 4 | if (!env.WECOM_BOT_URL) { 5 | return null; 6 | } 7 | 8 | try { 9 | // 确保消息不超过限制 10 | const truncatedMessage = truncateMessage(message); 11 | 12 | const response = await fetch(env.WECOM_BOT_URL, { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | body: JSON.stringify({ 18 | msgtype: 'text', 19 | text: { 20 | content: truncatedMessage 21 | } 22 | }), 23 | }); 24 | 25 | const result = await response.json(); 26 | if (result.errcode !== 0) { 27 | console.error(`企业微信机器人 API 错误: ${result.errmsg}`); 28 | return null; 29 | } 30 | 31 | return result; 32 | } catch (error) { 33 | console.error('发送消息到企业微信失败:', error); 34 | return null; 35 | } 36 | } 37 | 38 | // 格式化内容为企业微信消息(纯文本) 39 | function formatContentForWecom(type, title, content, url = null, isEdit = false) { 40 | // 根据类型选择不同的emoji 41 | const typeEmoji = type === 'file' ? '📄' : type === 'image' ? '🖼️' : '📝'; 42 | 43 | let message = ''; 44 | if (isEdit) { 45 | message = `✏️ 内容已更新\n\n`; 46 | } else { 47 | message = `${typeEmoji} 新${type === 'file' ? '文件' : type === 'image' ? '图片' : '内容'}上传\n\n`; 48 | } 49 | 50 | message += `📌 标题: ${title}\n\n`; 51 | 52 | if (type === 'text' || type === 'code' || type === 'poetry') { 53 | message += `💬 内容:\n\n${content}`; 54 | } else if (type === 'file' || type === 'image') { 55 | message += `🔗 链接: ${url}`; 56 | } 57 | 58 | if (isEdit) { 59 | message += '\n\n✨ 此内容已被编辑'; 60 | } 61 | 62 | return message; 63 | } 64 | 65 | // 截断消息以符合企业微信限制 66 | function truncateMessage(message) { 67 | const MAX_LENGTH = 2048; // 企业微信文本消息长度限制 68 | 69 | if (message.length <= MAX_LENGTH) { 70 | return message; 71 | } 72 | 73 | return message.substring(0, MAX_LENGTH - 3) + '...'; 74 | } 75 | 76 | export { sendToWecom, formatContentForWecom }; -------------------------------------------------------------------------------- /functions/clear-all.js: -------------------------------------------------------------------------------- 1 | import { sendToTelegram } from './_telegram.js'; 2 | 3 | export async function onRequestPost({ env }) { 4 | try { 5 | // 1. 获取当前内容数量 6 | const stats = await env.DB.prepare('SELECT COUNT(*) as total FROM content_blocks').first(); 7 | const totalItems = stats.total; 8 | 9 | // 2. 清空数据库内容 10 | await env.DB.prepare('DELETE FROM content_blocks').run(); 11 | 12 | // 3. 获取所有图片和文件的键 13 | const imageKeys = await env.IMAGES.list(); 14 | const fileKeys = await env.FILES.list(); 15 | 16 | // 4. 删除所有图片 17 | for (const key of imageKeys.keys) { 18 | await env.IMAGES.delete(key.name); 19 | } 20 | 21 | // 5. 删除所有文件 22 | for (const key of fileKeys.keys) { 23 | await env.FILES.delete(key.name); 24 | } 25 | 26 | // 6. 发送清空通知到 Telegram 27 | const message = `🗑 内容已全部清空\n\n` + 28 | `清空内容:\n` + 29 | `- 数据库记录: ${totalItems} 条\n` + 30 | `- 图片文件: ${imageKeys.keys.length} 个\n` + 31 | `- 其他文件: ${fileKeys.keys.length} 个\n\n` + 32 | `所有内容已被永久删除`; 33 | await sendToTelegram(env, message); 34 | 35 | return new Response(JSON.stringify({ 36 | message: '所有内容已清空', 37 | details: { 38 | records: totalItems, 39 | images: imageKeys.keys.length, 40 | files: fileKeys.keys.length 41 | } 42 | }), { 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | 'Access-Control-Allow-Origin': '*' 46 | } 47 | }); 48 | } catch (error) { 49 | console.error('清空失败:', error); 50 | return new Response(JSON.stringify({ 51 | error: '清空失败: ' + error.message 52 | }), { 53 | status: 500, 54 | headers: { 55 | 'Content-Type': 'application/json', 56 | 'Access-Control-Allow-Origin': '*' 57 | } 58 | }); 59 | } 60 | } 61 | 62 | export function onRequestOptions() { 63 | return new Response(null, { 64 | headers: { 65 | 'Access-Control-Allow-Origin': '*', 66 | 'Access-Control-Allow-Methods': 'POST, OPTIONS', 67 | 'Access-Control-Allow-Headers': 'Content-Type', 68 | 'Access-Control-Max-Age': '86400', 69 | }, 70 | }); 71 | } -------------------------------------------------------------------------------- /functions/contents.js: -------------------------------------------------------------------------------- 1 | import { sendToTelegram, formatContentForTelegram } from './_telegram.js'; 2 | import { sendToWecom, formatContentForWecom } from './_wecom.js'; 3 | 4 | export async function onRequestGet({ request, env }) { 5 | try { 6 | const { results } = await env.DB.prepare( 7 | 'SELECT id, type, title, content, created_at as createdAt, updated_at as updatedAt FROM content_blocks ORDER BY id DESC' 8 | ).all(); 9 | 10 | return new Response(JSON.stringify(results), { 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | 'Access-Control-Allow-Origin': '*' 14 | } 15 | }); 16 | } catch (error) { 17 | console.error('Database error:', error); 18 | return new Response(JSON.stringify({ error: error.message }), { 19 | status: 500, 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | 'Access-Control-Allow-Origin': '*' 23 | } 24 | }); 25 | } 26 | } 27 | 28 | export async function onRequestPost({ request, env }) { 29 | try { 30 | const { type, title, content, fileType, fileSize } = await request.json(); 31 | 32 | if (!type || !title || !content) { 33 | return new Response(JSON.stringify({ error: '缺少必要字段' }), { 34 | status: 400, 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | 'Access-Control-Allow-Origin': '*' 38 | } 39 | }); 40 | } 41 | 42 | const { success } = await env.DB.prepare( 43 | 'INSERT INTO content_blocks (type, title, content, created_at, updated_at) VALUES (?, ?, ?, datetime("now"), datetime("now"))' 44 | ).bind(type, title, content).run(); 45 | 46 | if (!success) { 47 | throw new Error('创建内容失败'); 48 | } 49 | 50 | // 发送到 Telegram 51 | const telegramMessage = formatContentForTelegram(type, title, content, content); 52 | await sendToTelegram(env, telegramMessage); 53 | 54 | // 发送到企业微信 55 | const wecomMessage = formatContentForWecom(type, title, content, content); 56 | await sendToWecom(env, wecomMessage); 57 | 58 | return new Response(JSON.stringify({ 59 | type, 60 | title, 61 | content 62 | }), { 63 | headers: { 64 | 'Content-Type': 'application/json', 65 | 'Access-Control-Allow-Origin': '*' 66 | } 67 | }); 68 | } catch (error) { 69 | console.error('Database error:', error); 70 | return new Response(JSON.stringify({ error: error.message }), { 71 | status: 500, 72 | headers: { 73 | 'Content-Type': 'application/json', 74 | 'Access-Control-Allow-Origin': '*' 75 | } 76 | }); 77 | } 78 | } 79 | 80 | export async function onRequestOptions() { 81 | return new Response(null, { 82 | headers: { 83 | 'Access-Control-Allow-Origin': '*', 84 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 85 | 'Access-Control-Allow-Headers': 'Content-Type', 86 | 'Access-Control-Max-Age': '86400', 87 | }, 88 | }); 89 | } -------------------------------------------------------------------------------- /functions/contents/[id].js: -------------------------------------------------------------------------------- 1 | import { sendToTelegram, formatContentForTelegram } from '../_telegram.js'; 2 | import { sendToWecom, formatContentForWecom } from '../_wecom.js'; 3 | 4 | export async function onRequestPut({ request, env, params }) { 5 | try { 6 | const { type, title, content } = await request.json(); 7 | if (!type || !title || !content) { 8 | return new Response(JSON.stringify({ error: '缺少必要字段' }), { 9 | status: 400, 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | 'Access-Control-Allow-Origin': '*' 13 | } 14 | }); 15 | } 16 | 17 | const { success } = await env.DB.prepare( 18 | 'UPDATE content_blocks SET type = ?, title = ?, content = ?, updated_at = datetime("now") WHERE id = ?' 19 | ).bind(type, title, content, params.id).run(); 20 | 21 | if (!success) { 22 | return new Response(JSON.stringify({ error: '内容不存在' }), { 23 | status: 404, 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | 'Access-Control-Allow-Origin': '*' 27 | } 28 | }); 29 | } 30 | 31 | // 发送到 Telegram,添加编辑标记 32 | const telegramMessage = formatContentForTelegram(type, title, content, content, true); 33 | await sendToTelegram(env, telegramMessage); 34 | 35 | // 发送到企业微信,添加编辑标记 36 | const wecomMessage = formatContentForWecom(type, title, content, content, true); 37 | await sendToWecom(env, wecomMessage); 38 | 39 | return new Response(JSON.stringify({ id: params.id, type, title, content }), { 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | 'Access-Control-Allow-Origin': '*' 43 | } 44 | }); 45 | } catch (error) { 46 | console.error('Database error:', error); 47 | return new Response(JSON.stringify({ error: error.message }), { 48 | status: 500, 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | 'Access-Control-Allow-Origin': '*' 52 | } 53 | }); 54 | } 55 | } 56 | 57 | export async function onRequestDelete({ env, params }) { 58 | try { 59 | // 首先获取内容信息 60 | const content = await env.DB.prepare( 61 | 'SELECT type, title, content FROM content_blocks WHERE id = ?' 62 | ).bind(params.id).first(); 63 | 64 | if (!content) { 65 | return new Response(JSON.stringify({ error: '内容不存在' }), { 66 | status: 404, 67 | headers: { 68 | 'Content-Type': 'application/json', 69 | 'Access-Control-Allow-Origin': '*' 70 | } 71 | }); 72 | } 73 | 74 | // 如果是文件类型,从URL中提取文件名并删除KV中的文件 75 | if (content.type === 'file' || content.type === 'image') { 76 | try { 77 | const url = new URL(content.content); 78 | const filename = url.pathname.split('/').pop(); 79 | 80 | // 根据类型选择正确的KV存储 81 | const storage = content.type === 'file' ? env.FILES : env.IMAGES; 82 | if (storage) { 83 | await storage.delete(filename); 84 | console.log(`Deleted ${content.type} from KV:`, filename); 85 | } 86 | } catch (storageError) { 87 | console.error(`Error deleting ${content.type} from KV:`, storageError); 88 | } 89 | } 90 | 91 | // 删除内容记录 92 | const { success } = await env.DB.prepare( 93 | 'DELETE FROM content_blocks WHERE id = ?' 94 | ).bind(params.id).run(); 95 | 96 | // 发送删除通知到 Telegram 97 | const message = `🗑 内容已删除\n\n` + 98 | `类型: ${getContentTypeName(content.type)}\n` + 99 | `标题: ${content.title}\n\n` + 100 | `此内容已被永久删除`; 101 | await sendToTelegram(env, message); 102 | 103 | return new Response(JSON.stringify({ message: '删除成功' }), { 104 | headers: { 105 | 'Content-Type': 'application/json', 106 | 'Access-Control-Allow-Origin': '*' 107 | } 108 | }); 109 | } catch (error) { 110 | console.error('Database error:', error); 111 | return new Response(JSON.stringify({ error: error.message }), { 112 | status: 500, 113 | headers: { 114 | 'Content-Type': 'application/json', 115 | 'Access-Control-Allow-Origin': '*' 116 | } 117 | }); 118 | } 119 | } 120 | 121 | // 获取内容类型的友好名称 122 | function getContentTypeName(type) { 123 | const typeNames = { 124 | 'text': '文本', 125 | 'code': '代码', 126 | 'poetry': '诗歌', 127 | 'image': '图片', 128 | 'file': '文件' 129 | }; 130 | return typeNames[type] || type; 131 | } 132 | 133 | export async function onRequestOptions() { 134 | return new Response(null, { 135 | headers: { 136 | 'Access-Control-Allow-Origin': '*', 137 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 138 | 'Access-Control-Allow-Headers': 'Content-Type', 139 | 'Access-Control-Max-Age': '86400', 140 | }, 141 | }); 142 | } -------------------------------------------------------------------------------- /functions/files.js: -------------------------------------------------------------------------------- 1 | export async function onRequestGet({ request, env }) { 2 | try { 3 | const url = new URL(request.url); 4 | const filename = url.pathname.split('/').pop(); 5 | console.log('Requesting file:', filename); 6 | 7 | // 从KV存储获取文件 8 | const file = await env.FILES.get(filename, { type: 'arrayBuffer' }); 9 | const metadata = await env.FILES.getWithMetadata(filename); 10 | 11 | if (!file) { 12 | console.log('File not found:', filename); 13 | return new Response('文件不存在', { 14 | status: 404, 15 | headers: { 16 | 'Content-Type': 'text/plain', 17 | 'Access-Control-Allow-Origin': '*' 18 | } 19 | }); 20 | } 21 | 22 | // 获取原始文件名 23 | const originalName = metadata?.metadata?.filename || filename; 24 | 25 | // 返回文件 26 | return new Response(file, { 27 | headers: { 28 | 'Content-Type': metadata?.metadata?.contentType || 'application/octet-stream', 29 | 'Content-Disposition': `attachment; filename="${originalName}"`, 30 | 'Content-Length': metadata?.metadata?.size || file.byteLength, 31 | 'Cache-Control': 'public, max-age=31536000', 32 | 'Access-Control-Allow-Origin': '*' 33 | } 34 | }); 35 | } catch (error) { 36 | console.error('Get file error:', error); 37 | return new Response('Error fetching file: ' + error.message, { 38 | status: 500, 39 | headers: { 40 | 'Content-Type': 'text/plain', 41 | 'Access-Control-Allow-Origin': '*' 42 | } 43 | }); 44 | } 45 | } 46 | 47 | export async function onRequestOptions() { 48 | return new Response(null, { 49 | headers: { 50 | 'Access-Control-Allow-Origin': '*', 51 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 52 | 'Access-Control-Allow-Headers': 'Content-Type', 53 | 'Access-Control-Max-Age': '86400', 54 | }, 55 | }); 56 | } -------------------------------------------------------------------------------- /functions/files/[filename].js: -------------------------------------------------------------------------------- 1 | export async function onRequestGet({ params, env }) { 2 | try { 3 | const filename = params.filename; 4 | console.log('Requesting file:', filename); 5 | 6 | // 使用与图片相同的方式获取文件 7 | const file = await env.FILES.get(filename, { type: 'arrayBuffer' }); 8 | const metadata = await env.FILES.getWithMetadata(filename); 9 | 10 | if (!file) { 11 | console.log('File not found:', filename); 12 | return new Response('文件不存在', { 13 | status: 404, 14 | headers: { 15 | 'Content-Type': 'text/plain', 16 | 'Access-Control-Allow-Origin': '*' 17 | } 18 | }); 19 | } 20 | 21 | // 获取原始文件名 22 | const originalName = metadata?.httpMetadata?.contentDisposition?.match(/filename="(.+)"/)?.[1] || filename; 23 | 24 | return new Response(file, { 25 | headers: { 26 | 'Content-Type': metadata?.httpMetadata?.contentType || 'application/octet-stream', 27 | 'Content-Disposition': `attachment; filename="${originalName}"`, 28 | 'Access-Control-Allow-Origin': '*', 29 | 'Content-Length': file.byteLength 30 | } 31 | }); 32 | } catch (error) { 33 | console.error('Get file error:', error); 34 | return new Response('Error fetching file: ' + error.message, { 35 | status: 500, 36 | headers: { 37 | 'Content-Type': 'text/plain', 38 | 'Access-Control-Allow-Origin': '*' 39 | } 40 | }); 41 | } 42 | } -------------------------------------------------------------------------------- /functions/files/[name].js: -------------------------------------------------------------------------------- 1 | export async function onRequestGet({ request, env, params }) { 2 | try { 3 | // 处理 CORS 预检请求 4 | if (request.method === 'OPTIONS') { 5 | return new Response(null, { 6 | headers: { 7 | 'Access-Control-Allow-Origin': '*', 8 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 9 | 'Access-Control-Allow-Headers': '*', 10 | 'Access-Control-Max-Age': '86400', 11 | } 12 | }); 13 | } 14 | 15 | if (!env.FILES) { 16 | throw new Error('FILES binding not found'); 17 | } 18 | 19 | const filename = params.name; 20 | const origin = request.headers.get('origin') || request.url; 21 | console.log('Requesting file:', filename, 'Origin:', origin); 22 | 23 | // 从KV存储获取文件 24 | const file = await env.FILES.get(filename, { type: 'arrayBuffer' }); 25 | const { metadata } = await env.FILES.getWithMetadata(filename); 26 | 27 | if (!file) { 28 | return new Response('File not found', { 29 | status: 404, 30 | headers: { 31 | 'Access-Control-Allow-Origin': '*', 32 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 33 | 'Access-Control-Allow-Headers': '*' 34 | } 35 | }); 36 | } 37 | 38 | console.log('File found:', { 39 | size: file.byteLength, 40 | metadata: metadata, 41 | filename: filename 42 | }); 43 | 44 | // 设置响应头 45 | const headers = new Headers({ 46 | 'Content-Type': metadata?.contentType || 'application/octet-stream', 47 | 'Content-Disposition': `attachment; filename="${encodeURIComponent(metadata?.originalName || filename)}"`, 48 | 'Content-Length': file.byteLength.toString(), 49 | 'Cache-Control': 'no-cache, no-store, must-revalidate', 50 | 'Access-Control-Allow-Origin': '*', 51 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 52 | 'Access-Control-Allow-Headers': '*', 53 | 'Access-Control-Expose-Headers': 'Content-Disposition, Content-Length', 54 | 'Accept-Ranges': 'bytes' 55 | }); 56 | 57 | // 返回文件 58 | return new Response(file, { headers }); 59 | } catch (error) { 60 | console.error('Get file error:', error); 61 | return new Response('Error fetching file: ' + error.message, { 62 | status: 500, 63 | headers: { 64 | 'Content-Type': 'text/plain', 65 | 'Access-Control-Allow-Origin': '*', 66 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 67 | 'Access-Control-Allow-Headers': '*' 68 | } 69 | }); 70 | } 71 | } 72 | 73 | export async function onRequestOptions() { 74 | return new Response(null, { 75 | headers: { 76 | 'Access-Control-Allow-Origin': '*', 77 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 78 | 'Access-Control-Allow-Headers': '*', 79 | 'Access-Control-Max-Age': '86400', 80 | }, 81 | }); 82 | } -------------------------------------------------------------------------------- /functions/files/upload.js: -------------------------------------------------------------------------------- 1 | export async function onRequest(context) { 2 | try { 3 | const formData = await context.request.formData(); 4 | const file = formData.get('file'); 5 | 6 | if (!file) { 7 | return new Response(JSON.stringify({ error: '没有找到文件' }), { 8 | status: 400, 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | 'Access-Control-Allow-Origin': '*' 12 | } 13 | }); 14 | } 15 | 16 | // 生成唯一的文件名 17 | const timestamp = Date.now(); 18 | const randomString = Math.random().toString(36).substring(2, 15); 19 | const originalName = file.name; 20 | const extension = originalName.split('.').pop(); 21 | const filename = `${timestamp}-${randomString}.${extension}`; 22 | 23 | // 将文件转换为arrayBuffer并上传 24 | const arrayBuffer = await file.arrayBuffer(); 25 | await context.env.FILES.put(filename, arrayBuffer, { 26 | metadata: { 27 | contentType: file.type, 28 | filename: originalName, 29 | size: arrayBuffer.byteLength, 30 | httpMetadata: { 31 | contentType: file.type, 32 | contentDisposition: `attachment; filename="${originalName}"` 33 | } 34 | } 35 | }); 36 | 37 | console.log('File saved:', filename, 'Size:', arrayBuffer.byteLength, 'Type:', file.type); 38 | 39 | // 返回文件的URL 40 | const url = `${new URL(context.request.url).origin}/files/${filename}`; 41 | 42 | return new Response(JSON.stringify({ 43 | url, 44 | filename, 45 | size: arrayBuffer.byteLength, 46 | type: file.type 47 | }), { 48 | headers: { 49 | 'Content-Type': 'application/json', 50 | 'Access-Control-Allow-Origin': '*' 51 | } 52 | }); 53 | } catch (error) { 54 | console.error('Upload error:', error); 55 | return new Response(JSON.stringify({ error: error.message }), { 56 | status: 500, 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | 'Access-Control-Allow-Origin': '*' 60 | } 61 | }); 62 | } 63 | } -------------------------------------------------------------------------------- /functions/images.js: -------------------------------------------------------------------------------- 1 | export async function onRequestPost({ request, env }) { 2 | try { 3 | if (!env.IMAGES) { 4 | throw new Error('IMAGES binding not found'); 5 | } 6 | 7 | const formData = await request.formData(); 8 | const imageFile = formData.get('image'); 9 | 10 | if (!imageFile) { 11 | throw new Error('No image file provided'); 12 | } 13 | 14 | // 生成唯一的文件名 15 | const timestamp = Date.now(); 16 | const randomString = Math.random().toString(36).substring(2, 15); 17 | const extension = imageFile.name.split('.').pop().toLowerCase(); 18 | const filename = `${timestamp}-${randomString}.${extension}`; 19 | 20 | // 将图片保存到KV存储 21 | const arrayBuffer = await imageFile.arrayBuffer(); 22 | await env.IMAGES.put(filename, arrayBuffer, { 23 | metadata: { 24 | contentType: imageFile.type, 25 | filename: imageFile.name, 26 | size: arrayBuffer.byteLength 27 | } 28 | }); 29 | 30 | console.log('Image saved:', filename, 'Size:', arrayBuffer.byteLength, 'Type:', imageFile.type); 31 | 32 | // 返回完整的图片URL 33 | const url = new URL(request.url); 34 | const baseUrl = `${url.protocol}//${url.host}`; 35 | const imageUrl = `${baseUrl}/images/${filename}`; 36 | 37 | return new Response( 38 | JSON.stringify({ 39 | url: imageUrl, 40 | filename: filename, 41 | size: arrayBuffer.byteLength, 42 | type: imageFile.type 43 | }), { 44 | headers: { 45 | 'Content-Type': 'application/json', 46 | 'Access-Control-Allow-Origin': '*' 47 | } 48 | } 49 | ); 50 | } catch (error) { 51 | console.error('Upload error:', error); 52 | return new Response( 53 | JSON.stringify({ 54 | error: error.message 55 | }), { 56 | status: 500, 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | 'Access-Control-Allow-Origin': '*' 60 | } 61 | } 62 | ); 63 | } 64 | } 65 | 66 | export async function onRequestGet({ request, env }) { 67 | try { 68 | if (!env.IMAGES) { 69 | throw new Error('IMAGES binding not found'); 70 | } 71 | 72 | const url = new URL(request.url); 73 | const filename = url.pathname.split('/').pop(); 74 | 75 | // 从KV存储获取图片 76 | const metadata = await env.IMAGES.getWithMetadata(filename); 77 | if (!metadata.value) { 78 | return new Response('Image not found', { status: 404 }); 79 | } 80 | 81 | // 返回图片 82 | return new Response(metadata.value, { 83 | headers: { 84 | 'Content-Type': metadata.metadata?.contentType || 'image/jpeg', 85 | 'Cache-Control': 'public, max-age=31536000', 86 | 'Access-Control-Allow-Origin': '*' 87 | } 88 | }); 89 | } catch (error) { 90 | console.error('Get image error:', error); 91 | return new Response('Error fetching image', { status: 500 }); 92 | } 93 | } 94 | 95 | export async function onRequestOptions() { 96 | return new Response(null, { 97 | headers: { 98 | 'Access-Control-Allow-Origin': '*', 99 | 'Access-Control-Allow-Methods': 'POST, OPTIONS', 100 | 'Access-Control-Allow-Headers': 'Content-Type', 101 | 'Access-Control-Max-Age': '86400', 102 | }, 103 | }); 104 | } -------------------------------------------------------------------------------- /functions/images/[name].js: -------------------------------------------------------------------------------- 1 | export async function onRequestGet({ request, env, params }) { 2 | try { 3 | if (!env.IMAGES) { 4 | throw new Error('IMAGES binding not found'); 5 | } 6 | 7 | const filename = params.name; 8 | console.log('Requesting image:', filename); 9 | 10 | // 从KV存储获取图片 11 | const image = await env.IMAGES.get(filename, { type: 'arrayBuffer' }); 12 | const metadata = await env.IMAGES.getWithMetadata(filename); 13 | 14 | if (!image) { 15 | console.log('Image not found:', filename); 16 | return new Response('Image not found', { 17 | status: 404, 18 | headers: { 19 | 'Content-Type': 'text/plain', 20 | 'Access-Control-Allow-Origin': '*' 21 | } 22 | }); 23 | } 24 | 25 | // 返回图片,设置正确的Content-Type 26 | return new Response(image, { 27 | headers: { 28 | 'Content-Type': metadata.metadata?.contentType || 'image/jpeg', 29 | 'Cache-Control': 'public, max-age=31536000', 30 | 'Access-Control-Allow-Origin': '*' 31 | } 32 | }); 33 | } catch (error) { 34 | console.error('Get image error:', error); 35 | return new Response('Error fetching image: ' + error.message, { 36 | status: 500, 37 | headers: { 38 | 'Content-Type': 'text/plain', 39 | 'Access-Control-Allow-Origin': '*' 40 | } 41 | }); 42 | } 43 | } 44 | 45 | export async function onRequestDelete({ request, env, params }) { 46 | try { 47 | if (!env.IMAGES) { 48 | throw new Error('IMAGES binding not found'); 49 | } 50 | 51 | const filename = params.name; 52 | 53 | // 从KV存储删除图片 54 | await env.IMAGES.delete(filename); 55 | 56 | return new Response(JSON.stringify({ message: '图片删除成功' }), { 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | 'Access-Control-Allow-Origin': '*' 60 | } 61 | }); 62 | } catch (error) { 63 | console.error('Delete image error:', error); 64 | return new Response(JSON.stringify({ error: error.message }), { 65 | status: 500, 66 | headers: { 67 | 'Content-Type': 'application/json', 68 | 'Access-Control-Allow-Origin': '*' 69 | } 70 | }); 71 | } 72 | } 73 | 74 | export async function onRequestOptions() { 75 | return new Response(null, { 76 | headers: { 77 | 'Access-Control-Allow-Origin': '*', 78 | 'Access-Control-Allow-Methods': 'GET, DELETE, OPTIONS', 79 | 'Access-Control-Allow-Headers': 'Content-Type', 80 | 'Access-Control-Max-Age': '86400', 81 | }, 82 | }); 83 | } -------------------------------------------------------------------------------- /git_delete.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > nul 3 | setlocal enabledelayedexpansion 4 | 5 | :: Set window title 6 | title GitHub Auto Delete Tool 7 | 8 | :: Default values 9 | set "COMMIT_MESSAGE=Auto commit" 10 | set "REMOTE_BRANCH=main" 11 | 12 | :: Configure git for Chinese display 13 | git config --global core.quotepath false 14 | git config --global i18n.logoutputencoding utf-8 15 | git config --global i18n.commitencoding utf-8 16 | git config --global gui.encoding utf-8 17 | 18 | :list_files 19 | cls 20 | :: Reset all counters and arrays 21 | set i=1 22 | set "dir_count=0" 23 | set "file_count=0" 24 | set "md_count=0" 25 | for /l %%n in (1,1,100) do ( 26 | set "file_%%n=" 27 | ) 28 | 29 | :: Show header with current time 30 | for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a" 31 | set "YY=%dt:~2,2%" 32 | set "YYYY=%dt:~0,4%" 33 | set "MM=%dt:~4,2%" 34 | set "DD=%dt:~6,2%" 35 | set "HH=%dt:~8,2%" 36 | set "Min=%dt:~10,2%" 37 | set "Sec=%dt:~12,2%" 38 | 39 | echo. 40 | echo ════════════════════════════════════════ 41 | echo GitHub Auto Delete Tool 42 | echo ════════════════════════════════════════ 43 | echo Time: %HH%:%Min%:%Sec% Date: %YYYY%-%MM%-%DD% 44 | echo ──────────────────────────────────────── 45 | 46 | :: Fetch latest changes from remote 47 | echo [*] Fetching latest changes from remote... 48 | git fetch origin %REMOTE_BRANCH% 49 | 50 | echo. 51 | echo Files in repository: 52 | echo ──────────────────────────────────────── 53 | 54 | :: Temporary files for sorting 55 | set "temp_dir=%TEMP%\dirs.txt" 56 | set "temp_md=%TEMP%\md_files.txt" 57 | set "temp_other=%TEMP%\other_files.txt" 58 | set "temp_special=%TEMP%\special_files.txt" 59 | 60 | :: Clear temporary files 61 | type nul > "%temp_dir%" 62 | type nul > "%temp_md%" 63 | type nul > "%temp_other%" 64 | type nul > "%temp_special%" 65 | 66 | :: Sort files by type 67 | for /f "delims=" %%a in ('git ls-files') do ( 68 | set "filepath=%%a" 69 | if "!filepath:~-1!"=="/" ( 70 | echo %%a>> "%temp_dir%" 71 | set /a dir_count+=1 72 | ) else if "!filepath!"=="README.md" ( 73 | echo %%a>> "%temp_special%" 74 | ) else if "!filepath:~-3!"==".md" ( 75 | echo %%a>> "%temp_md%" 76 | set /a md_count+=1 77 | ) else ( 78 | echo %%a>> "%temp_other%" 79 | set /a file_count+=1 80 | ) 81 | ) 82 | 83 | :: Display special files first (README.md) 84 | for /f "delims=" %%a in ('type "%temp_special%"') do ( 85 | echo !i!. [SPECIAL] %%a 86 | set "file_!i!=%%a" 87 | set /a i+=1 88 | ) 89 | if exist "%temp_special%" ( 90 | if %i% gtr 1 echo. 91 | ) 92 | 93 | :: Display directories 94 | if %dir_count% gtr 0 ( 95 | echo [Directories] 96 | echo ──────────────────────────────────────── 97 | for /f "delims=" %%a in ('type "%temp_dir%"') do ( 98 | echo !i!. [DIR] %%a 99 | set "file_!i!=%%a" 100 | set /a i+=1 101 | ) 102 | echo. 103 | ) 104 | 105 | :: Display markdown files 106 | if %md_count% gtr 0 ( 107 | echo [Markdown Files] 108 | echo ──────────────────────────────────────── 109 | for /f "delims=" %%a in ('type "%temp_md%"') do ( 110 | echo !i!. [MD] %%a 111 | set "file_!i!=%%a" 112 | set /a i+=1 113 | ) 114 | echo. 115 | ) 116 | 117 | :: Display other files 118 | if %file_count% gtr 0 ( 119 | echo [Other Files] 120 | echo ──────────────────────────────────────── 121 | for /f "delims=" %%a in ('type "%temp_other%"') do ( 122 | echo !i!. %%a 123 | set "file_!i!=%%a" 124 | set /a i+=1 125 | ) 126 | ) 127 | 128 | :: Clean up temp files 129 | del "%temp_dir%" 2>nul 130 | del "%temp_md%" 2>nul 131 | del "%temp_other%" 2>nul 132 | del "%temp_special%" 2>nul 133 | 134 | set /a total_files=i-1 135 | echo ════════════════════════════════════════ 136 | 137 | :: Show file statistics 138 | echo Total: %total_files% files ^( MD: %md_count%, Other: %file_count% ^) 139 | echo ──────────────────────────────────────── 140 | 141 | :: Prompt user to input numbers 142 | echo. 143 | echo Enter numbers separated by spaces (e.g., "1 3 5") 144 | set /p FILE_NUMS=" Enter the numbers of files to delete (1-%total_files%, or 'q' to quit): " 145 | 146 | :: Check for quit 147 | if /i "%FILE_NUMS%"=="q" exit /b 148 | 149 | :: Initialize deletion list 150 | set "files_to_delete=" 151 | set "invalid_numbers=0" 152 | 153 | :: Validate each number and build deletion list 154 | for %%n in (%FILE_NUMS%) do ( 155 | set "num=%%n" 156 | if !num! LSS 1 ( 157 | set /a invalid_numbers+=1 158 | ) else if !num! GTR %total_files% ( 159 | set /a invalid_numbers+=1 160 | ) else ( 161 | if defined files_to_delete ( 162 | set "files_to_delete=!files_to_delete!,!file_%%n!" 163 | ) else ( 164 | set "files_to_delete=!file_%%n!" 165 | ) 166 | ) 167 | ) 168 | 169 | :: Check if any numbers were invalid 170 | if !invalid_numbers! GTR 0 ( 171 | echo [!] Invalid number^(s^) detected 172 | pause 173 | goto list_files 174 | ) 175 | 176 | :: Show selected files and confirm 177 | echo. 178 | echo Selected files for deletion: 179 | echo ──────────────────────────────────────── 180 | for %%f in (%files_to_delete%) do ( 181 | echo - %%f 182 | ) 183 | echo. 184 | set "CONFIRM=" 185 | set /p "CONFIRM=Are you sure you want to delete these files? (Y/N, default=Y): " 186 | if /i "!CONFIRM!"=="N" ( 187 | echo [!] Operation cancelled 188 | pause 189 | goto list_files 190 | ) 191 | 192 | :: Remove files from git 193 | echo. 194 | echo [*] Removing files... 195 | for %%f in (%files_to_delete%) do ( 196 | git rm --cached "%%f" 197 | ) 198 | 199 | :: Commit changes 200 | echo [*] Committing changes... 201 | git commit -m "%COMMIT_MESSAGE% - Delete multiple files" 202 | 203 | :: Push to remote repository 204 | echo [*] Pushing to remote repository... 205 | git push origin %REMOTE_BRANCH% 206 | 207 | echo. 208 | echo [√] Files have been deleted from remote repository 209 | echo ════════════════════════════════════════ 210 | pause 211 | goto list_files -------------------------------------------------------------------------------- /git_upload.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal EnableExtensions EnableDelayedExpansion 3 | cd /d "%~dp0" 4 | 5 | :: Store root directory path 6 | set "ROOT_DIR=%CD%" 7 | 8 | :: Initialize check 9 | echo [INFO] Starting GitHub Auto Upload Tool... 10 | echo [INFO] Current directory: %CD% 11 | pause 12 | 13 | title GitHub Auto Upload Tool 14 | 15 | :: Check Git installation 16 | echo [INFO] Checking Git installation... 17 | where git >nul 2>nul 18 | if errorlevel 1 ( 19 | echo [ERROR] Git is not installed! Please install Git first. 20 | pause 21 | goto :eof 22 | ) 23 | 24 | :: Create config folder 25 | if not exist ".git-upload-config" ( 26 | echo [INFO] Creating config folder... 27 | mkdir ".git-upload-config" 2>nul 28 | if errorlevel 1 ( 29 | echo [ERROR] Failed to create config folder 30 | pause 31 | goto :eof 32 | ) 33 | ) 34 | 35 | :: Create .gitignore 36 | if not exist ".git-upload-config\.gitignore" ( 37 | echo [INFO] Creating .gitignore file... 38 | ( 39 | echo .git-upload-config/ 40 | ) > ".git-upload-config\.gitignore" 41 | ) 42 | 43 | :: Remove old .gitignore 44 | if exist ".gitignore" ( 45 | echo [INFO] Removing old .gitignore file... 46 | del /f /q ".gitignore" >nul 2>&1 47 | ) 48 | 49 | :: Configure git exclude file 50 | echo [INFO] Configuring git exclude file... 51 | git config core.excludesFile ".git-upload-config\.gitignore" >nul 2>&1 52 | 53 | :: Check proxy config 54 | if exist ".git-upload-config\proxy_config.txt" ( 55 | set /p PROXY_PORT=<.git-upload-config\proxy_config.txt 56 | echo [INFO] Current proxy port: !PROXY_PORT! 57 | set /p "CHANGE_PROXY=Change proxy port? (Y/N, default=N): " 58 | if /i "!CHANGE_PROXY!"=="Y" ( 59 | set /p "PROXY_PORT=Enter new proxy port (default 7890): " 60 | if "!PROXY_PORT!"=="" set "PROXY_PORT=7890" 61 | echo !PROXY_PORT!> .git-upload-config\proxy_config.txt 62 | echo [INFO] New proxy port saved 63 | ) else ( 64 | echo [INFO] Using saved proxy port: !PROXY_PORT! 65 | ) 66 | ) else ( 67 | echo [INFO] First time proxy setup... 68 | set /p "PROXY_PORT=Enter proxy port (default 7890): " 69 | if "!PROXY_PORT!"=="" set "PROXY_PORT=7890" 70 | echo !PROXY_PORT!> .git-upload-config\proxy_config.txt 71 | echo [INFO] Proxy port saved 72 | ) 73 | 74 | :: Set proxy with timeout settings 75 | echo [INFO] Setting up Git proxy... 76 | git config --global http.proxy "http://127.0.0.1:!PROXY_PORT!" 2>nul 77 | git config --global https.proxy "http://127.0.0.1:!PROXY_PORT!" 2>nul 78 | git config --global http.lowSpeedLimit 1000 79 | git config --global http.lowSpeedTime 10 80 | git config --global http.postBuffer 524288000 81 | 82 | :: Repository config 83 | if not exist ".git-upload-config\repo_config.txt" ( 84 | echo [INFO] First time repository setup... 85 | :input_repo 86 | set /p "REPO_URL=Enter GitHub repository URL: " 87 | if "!REPO_URL!"=="" ( 88 | echo [ERROR] Repository URL cannot be empty 89 | goto input_repo 90 | ) 91 | echo !REPO_URL!> .git-upload-config\repo_config.txt 92 | echo [INFO] Repository URL saved: !REPO_URL! 93 | ) else ( 94 | set /p REPO_URL=<.git-upload-config\repo_config.txt 95 | if "!REPO_URL!"=="" ( 96 | echo [ERROR] Repository URL is empty in config 97 | del .git-upload-config\repo_config.txt 98 | goto :eof 99 | ) 100 | echo [INFO] Using saved repository: !REPO_URL! 101 | ) 102 | 103 | :: Git initialization 104 | if not exist .git ( 105 | echo [INFO] Initializing git repository... 106 | git init 107 | if errorlevel 1 ( 108 | echo [ERROR] Failed to initialize git repository 109 | goto :eof 110 | ) 111 | ) 112 | 113 | :: Configure remote 114 | echo [INFO] Configuring remote... 115 | git remote remove origin 2>nul 116 | git remote add origin !REPO_URL! 117 | if errorlevel 1 ( 118 | echo [ERROR] Failed to add remote origin 119 | goto :eof 120 | ) 121 | 122 | :: Initial git setup 123 | git config --global core.autocrlf true 124 | git config --global core.safecrlf false 125 | 126 | :: Initial branch setup 127 | git checkout -b main 2>nul 128 | if errorlevel 1 ( 129 | git checkout main 2>nul 130 | ) 131 | 132 | goto show_folder_menu 133 | 134 | :show_folder_menu 135 | cls 136 | echo ==================================== 137 | echo GitHub Auto Upload Tool 138 | echo ==================================== 139 | echo Current path: %CD% 140 | echo. 141 | echo [0] Upload all files 142 | echo [B] Back to parent directory 143 | echo. 144 | echo === Folders === 145 | 146 | :: List folders first 147 | set itemCount=0 148 | for /f "tokens=*" %%F in ('dir /b /ad') do ( 149 | if "%%F" neq ".git-upload-config" ( 150 | if "%%F" neq ".git" ( 151 | if not exist "%%F\.git" ( 152 | set /a itemCount+=1 153 | set "item_!itemCount!=%%F" 154 | set "type_!itemCount!=folder" 155 | echo [!itemCount!] %%F 156 | ) 157 | ) 158 | ) 159 | ) 160 | 161 | echo. 162 | echo === Files === 163 | 164 | :: Then list files 165 | for /f "tokens=*" %%F in ('dir /b /a-d') do ( 166 | if "%%F" neq ".git-upload-config" ( 167 | set /a itemCount+=1 168 | set "item_!itemCount!=%%F" 169 | set "type_!itemCount!=file" 170 | echo [!itemCount!] %%F 171 | ) 172 | ) 173 | 174 | echo. 175 | echo Total items: %itemCount% 176 | echo Enter numbers separated by space to select multiple items 177 | set /p "choice=Select number(s) (0=all, B=back): " 178 | 179 | :: Check for back command 180 | if /i "!choice!"=="B" ( 181 | cd .. 182 | goto show_folder_menu 183 | ) 184 | 185 | :: Simple validation 186 | if "!choice!"=="0" goto process_upload 187 | if "!choice!"=="" goto show_folder_menu 188 | 189 | :: Process selection 190 | for %%i in (!choice!) do ( 191 | if defined item_%%i ( 192 | set "selected=!item_%%i!" 193 | if "!type_%%i!"=="folder" ( 194 | echo [INFO] Entering directory: !selected! 195 | cd "!selected!" 196 | goto show_folder_menu 197 | ) 198 | ) 199 | ) 200 | 201 | :process_upload 202 | :: Store current directory 203 | set "CURRENT_DIR=%CD%" 204 | 205 | :: Change to root directory for git operations 206 | cd /d "%ROOT_DIR%" 207 | 208 | if "!choice!"=="0" ( 209 | echo [INFO] Adding all files... 210 | :: If in subdirectory, only add files from that directory 211 | if /i not "%CURRENT_DIR%"=="%ROOT_DIR%" ( 212 | set "REL_PATH=!CURRENT_DIR:%ROOT_DIR%\=!" 213 | :: Skip .git-upload-config and git repositories 214 | for /f "tokens=*" %%F in ('dir /b "!CURRENT_DIR!"') do ( 215 | if "%%F" neq ".git-upload-config" ( 216 | if "%%F" neq ".git" ( 217 | if not exist "!CURRENT_DIR!\%%F\.git" ( 218 | git add "!REL_PATH!\%%F" 219 | echo [INFO] Added: !REL_PATH!\%%F 220 | ) else ( 221 | echo [SKIP] Git repository: !REL_PATH!\%%F 222 | ) 223 | ) 224 | ) 225 | ) 226 | ) else ( 227 | :: In root directory, add all files 228 | for /f "tokens=*" %%F in ('dir /b') do ( 229 | if "%%F" neq ".git-upload-config" ( 230 | if "%%F" neq ".git" ( 231 | if not exist "%%F\.git" ( 232 | git add "%%F" 233 | echo [INFO] Added: %%F 234 | ) else ( 235 | echo [SKIP] Git repository: %%F 236 | ) 237 | ) 238 | ) else ( 239 | echo [SKIP] Config folder: %%F 240 | ) 241 | ) 242 | ) 243 | ) else ( 244 | echo [INFO] Adding selected files... 245 | :: Process multiple selections 246 | for %%i in (!choice!) do ( 247 | if defined item_%%i ( 248 | if "!type_%%i!"=="file" ( 249 | set "selected=!item_%%i!" 250 | if /i not "%CURRENT_DIR%"=="%ROOT_DIR%" ( 251 | set "REL_PATH=!CURRENT_DIR:%ROOT_DIR%\=!" 252 | git add "!REL_PATH!\!selected!" 253 | echo [INFO] Added: !REL_PATH!\!selected! 254 | ) else ( 255 | git add "!selected!" 256 | echo [INFO] Added: !selected! 257 | ) 258 | ) 259 | ) 260 | ) 261 | ) 262 | 263 | :: Commit changes 264 | for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value') do set datetime=%%I 265 | set COMMIT_DATE=!datetime:~0,4!-!datetime:~4,2!-!datetime:~6,2! 266 | set COMMIT_TIME=!datetime:~8,2!:!datetime:~10,2! 267 | 268 | :: Check if there are changes to commit 269 | git diff --cached --quiet 270 | if errorlevel 1 ( 271 | echo [INFO] Committing changes... 272 | git commit -m "[BAT] !COMMIT_DATE! !COMMIT_TIME!" 273 | if errorlevel 1 ( 274 | echo [ERROR] Failed to commit changes 275 | cd /d "!CURRENT_DIR!" 276 | pause 277 | goto show_folder_menu 278 | ) 279 | 280 | echo [INFO] Pushing to GitHub... 281 | git push -u origin main --force 282 | if errorlevel 1 ( 283 | echo [ERROR] Failed to push to GitHub 284 | echo [INFO] Retrying with different settings... 285 | 286 | :: Try with different proxy settings 287 | git config --global http.proxy "socks5://127.0.0.1:!PROXY_PORT!" 2>nul 288 | git config --global https.proxy "socks5://127.0.0.1:!PROXY_PORT!" 2>nul 289 | 290 | git push -u origin main --force 291 | if errorlevel 1 ( 292 | echo [ERROR] Push failed again 293 | echo [INFO] Restoring original proxy settings... 294 | git config --global http.proxy "http://127.0.0.1:!PROXY_PORT!" 2>nul 295 | git config --global https.proxy "http://127.0.0.1:!PROXY_PORT!" 2>nul 296 | cd /d "!CURRENT_DIR!" 297 | pause 298 | goto show_folder_menu 299 | ) 300 | ) 301 | ) else ( 302 | echo [WARNING] No changes to commit 303 | cd /d "!CURRENT_DIR!" 304 | pause 305 | goto show_folder_menu 306 | ) 307 | 308 | :: Return to original directory 309 | cd /d "!CURRENT_DIR!" 310 | 311 | echo. 312 | echo ==================================== 313 | echo [SUCCESS] Upload completed! 314 | echo ==================================== 315 | echo Repository: !REPO_URL! 316 | echo. 317 | pause 318 | goto show_folder_menu 319 | 320 | :exit_script 321 | echo [INFO] Cleaning up proxy settings... 322 | git config --global --unset http.proxy 323 | git config --global --unset https.proxy 324 | echo [INFO] Cleanup completed 325 | exit /b 326 | 327 | :eof 328 | goto exit_script -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 🚵🏽 BAOER の 中转信箱 📬 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 256 | 257 | 258 | 259 | 279 | 280 |
281 |
282 |
283 |

🚵🏽 BAOER の 中转信箱 📬

284 |
285 | 291 | 298 |
299 |
300 | 301 |
302 | 303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 | 338 | 339 | 379 |
380 |
381 | 382 | 383 | 384 | 385 | 390 | 391 | 392 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | const API_BASE_URL = "/contents"; const IMAGES_API_URL = "/images"; const FILES_API_URL = "/files"; const FILES_UPLOAD_URL = "/files/upload"; const DOWNLOAD_API_URL = "/download"; const _k = "_v"; const _e = "_e"; const _d = 15; const _c = "_c"; let isVerified = false; let verifiedUntil = 0; const CONTENT_CACHE_KEY = "content_cache"; const CONTENT_CACHE_EXPIRY_KEY = "content_cache_expiry"; const CACHE_EXPIRY_DAYS = 15; async function checkPasswordProtection() { try { const now = (new Date).getTime(); if (isVerified && now < verifiedUntil) { return true } const e = localStorage.getItem(_e); if (e) { const expiry = parseInt(e); if (now < expiry) { isVerified = true; verifiedUntil = expiry; return true } } const response = await fetch("/_vars/ACCESS_PASSWORD"); const needPassword = response.status !== 204; if (!needPassword) { isVerified = true; verifiedUntil = now + _d * 24 * 60 * 60 * 1e3; return true } if (!response.ok) { console.error("Error:", response.status); return true } document.getElementById("passwordOverlay").style.display = "flex"; document.getElementById("mainContent").classList.add("content-blur"); document.body.classList.add("password-active"); return false } catch (error) { console.error("Error:", error); return true } } async function verifyPassword(event) { if (event) { event.preventDefault() } const input = document.getElementById("accessPassword"); const pwd = input.value; try { const response = await fetch("/_vars/ACCESS_PASSWORD"); if (!response.ok) { throw new Error("Error") } const correct = await response.text(); const now = (new Date).getTime(); if (pwd === correct) { const expiry = now + _d * 24 * 60 * 60 * 1e3; isVerified = true; verifiedUntil = expiry; localStorage.setItem(_k, now.toString()); localStorage.setItem(_e, expiry.toString()); document.getElementById("passwordOverlay").style.display = "none"; document.getElementById("mainContent").classList.remove("content-blur"); document.body.classList.remove("password-active"); showToast("验证成功!") } else { showToast("密码错误!", "error"); input.value = "" } } catch (error) { console.error("Error:", error); showToast("Error: " + error.message, "error") } } document.addEventListener("keypress", function (e) { if (e.key === "Enter" && document.getElementById("passwordOverlay").style.display !== "none") { verifyPassword() } }); let currentEditId = null; let lastUpdateTime = Date.now(); let updateCheckInterval = null; let contentCache = []; let contentContainer; let syncInterval = 3e4; let zoomInstance = null; const SYNC_INTERVAL_KEY = "sync_interval"; const SYNC_INTERVAL_EXPIRY_KEY = "sync_interval_expiry"; async function getSyncInterval() { try { const savedInterval = localStorage.getItem(SYNC_INTERVAL_KEY); const expiry = localStorage.getItem(SYNC_INTERVAL_EXPIRY_KEY); if (savedInterval && expiry && (new Date).getTime() < parseInt(expiry)) { const parsedInterval = parseInt(savedInterval); if (!isNaN(parsedInterval) && parsedInterval >= 5e3) { syncInterval = parsedInterval; console.log("从本地存储加载同步间隔:", syncInterval, "ms"); return } } const response = await fetch("/_vars/SYNC_INTERVAL"); if (response.ok) { const interval = await response.text(); const parsedInterval = parseInt(interval); if (!isNaN(parsedInterval) && parsedInterval >= 5e3) { syncInterval = parsedInterval; const expiryDate = new Date; expiryDate.setDate(expiryDate.getDate() + 7); localStorage.setItem(SYNC_INTERVAL_KEY, syncInterval.toString()); localStorage.setItem(SYNC_INTERVAL_EXPIRY_KEY, expiryDate.getTime().toString()); console.log("从服务器加载同步间隔:", syncInterval, "ms") } } } catch (error) { console.warn("无法获取同步间隔配置,使用默认值:", syncInterval, "ms") } } function getFileIcon(filename) { const ext = filename.toLowerCase().split(".").pop(); if (["md", "markdown", "mdown", "mkd"].includes(ext)) return "markdown"; if (["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "ico", "tiff", "heic"].includes(ext)) return "image"; if (ext === "pdf") return "pdf"; if (["doc", "docx", "rtf", "odt", "pages"].includes(ext)) return "word"; if (["xls", "xlsx", "csv", "ods", "numbers"].includes(ext)) return "excel"; if (["ppt", "pptx", "odp", "key"].includes(ext)) return "powerpoint"; if (["txt", "log", "ini", "conf", "cfg"].includes(ext)) return "text"; if (ext === "exe") return "windows"; if (ext === "msi") return "windows-installer"; if (ext === "apk") return "android"; if (ext === "app" || ext === "dmg") return "macos"; if (ext === "deb" || ext === "rpm") return "linux"; if (["appx", "msix"].includes(ext)) return "windows-store"; if (["ipa", "pkg"].includes(ext)) return "ios"; if (["js", "ts", "jsx", "tsx", "json", "html", "css", "scss", "less", "sass", "php", "py", "java", "c", "cpp", "cs", "go", "rb", "swift", "kt", "rs", "dart", "vue", "sql", "sh", "bash", "yml", "yaml", "xml"].includes(ext)) return "code"; if (["zip", "rar", "7z", "tar", "gz", "bz2", "xz", "tgz"].includes(ext)) return "archive"; if (["mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "3gp", "mpg", "mpeg", "ogv"].includes(ext)) return "video"; if (["mp3", "wav", "ogg", "flac", "m4a", "aac", "wma", "opus", "mid", "midi"].includes(ext)) return "audio"; return "generic" } function getFileTypeDescription(filename) { const ext = filename.toLowerCase().split(".").pop(); if (["md", "markdown", "mdown", "mkd"].includes(ext)) return "Markdown文档"; if (["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "ico", "tiff", "heic"].includes(ext)) return "图片文件"; if (ext === "pdf") return "PDF文档"; if (["doc", "docx", "rtf", "odt", "pages"].includes(ext)) return "Word文档"; if (["xls", "xlsx", "csv", "ods", "numbers"].includes(ext)) return "Excel表格"; if (["ppt", "pptx", "odp", "key"].includes(ext)) return "PowerPoint演示文稿"; if (["txt", "log"].includes(ext)) return "文本文件"; if (["ini", "conf", "cfg"].includes(ext)) return "配置文件"; if (ext === "exe") return "Windows可执行程序"; if (ext === "msi") return "Windows安装程序"; if (ext === "apk") return "Android应用程序"; if (ext === "app") return "macOS应用程序"; if (ext === "dmg") return "macOS安装镜像"; if (ext === "deb") return "Debian/Ubuntu安装包"; if (ext === "rpm") return "RedHat/Fedora安装包"; if (["appx", "msix"].includes(ext)) return "Windows商店应用"; if (ext === "ipa") return "iOS应用程序"; if (ext === "pkg") return "macOS安装包"; if (["js", "ts"].includes(ext)) return "JavaScript/TypeScript文件"; if (["jsx", "tsx"].includes(ext)) return "React组件"; if (ext === "vue") return "Vue组件"; if (ext === "html") return "HTML文件"; if (["css", "scss", "less", "sass"].includes(ext)) return "样式表"; if (ext === "php") return "PHP文件"; if (ext === "py") return "Python文件"; if (ext === "java") return "Java文件"; if (["c", "cpp"].includes(ext)) return "C/C++文件"; if (ext === "cs") return "C#文件"; if (ext === "go") return "Go文件"; if (ext === "rb") return "Ruby文件"; if (ext === "swift") return "Swift文件"; if (ext === "kt") return "Kotlin文件"; if (ext === "rs") return "Rust文件"; if (ext === "dart") return "Dart文件"; if (ext === "sql") return "SQL文件"; if (["sh", "bash"].includes(ext)) return "Shell文本"; if (["yml", "yaml"].includes(ext)) return "YAML配置"; if (ext === "xml") return "XML文件"; if (["zip", "rar", "7z"].includes(ext)) return "压缩文件"; if (["tar", "gz", "bz2", "xz", "tgz"].includes(ext)) return "归档文件"; if (["mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "3gp", "mpg", "mpeg", "ogv"].includes(ext)) return "视频文件"; if (["mp3", "wav", "ogg", "flac", "m4a", "aac", "wma", "opus"].includes(ext)) return "音频文件"; if (["mid", "midi"].includes(ext)) return "MIDI音乐"; return `${ext.toUpperCase()}文件` } function formatFileSize(bytes) { if (!bytes || bytes === 0) return "未知大小"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] } function encodeContent(text) { return btoa(unescape(encodeURIComponent(text))) } function decodeContent(encoded) { return decodeURIComponent(escape(atob(encoded))) } function showToast(message, type = "success") { const existingToast = document.querySelector(".toast"); if (existingToast) { existingToast.remove() } const toast = document.createElement("div"); toast.className = `toast ${type}`; toast.textContent = message; document.body.appendChild(toast); requestAnimationFrame(() => { toast.classList.add("show") }); setTimeout(() => { toast.classList.add("fade-out"); setTimeout(() => toast.remove(), 300) }, 2e3) } function previewImage(input) { const preview = document.getElementById("imagePreview"); preview.innerHTML = ""; if (input.files && input.files[0]) { const file = input.files[0]; const img = document.createElement("img"); img.alt = "预览"; img.onload = function () { window.URL.revokeObjectURL(this.src) }; img.src = window.URL.createObjectURL(file); preview.appendChild(img) } } function copyText(encodedText, type) { const text = decodeContent(encodedText); let copyContent = text; if (type === "poetry") { copyContent = text.split("\n").join("\r\n") } else if (type === "image") { copyContent = text } navigator.clipboard.writeText(copyContent).then(() => { showToast("复制成功!") })["catch"](() => { const textarea = document.createElement("textarea"); textarea.value = copyContent; document.body.appendChild(textarea); textarea.select(); try { document.execCommand("copy"); showToast("复制成功!") } catch (e) { showToast("复制失败,请手动复制", "error") } document.body.removeChild(textarea) }) } function showConfirmDialog(title, message) { 2 | return new Promise(resolve => { 3 | const wrapper = document.createElement("div"); wrapper.innerHTML = ` 4 |
5 |
6 |
7 |
${title}
8 |
${message}
9 |
10 | 11 | 12 |
13 |
14 |
15 | `; document.body.appendChild(wrapper); const buttons = wrapper.querySelectorAll(".btn"); buttons.forEach(button => { button.addEventListener("click", () => { wrapper.remove(); resolve(button.classList.contains("btn-primary")) }) }) 16 | }) 17 | } function getFileIconUrl(filename) { const ext = filename.toLowerCase().split(".").pop(); return `https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@main/icons/${ext}.svg` } async function downloadFile(url, filename) { try { showToast("准备下载文件..."); const response = await fetch(url, { method: "GET", headers: { Accept: "*/*" } }); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`) } const contentDisposition = response.headers.get("content-disposition"); const match = contentDisposition?.match(/filename="(.+)"/); const actualFilename = match ? decodeURIComponent(match[1]) : filename; const reader = response.body.getReader(); const contentLength = response.headers.get("content-length"); let receivedLength = 0; const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) { break } chunks.push(value); receivedLength += value.length; if (contentLength) { const progress = (receivedLength / contentLength * 100).toFixed(2); showToast(`下载进度: ${progress}%`) } } const blob = new Blob(chunks); const blobUrl = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = blobUrl; link.download = actualFilename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(blobUrl); showToast("文件下载完成") } catch (error) { console.error("下载失败:", error); showToast("下载失败: " + error.message, "error") } } function formatDate(timestamp) { const date = new Date(timestamp); const beijingDate = new Date(date.getTime() + 8 * 60 * 60 * 1e3); const year = beijingDate.getFullYear(); const month = String(beijingDate.getMonth() + 1).padStart(2, "0"); const day = String(beijingDate.getDate()).padStart(2, "0"); const hours = String(beijingDate.getHours()).padStart(2, "0"); const minutes = String(beijingDate.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day} ${hours}:${minutes}` } const md = window.markdownit({ html: true, breaks: true, linkify: true, typographer: true, quotes: ['""', "''"] }).use(window.markdownitEmoji).use(window.markdownitSub).use(window.markdownitSup).use(window.markdownitFootnote).use(window.markdownitTaskLists, { enabled: true, label: true, labelAfter: true }); const zoom = mediumZoom("[data-zoomable]", { margin: 48, background: "rgba(0, 0, 0, 0.9)", scrollOffset: 0, container: document.body, template: null, transition: { duration: 400, timing: "cubic-bezier(0.4, 0, 0.2, 1)" } }); md.renderer.rules.image = function (tokens, idx, options, env, slf) { const token = tokens[idx]; const src = token.attrGet("src"); const alt = token.content || ""; const title = token.attrGet("title") || ""; return `${alt}` }; function parseVideoUrl(url) { const videoExtensions = /\.(mp4|mkv|webm|avi|mov|wmv|flv)$/i; if (videoExtensions.test(url)) { return { type: "video", url: url } } const youtubeMatch = url.match(/(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^?&\s]+)/); if (youtubeMatch) { return { type: "youtube", id: youtubeMatch[1], embed: `https://www.youtube.com/embed/${youtubeMatch[1]}` } } const bilibiliMatch = url.match(/(?:bilibili\.com\/video\/)([^?&\s/]+)/); if (bilibiliMatch) { return { type: "bilibili", id: bilibiliMatch[1], embed: `//player.bilibili.com/player.html?bvid=${bilibiliMatch[1]}&page=1` } } return null } md.renderer.rules.link_open = function (tokens, idx, options, env, self) { const token = tokens[idx]; const href = token.attrGet("href"); if (href) { const video = parseVideoUrl(href); if (video) { token.video = video; return "" } token.attrPush(["target", "_blank"]); token.attrPush(["rel", "noopener noreferrer"]) } return self.renderToken(tokens, idx, options) }; md.renderer.rules.link_close = function (tokens, idx, options, env, self) { 18 | if (idx >= 2 && tokens[idx - 2]) { 19 | const openToken = tokens[idx - 2]; if (openToken && openToken.video) { 20 | const video = openToken.video; if (video.type === "youtube") { 21 | return `
22 | 27 |
`} else if (video.type === "bilibili") { 28 | return `
29 | 33 |
`} else if (video.type === "video") { 34 | return `
35 | 39 |
`} 40 | } 41 | } return self.renderToken(tokens, idx, options) 42 | }; md.renderer.rules.fence = function (tokens, idx, options, env, slf) { 43 | const token = tokens[idx]; const code = token.content; const lang = token.info || ""; const highlighted = Prism.highlight(code, Prism.languages[lang] || Prism.languages.plain, lang); return `
44 |
${highlighted}
45 | 46 |
`}; window.copyCode = function (button) { const pre = button.parentElement.querySelector("pre"); const code = pre.textContent; navigator.clipboard.writeText(code).then(() => { const originalText = button.textContent; button.textContent = "已复制!"; button.style.background = "#4CAF50"; button.style.color = "white"; setTimeout(() => { button.textContent = originalText; button.style.background = ""; button.style.color = "" }, 2e3) })["catch"](err => { console.error("复制失败:", err); showToast("复制失败,请手动复制", "error") }) }; function renderContents(contents) { 47 | if (!contentContainer) { contentContainer = document.getElementById("content-container") } if (!contents || contents.length === 0) { 48 | contentContainer.innerHTML = ` 49 |
50 |
📝
51 |
还没有任何内容
52 |
点击"添加内容"开始创建
53 |
54 | `; return 55 | } const fragment = document.createDocumentFragment(); contents.forEach(content => { 56 | const section = document.createElement("section"); section.className = "text-block"; let contentHtml = ""; let downloadButton = ""; try { 57 | if (content.type === "image" || content.type === "file") { 58 | if (content.type === "image") { contentHtml = `
${content.title}
` } else { 59 | const fileIcon = getFileIcon(content.title); const fileType = getFileTypeDescription(content.title); contentHtml = ` 60 |
61 | 62 |
63 |
${content.title}
64 |
${fileType}
65 |
66 |
`} downloadButton = `` 67 | } else if (content.type === "code") { const escapedContent = content.content.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); contentHtml = `
${escapedContent}
` } else if (content.type === "poetry") { contentHtml = content.content.split("\n").map(line => `

${line.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'")}

`).join("") } else { contentHtml = md.render(content.content) } 68 | } catch (error) { console.error("Card rendering error:", content.id, error); contentHtml = `
内容渲染失败
` } const encodedContent = encodeContent(content.content); const modifiedDate = formatDate(content.updatedAt || content.createdAt || Date.now()); section.innerHTML = ` 69 |
70 |

${content.title}

71 |
72 | 修改于 ${modifiedDate} 73 |
74 |
75 |
76 | ${contentHtml} 77 |
78 |
79 | 80 | ${downloadButton} 81 | 82 | 83 |
84 | `; fragment.appendChild(section) 85 | }); contentContainer.innerHTML = ""; contentContainer.appendChild(fragment); requestAnimationFrame(() => { Prism.highlightAll(); zoom.detach(); zoom.attach("[data-zoomable]") }) 86 | } async function loadContents(showLoading = true) { if (!contentContainer) { contentContainer = document.getElementById("content-container") } try { const cachedContent = localStorage.getItem(CONTENT_CACHE_KEY); const cacheExpiry = localStorage.getItem(CONTENT_CACHE_EXPIRY_KEY); if (cachedContent && cacheExpiry && (new Date).getTime() < parseInt(cacheExpiry)) { const newContent = JSON.parse(cachedContent); if (!contentCache || contentCache.length === 0) { contentCache = newContent; await renderContents(contentCache); console.log("从本地缓存加载内容") } fetchAndUpdateContent(false); return } await fetchAndUpdateContent(showLoading) } catch (error) { console.error("加载内容失败:", error); if (showLoading) { showError(`加载内容失败: ${error.message}`) } } } async function fetchAndUpdateContent(showLoading = true) { const response = await fetch(API_BASE_URL, { headers: { Accept: "application/json" } }); if (!response.ok) { const data = await response.json(); throw new Error(data.details || data.error || "加载失败") } const data = await response.json(); const hasContentChanged = JSON.stringify(contentCache) !== JSON.stringify(data); if (hasContentChanged) { console.log("检测到内容变化,更新界面"); contentCache = data || []; await renderContents(contentCache); const expiryDate = new Date; expiryDate.setDate(expiryDate.getDate() + CACHE_EXPIRY_DAYS); localStorage.setItem(CONTENT_CACHE_KEY, JSON.stringify(contentCache)); localStorage.setItem(CONTENT_CACHE_EXPIRY_KEY, expiryDate.getTime().toString()); console.log("内容已更新并缓存") } else { console.log("内容未发生变化,保持当前显示") } lastUpdateTime = Date.now() } window.deleteContent = async function (id) { const confirmed = await showConfirmDialog("确认删除", "确定要删除这条内容吗?此操作无法撤销。"); if (confirmed) { try { const response = await fetch(`${API_BASE_URL}/${id}`, { method: "DELETE", headers: { Accept: "application/json" } }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || "删除失败") } contentCache = contentCache.filter(item => item.id !== id); renderContents(contentCache); localStorage.setItem(CONTENT_CACHE_KEY, JSON.stringify(contentCache)); showToast("删除成功!") } catch (error) { console.error("删除失败:", error); showToast(error.message, "error") } } }; window.handleTypeChange = function (type) { 87 | const contentGroup = document.getElementById("contentGroup"); const imageGroup = document.getElementById("imageGroup"); const fileGroup = document.getElementById("fileGroup"); const editContent = document.getElementById("editContent"); const editImage = document.getElementById("editImage"); const editFile = document.getElementById("editFile"); const titleInput = document.getElementById("editTitle"); const titleGroup = document.getElementById("titleGroup"); const fileInfo = document.querySelector(".file-info"); contentGroup.style.display = "none"; imageGroup.style.display = "none"; fileGroup.style.display = "none"; titleGroup.style.display = "block"; editContent.required = false; editImage.required = false; editFile.required = false; titleInput.required = false; if (type === "image") { imageGroup.style.display = "block"; editImage.required = true; titleGroup.style.display = "none" } else if (type === "file") { 88 | fileGroup.style.display = "block"; editFile.required = true; if (!editFile.files || !editFile.files[0]) { 89 | fileInfo.innerHTML = ` 90 |
91 | 92 |
93 |
支持所有类型的文件
94 |
95 |
96 | `} 97 | } else { contentGroup.style.display = "block"; editContent.required = true } 98 | }; window.editContent = function (id) { 99 | const content = contentCache.find(item => item.id === id); if (!content) return; const form = document.createElement("form"); form.className = "edit-form"; form.innerHTML = ` 100 |
101 | 102 | 103 |
104 |
105 | 106 | 111 |
112 |
113 | 114 | 115 |
116 |
117 | 118 | 119 |
120 | `; currentEditId = content.id; document.getElementById("editType").value = content.type; document.getElementById("editTitle").value = content.title; document.getElementById("editContent").value = content.content; if (content.type === "image") { const preview = document.getElementById("imagePreview"); preview.innerHTML = `预览` } handleTypeChange(content.type); document.getElementById("editModal").style.display = "block" 121 | }; function initBackToTop() { const backToTop = document.querySelector(".back-to-top"); const scrollThreshold = 400; window.addEventListener("scroll", () => { if (window.scrollY > scrollThreshold) { backToTop.classList.add("visible") } else { backToTop.classList.remove("visible") } }); backToTop.addEventListener("click", () => { window.scrollTo({ top: 0, behavior: "smooth" }) }) } window.clearAllContent = async function () { 122 | const confirmDialog = document.createElement("div"); confirmDialog.innerHTML = ` 123 |
124 |
125 |

确认清空

126 |

此操作将清空所有内容,包括:

127 |
    128 |
  • 所有文本、代码和诗歌
  • 129 |
  • 所有上传的图片
  • 130 |
  • 所有上传的文件
  • 131 |
132 |

此操作不可恢复,请确认!

133 |
134 | 135 | 136 |
137 |
138 | `; document.body.appendChild(confirmDialog) 139 | }; async function executeContentClear(button) { try { button.disabled = true; button.innerHTML = '清空中... '; const response = await fetch("/clear-all", { method: "POST", headers: { "Content-Type": "application/json" } }); if (!response.ok) { throw new Error("清空失败") } contentCache = []; renderContents([]); button.closest(".confirm-dialog").parentElement.remove(); showToast("已清空所有内容") } catch (error) { console.error("清空失败:", error); showToast("清空失败: " + error.message, "error"); button.disabled = false; button.textContent = "确认清空" } } function startUpdateCheck() { if (updateCheckInterval) { clearInterval(updateCheckInterval) } updateCheckInterval = setInterval(() => loadContents(false), syncInterval) } function stopUpdateCheck() { if (updateCheckInterval) { clearInterval(updateCheckInterval); updateCheckInterval = null } } window.addEventListener("beforeunload", () => { stopUpdateCheck() }); document.addEventListener("visibilitychange", () => { if (document.hidden) { stopUpdateCheck() } else { startUpdateCheck(); loadContents(false) } }); document.addEventListener("DOMContentLoaded", async () => { 140 | await checkPasswordProtection(); await getSyncInterval(); contentContainer = document.getElementById("content-container"); const editModal = document.getElementById("editModal"); const editForm = document.getElementById("editForm"); const addNewBtn = document.getElementById("addNewBtn"); const editImage = document.getElementById("editImage"); await loadContents(true); setupEventListeners(); startUpdateCheck(); initBackToTop(); function setupEventListeners() { if (addNewBtn) { addNewBtn.addEventListener("click", () => openModal()) } editForm.addEventListener("submit", handleFormSubmit); editImage.addEventListener("change", handleImagePreview); document.addEventListener("paste", handlePaste) } async function handlePaste(event) { const target = event.target; if (target.tagName === "TEXTAREA" || target.tagName === "INPUT" || target.isContentEditable) { return } const items = event.clipboardData?.items; if (!items) return; for (const item of items) { console.log("粘贴类型:", item.type); if (item.type.indexOf("image") !== -1) { const file = item.getAsFile(); if (file) { const dataTransfer = new DataTransfer; dataTransfer.items.add(file); currentEditId = null; const editType = document.getElementById("editType"); const editTitle = document.getElementById("editTitle"); const editImage = document.getElementById("editImage"); const imagePreview = document.getElementById("imagePreview"); editType.value = "image"; editTitle.value = `粘贴的图片_${(new Date).getTime()}.png`; editImage.files = dataTransfer.files; const reader = new FileReader; reader.onload = function (e) { imagePreview.innerHTML = `预览` }; reader.readAsDataURL(file); handleTypeChange("image"); document.getElementById("editModal").style.display = "block"; return } } else if (item.kind === "file" && !item.type.startsWith("image/")) { const file = item.getAsFile(); if (file) { const dataTransfer = new DataTransfer; dataTransfer.items.add(file); currentEditId = null; const editType = document.getElementById("editType"); const editTitle = document.getElementById("editTitle"); const editFile = document.getElementById("editFile"); editType.value = "file"; editTitle.value = file.name; editFile.files = dataTransfer.files; handleTypeChange("file"); updateFileInfo(file); document.getElementById("editModal").style.display = "block"; return } } else if (item.type === "text/plain") { item.getAsString(async text => { const isCode = detectCodeContent(text); currentEditId = null; document.getElementById("editType").value = isCode ? "code" : "text"; document.getElementById("editTitle").value = ""; document.getElementById("editContent").value = text; handleTypeChange(isCode ? "code" : "text"); document.getElementById("editModal").style.display = "block" }); return } } } function detectCodeContent(text) { const codePatterns = [/^(const|let|var|function|class|import|export|if|for|while)\s/m, /{[\s\S]*}/m, /\(\s*\)\s*=>/m, /\b(function|class)\s+\w+\s*\(/m, /\b(if|for|while)\s*\([^)]*\)/m, /\b(return|break|continue)\s/m, /[{};]\s*$/m, /^\s*(public|private|protected)\s/m, /\b(try|catch|finally)\s*{/m, /\b(async|await|Promise)\b/m, /\b(import|export)\s+.*\bfrom\s+['"][^'"]+['"]/m, /\b(const|let|var)\s+\w+\s*=\s*require\s*\(/m]; return codePatterns.some(pattern => pattern.test(text)) } function handleImagePreview(event) { const file = event.target.files[0]; if (file) { const titleInput = document.getElementById("editTitle"); titleInput.value = file.name; const reader = new FileReader; reader.onload = function (e) { const preview = document.getElementById("imagePreview"); preview.innerHTML = `预览` }; reader.readAsDataURL(file) } } window.handleFileSelect = function (event) { const file = event.target.files[0]; if (file) { const titleInput = document.getElementById("editTitle"); titleInput.value = file.name; updateFileInfo(file) } }; function updateFileInfo(file) { 141 | const fileInfo = document.querySelector(".file-info"); const fileIcon = getFileIcon(file.name); fileInfo.innerHTML = ` 142 |
143 | 144 |
145 |
${file.name}
146 |
${getFileTypeDescription(file.name)}
147 |
${formatFileSize(file.size)}
148 |
149 |
150 | `} window.openModal = function () { 151 | currentEditId = null; const editForm = document.getElementById("editForm"); const editType = document.getElementById("editType"); const editTitle = document.getElementById("editTitle"); const editContent = document.getElementById("editContent"); const imagePreview = document.getElementById("imagePreview"); const editImage = document.getElementById("editImage"); const editFile = document.getElementById("editFile"); const fileInfo = document.querySelector(".file-info"); editForm.reset(); editType.value = "text"; editTitle.value = " "; editTitle.required = true; editTitle.onblur = function () { if (!this.value.trim()) { this.value = " " } }; editContent.value = ""; imagePreview.innerHTML = ""; if (fileInfo) { 152 | fileInfo.innerHTML = ` 153 |
154 | 155 |
156 |
支持所有类型的文件
157 |
158 |
159 | `} if (editImage) { editImage.value = "" } if (editFile) { editFile.value = "" } handleTypeChange("text"); document.getElementById("editModal").style.display = "block" 160 | }; window.closeModal = function () { document.getElementById("editModal").style.display = "none"; document.getElementById("editForm").reset(); document.getElementById("imagePreview").innerHTML = ""; currentEditId = null }; async function handleFormSubmit(event) { event.preventDefault(); const submitButton = event.submitter; submitButton.disabled = true; const originalText = submitButton.textContent; submitButton.innerHTML = '保存中... '; try { const type = document.getElementById("editType").value; const titleInput = document.getElementById("editTitle"); let title = titleInput.value.trim(); if (!title) { title = " "; titleInput.value = " " } let content = ""; if (type === "image") { const imageFile = document.getElementById("editImage").files[0]; const existingContent = document.getElementById("editContent").value; if (!imageFile && existingContent) { content = existingContent } else if (imageFile) { if (!title) { document.getElementById("editTitle").value = imageFile.name } const formData = new FormData; formData.append("image", imageFile); const uploadResponse = await fetch(IMAGES_API_URL, { method: "POST", body: formData }); if (!uploadResponse.ok) { const errorData = await uploadResponse.json(); throw new Error(errorData.error || "图片上传失败") } const { url } = await uploadResponse.json(); content = url } else { throw new Error("请选择图片文件") } } else if (type === "file") { const file = document.getElementById("editFile").files[0]; const existingContent = document.getElementById("editContent").value; if (!file && existingContent) { content = existingContent } else if (file) { if (!title) { document.getElementById("editTitle").value = file.name } const formData = new FormData; formData.append("file", file); console.log("开始上传文件:", file.name); const uploadResponse = await fetch(FILES_UPLOAD_URL, { method: "POST", body: formData }); console.log("上传响应状态:", uploadResponse.status); const responseText = await uploadResponse.text(); console.log("上传响应内容:", responseText); let responseData; try { responseData = JSON.parse(responseText) } catch (e) { console.error("解析响应失败:", e); throw new Error("服务器响应格式错误") } if (!uploadResponse.ok) { throw new Error(responseData.error || "文件上传失败") } if (!responseData.url) { console.error("响应数据:", responseData); throw new Error("上传成功但未返回文件URL") } content = responseData.url; console.log("文件上传成功:", content) } else { throw new Error("请选择文件") } } else { content = document.getElementById("editContent").value } const finalTitle = document.getElementById("editTitle").value; if (!type || !finalTitle || !content) { throw new Error("请填写所有必要字段") } const formData = { type: type, title: finalTitle, content: content }; if (currentEditId) { await updateContent(currentEditId, formData) } else { await createContent(formData) } closeModal(); await loadContents(false); showToast("保存成功!") } catch (error) { console.error("保存失败:", error); showToast(error.message, "error") } finally { submitButton.disabled = false; submitButton.textContent = originalText } } async function createContent(data) { const response = await fetch(API_BASE_URL, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(data) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "创建内容失败") } return await response.json() } async function updateContent(id, data) { const response = await fetch(`${API_BASE_URL}/${id}`, { method: "PUT", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(data) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "更新内容败") } return await response.json() } 161 | }); -------------------------------------------------------------------------------- /js/theme.js: -------------------------------------------------------------------------------- 1 | function initTheme() { const hoverArea = document.createElement("div"); hoverArea.className = "theme-toggle-hover-area"; const themeToggle = document.createElement("button"); themeToggle.className = "theme-toggle"; themeToggle.setAttribute("aria-label", "切换深色模式"); hoverArea.appendChild(themeToggle); document.body.appendChild(hoverArea); const savedTheme = localStorage.getItem("theme") || "light"; document.documentElement.setAttribute("data-theme", savedTheme); themeToggle.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme"); const newTheme = currentTheme === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", newTheme); localStorage.setItem("theme", newTheme) }) } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initTheme) } else { initTheme() } -------------------------------------------------------------------------------- /js/未压缩-main.js: -------------------------------------------------------------------------------- 1 | // API配置 2 | const API_BASE_URL = '/contents'; 3 | const IMAGES_API_URL = '/images'; 4 | const FILES_API_URL = '/files'; 5 | const FILES_UPLOAD_URL = '/files/upload'; 6 | const DOWNLOAD_API_URL = '/download'; 7 | 8 | // 密码验证相关 9 | const PASSWORD_VERIFIED_KEY = 'password_verified'; 10 | const PASSWORD_VERIFIED_EXPIRY_KEY = 'password_verified_expiry'; 11 | const VERIFY_EXPIRY_DAYS = 15; 12 | 13 | // 添加内容缓存相关的常量 14 | const CONTENT_CACHE_KEY = 'content_cache'; // 内容缓存的key 15 | const CONTENT_CACHE_EXPIRY_KEY = 'content_cache_expiry'; // 内容缓存过期时间的key 16 | const CACHE_EXPIRY_DAYS = 15; // 缓存有效期7天 17 | 18 | // 检查密码验证状态 19 | async function checkPasswordProtection() { 20 | try { 21 | const response = await fetch('/_vars/ACCESS_PASSWORD'); 22 | // 如果返回 204,说明未设置密码,不需要验证 23 | if (response.status === 204) { 24 | return true; 25 | } 26 | 27 | if (!response.ok) { 28 | console.error('获取密码配置失败:', response.status); 29 | return true; // 出错时默认允许访问 30 | } 31 | 32 | const verified = localStorage.getItem(PASSWORD_VERIFIED_KEY); 33 | const expiry = localStorage.getItem(PASSWORD_VERIFIED_EXPIRY_KEY); 34 | 35 | if (verified && expiry && new Date().getTime() < parseInt(expiry)) { 36 | return true; 37 | } 38 | 39 | document.getElementById('passwordOverlay').style.display = 'flex'; 40 | document.getElementById('mainContent').classList.add('content-blur'); 41 | document.body.classList.add('password-active'); 42 | return false; 43 | } catch (error) { 44 | console.error('检查密码保护失败:', error); 45 | return true; // 出错时默认允许访问 46 | } 47 | } 48 | 49 | // 验证密码 50 | async function verifyPassword(event) { 51 | if (event) { 52 | event.preventDefault(); 53 | } 54 | const passwordInput = document.getElementById('accessPassword'); 55 | const password = passwordInput.value; 56 | 57 | try { 58 | const response = await fetch('/_vars/ACCESS_PASSWORD'); 59 | if (!response.ok) { 60 | throw new Error('获取密码失败'); 61 | } 62 | 63 | const correctPassword = await response.text(); 64 | 65 | if (password === correctPassword) { 66 | const expiryDate = new Date(); 67 | expiryDate.setDate(expiryDate.getDate() + VERIFY_EXPIRY_DAYS); 68 | 69 | localStorage.setItem(PASSWORD_VERIFIED_KEY, 'true'); 70 | localStorage.setItem(PASSWORD_VERIFIED_EXPIRY_KEY, expiryDate.getTime().toString()); 71 | 72 | document.getElementById('passwordOverlay').style.display = 'none'; 73 | document.getElementById('mainContent').classList.remove('content-blur'); 74 | document.body.classList.remove('password-active'); 75 | showToast('验证成功!'); 76 | } else { 77 | showToast('密码错误!', 'error'); 78 | passwordInput.value = ''; 79 | } 80 | } catch (error) { 81 | console.error('密码验证失败:', error); 82 | showToast('验证失败: ' + error.message, 'error'); 83 | } 84 | } 85 | 86 | // 监听回车键 87 | document.addEventListener('keypress', function (e) { 88 | if (e.key === 'Enter' && document.getElementById('passwordOverlay').style.display !== 'none') { 89 | verifyPassword(); 90 | } 91 | }); 92 | 93 | // 全局变量 94 | let currentEditId = null; 95 | let lastUpdateTime = Date.now(); 96 | let updateCheckInterval = null; // 修改为 null 初始值 97 | let contentCache = []; 98 | let contentContainer; 99 | let syncInterval = 30000; // 默认30秒 100 | let zoomInstance = null; // 追踪灯箱实例 101 | const SYNC_INTERVAL_KEY = 'sync_interval'; // 本地存储的key 102 | const SYNC_INTERVAL_EXPIRY_KEY = 'sync_interval_expiry'; // 过期时间的key 103 | 104 | // 获取同步间隔配置 105 | async function getSyncInterval() { 106 | try { 107 | // 检查本地存储的值是否有效 108 | const savedInterval = localStorage.getItem(SYNC_INTERVAL_KEY); 109 | const expiry = localStorage.getItem(SYNC_INTERVAL_EXPIRY_KEY); 110 | 111 | if (savedInterval && expiry && new Date().getTime() < parseInt(expiry)) { 112 | // 如果本地存储的值未过期,直接使用 113 | const parsedInterval = parseInt(savedInterval); 114 | if (!isNaN(parsedInterval) && parsedInterval >= 5000) { 115 | syncInterval = parsedInterval; 116 | console.log('从本地存储加载同步间隔:', syncInterval, 'ms'); 117 | return; 118 | } 119 | } 120 | 121 | // 如果本地没有有效值,从服务器获取 122 | const response = await fetch('/_vars/SYNC_INTERVAL'); 123 | if (response.ok) { 124 | const interval = await response.text(); 125 | // 确保interval是一个有效的数字且不小于5秒 126 | const parsedInterval = parseInt(interval); 127 | if (!isNaN(parsedInterval) && parsedInterval >= 5000) { 128 | syncInterval = parsedInterval; 129 | 130 | // 保存到本地存储,设置7天过期时间 131 | const expiryDate = new Date(); 132 | expiryDate.setDate(expiryDate.getDate() + 7); 133 | localStorage.setItem(SYNC_INTERVAL_KEY, syncInterval.toString()); 134 | localStorage.setItem(SYNC_INTERVAL_EXPIRY_KEY, expiryDate.getTime().toString()); 135 | 136 | console.log('从服务器加载同步间隔:', syncInterval, 'ms'); 137 | } 138 | } 139 | } catch (error) { 140 | console.warn('无法获取同步间隔配置,使用默认值:', syncInterval, 'ms'); 141 | } 142 | } 143 | 144 | // 工具函数 145 | function getFileIcon(filename) { 146 | // 获取文件拓展名 147 | const ext = filename.toLowerCase().split('.').pop(); 148 | 149 | // Markdown文件 150 | if (['md', 'markdown', 'mdown', 'mkd'].includes(ext)) return 'markdown'; 151 | 152 | // 图片文件 153 | if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tiff', 'heic'].includes(ext)) return 'image'; 154 | 155 | // 文档文件 156 | if (ext === 'pdf') return 'pdf'; 157 | if (['doc', 'docx', 'rtf', 'odt', 'pages'].includes(ext)) return 'word'; 158 | if (['xls', 'xlsx', 'csv', 'ods', 'numbers'].includes(ext)) return 'excel'; 159 | if (['ppt', 'pptx', 'odp', 'key'].includes(ext)) return 'powerpoint'; 160 | if (['txt', 'log', 'ini', 'conf', 'cfg'].includes(ext)) return 'text'; 161 | 162 | // 应用程序文件 163 | if (ext === 'exe') return 'windows'; 164 | if (ext === 'msi') return 'windows-installer'; 165 | if (ext === 'apk') return 'android'; 166 | if (ext === 'app' || ext === 'dmg') return 'macos'; 167 | if (ext === 'deb' || ext === 'rpm') return 'linux'; 168 | if (['appx', 'msix'].includes(ext)) return 'windows-store'; 169 | if (['ipa', 'pkg'].includes(ext)) return 'ios'; 170 | 171 | // 代码文件 172 | if (['js', 'ts', 'jsx', 'tsx', 'json', 'html', 'css', 'scss', 'less', 'sass', 'php', 'py', 'java', 'c', 'cpp', 'cs', 'go', 'rb', 'swift', 'kt', 'rs', 'dart', 'vue', 'sql', 'sh', 'bash', 'yml', 'yaml', 'xml'].includes(ext)) return 'code'; 173 | 174 | // 压缩文件 175 | if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'tgz'].includes(ext)) return 'archive'; 176 | 177 | // 视频文件 178 | if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'm4v', '3gp', 'mpg', 'mpeg', 'ogv'].includes(ext)) return 'video'; 179 | 180 | // 音频文件 181 | if (['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus', 'mid', 'midi'].includes(ext)) return 'audio'; 182 | 183 | return 'generic'; 184 | } 185 | 186 | function getFileTypeDescription(filename) { 187 | // 获取文件扩展名 188 | const ext = filename.toLowerCase().split('.').pop(); 189 | 190 | // Markdown文件 191 | if (['md', 'markdown', 'mdown', 'mkd'].includes(ext)) return 'Markdown文档'; 192 | 193 | // 图片文件 194 | if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tiff', 'heic'].includes(ext)) return '图片文件'; 195 | 196 | // 文档文件 197 | if (ext === 'pdf') return 'PDF文档'; 198 | if (['doc', 'docx', 'rtf', 'odt', 'pages'].includes(ext)) return 'Word文档'; 199 | if (['xls', 'xlsx', 'csv', 'ods', 'numbers'].includes(ext)) return 'Excel表格'; 200 | if (['ppt', 'pptx', 'odp', 'key'].includes(ext)) return 'PowerPoint演示文稿'; 201 | if (['txt', 'log'].includes(ext)) return '文本文件'; 202 | if (['ini', 'conf', 'cfg'].includes(ext)) return '配置文件'; 203 | 204 | // 应用程序文件 205 | if (ext === 'exe') return 'Windows可执行程序'; 206 | if (ext === 'msi') return 'Windows安装程序'; 207 | if (ext === 'apk') return 'Android应用程序'; 208 | if (ext === 'app') return 'macOS应用程序'; 209 | if (ext === 'dmg') return 'macOS安装镜像'; 210 | if (ext === 'deb') return 'Debian/Ubuntu安装包'; 211 | if (ext === 'rpm') return 'RedHat/Fedora安装包'; 212 | if (['appx', 'msix'].includes(ext)) return 'Windows商店应用'; 213 | if (ext === 'ipa') return 'iOS应用程序'; 214 | if (ext === 'pkg') return 'macOS安装包'; 215 | 216 | // 代码文件 217 | if (['js', 'ts'].includes(ext)) return 'JavaScript/TypeScript文件'; 218 | if (['jsx', 'tsx'].includes(ext)) return 'React组件'; 219 | if (ext === 'vue') return 'Vue组件'; 220 | if (ext === 'html') return 'HTML文件'; 221 | if (['css', 'scss', 'less', 'sass'].includes(ext)) return '样式表'; 222 | if (ext === 'php') return 'PHP文件'; 223 | if (ext === 'py') return 'Python文件'; 224 | if (ext === 'java') return 'Java文件'; 225 | if (['c', 'cpp'].includes(ext)) return 'C/C++文件'; 226 | if (ext === 'cs') return 'C#文件'; 227 | if (ext === 'go') return 'Go文件'; 228 | if (ext === 'rb') return 'Ruby文件'; 229 | if (ext === 'swift') return 'Swift文件'; 230 | if (ext === 'kt') return 'Kotlin文件'; 231 | if (ext === 'rs') return 'Rust文件'; 232 | if (ext === 'dart') return 'Dart文件'; 233 | if (ext === 'sql') return 'SQL文件'; 234 | if (['sh', 'bash'].includes(ext)) return 'Shell文本'; 235 | if (['yml', 'yaml'].includes(ext)) return 'YAML配置'; 236 | if (ext === 'xml') return 'XML文件'; 237 | 238 | // 压缩文件 239 | if (['zip', 'rar', '7z'].includes(ext)) return '压缩文件'; 240 | if (['tar', 'gz', 'bz2', 'xz', 'tgz'].includes(ext)) return '归档文件'; 241 | 242 | // 视频文件 243 | if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'm4v', '3gp', 'mpg', 'mpeg', 'ogv'].includes(ext)) return '视频文件'; 244 | 245 | // 音频文件 246 | if (['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus'].includes(ext)) return '音频文件'; 247 | if (['mid', 'midi'].includes(ext)) return 'MIDI音乐'; 248 | 249 | return `${ext.toUpperCase()}文件`; 250 | } 251 | 252 | function formatFileSize(bytes) { 253 | if (!bytes || bytes === 0) return '未知大小'; 254 | const k = 1024; 255 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; 256 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 257 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 258 | } 259 | 260 | function encodeContent(text) { 261 | return btoa(unescape(encodeURIComponent(text))); 262 | } 263 | 264 | function decodeContent(encoded) { 265 | return decodeURIComponent(escape(atob(encoded))); 266 | } 267 | 268 | // 显示提示函数 269 | function showToast(message, type = 'success') { 270 | const existingToast = document.querySelector('.toast'); 271 | if (existingToast) { 272 | existingToast.remove(); 273 | } 274 | 275 | const toast = document.createElement('div'); 276 | toast.className = `toast ${type}`; 277 | toast.textContent = message; 278 | document.body.appendChild(toast); 279 | 280 | requestAnimationFrame(() => { 281 | toast.classList.add('show'); 282 | }); 283 | 284 | setTimeout(() => { 285 | toast.classList.add('fade-out'); 286 | setTimeout(() => toast.remove(), 300); 287 | }, 2000); 288 | } 289 | 290 | // 图片预览函数 291 | function previewImage(input) { 292 | const preview = document.getElementById('imagePreview'); 293 | preview.innerHTML = ''; // 清空预览区域 294 | 295 | if (input.files && input.files[0]) { 296 | const file = input.files[0]; 297 | const img = document.createElement('img'); 298 | img.alt = '预览'; 299 | img.onload = function () { 300 | window.URL.revokeObjectURL(this.src); // 清理旧的 URL 301 | }; 302 | img.src = window.URL.createObjectURL(file); 303 | preview.appendChild(img); 304 | } 305 | } 306 | 307 | // 复制函数 308 | function copyText(encodedText, type) { 309 | const text = decodeContent(encodedText); 310 | let copyContent = text; 311 | 312 | if (type === 'poetry') { 313 | copyContent = text.split('\n').join('\r\n'); 314 | } else if (type === 'image') { 315 | copyContent = text; 316 | } 317 | 318 | navigator.clipboard.writeText(copyContent).then(() => { 319 | showToast('复制成功!'); 320 | }).catch(() => { 321 | const textarea = document.createElement('textarea'); 322 | textarea.value = copyContent; 323 | document.body.appendChild(textarea); 324 | textarea.select(); 325 | try { 326 | document.execCommand('copy'); 327 | showToast('复制成功!'); 328 | } catch (e) { 329 | showToast('复制失败,请手动复制', 'error'); 330 | } 331 | document.body.removeChild(textarea); 332 | }); 333 | } 334 | 335 | // 显示确认对话框 336 | function showConfirmDialog(title, message) { 337 | return new Promise((resolve) => { 338 | const wrapper = document.createElement('div'); 339 | wrapper.innerHTML = ` 340 |
341 |
342 |
343 |
${title}
344 |
${message}
345 |
346 | 347 | 348 |
349 |
350 |
351 | `; 352 | 353 | document.body.appendChild(wrapper); 354 | 355 | const buttons = wrapper.querySelectorAll('.btn'); 356 | buttons.forEach(button => { 357 | button.addEventListener('click', () => { 358 | wrapper.remove(); 359 | resolve(button.classList.contains('btn-primary')); 360 | }); 361 | }); 362 | }); 363 | } 364 | 365 | // 获取文件图标URL 366 | function getFileIconUrl(filename) { 367 | // 获取文件扩展名 368 | const ext = filename.toLowerCase().split('.').pop(); 369 | // 使用在线图标服务 370 | return `https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@main/icons/${ext}.svg`; 371 | } 372 | 373 | // 下载文件函数 374 | async function downloadFile(url, filename) { 375 | try { 376 | showToast('准备下载文件...'); 377 | const response = await fetch(url, { 378 | method: 'GET', 379 | headers: { 380 | 'Accept': '*/*' 381 | } 382 | }); 383 | 384 | if (!response.ok) { 385 | throw new Error(`下载失败: ${response.status} ${response.statusText}`); 386 | } 387 | 388 | // 获取响应头中的文件名 389 | const contentDisposition = response.headers.get('content-disposition'); 390 | const match = contentDisposition?.match(/filename="(.+)"/); 391 | const actualFilename = match ? decodeURIComponent(match[1]) : filename; 392 | 393 | // 使用 streams API 处理大文件下载 394 | const reader = response.body.getReader(); 395 | const contentLength = response.headers.get('content-length'); 396 | let receivedLength = 0; 397 | const chunks = []; 398 | 399 | while (true) { 400 | const { done, value } = await reader.read(); 401 | 402 | if (done) { 403 | break; 404 | } 405 | 406 | chunks.push(value); 407 | receivedLength += value.length; 408 | 409 | // 更新下载进度 410 | if (contentLength) { 411 | const progress = ((receivedLength / contentLength) * 100).toFixed(2); 412 | showToast(`下载进度: ${progress}%`); 413 | } 414 | } 415 | 416 | // 合并所有chunks 417 | const blob = new Blob(chunks); 418 | const blobUrl = window.URL.createObjectURL(blob); 419 | 420 | const link = document.createElement('a'); 421 | link.href = blobUrl; 422 | link.download = actualFilename; 423 | document.body.appendChild(link); 424 | link.click(); 425 | document.body.removeChild(link); 426 | window.URL.revokeObjectURL(blobUrl); 427 | 428 | showToast('文件下载完成'); 429 | } catch (error) { 430 | console.error('下载失败:', error); 431 | showToast('下载失败: ' + error.message, 'error'); 432 | } 433 | } 434 | 435 | // 格式化日期 436 | function formatDate(timestamp) { 437 | const date = new Date(timestamp); 438 | const beijingDate = new Date(date.getTime() + 8 * 60 * 60 * 1000); 439 | const year = beijingDate.getFullYear(); 440 | const month = String(beijingDate.getMonth() + 1).padStart(2, '0'); 441 | const day = String(beijingDate.getDate()).padStart(2, '0'); 442 | const hours = String(beijingDate.getHours()).padStart(2, '0'); 443 | const minutes = String(beijingDate.getMinutes()).padStart(2, '0'); 444 | return `${year}-${month}-${day} ${hours}:${minutes}`; 445 | } 446 | 447 | // 初始化 markdown-it 448 | const md = window.markdownit({ 449 | html: true, // 启用 HTML 标签 450 | breaks: true, // 转换换行符为
451 | linkify: true, // 自动转换 URL 为链接 452 | typographer: true, // 启用一些语言中性的替换和引号美化 453 | quotes: ['""', '\'\''] // 引号样式 454 | }).use(window.markdownitEmoji) // 启用表情 455 | .use(window.markdownitSub) // 启用下标 456 | .use(window.markdownitSup) // 启用上标 457 | .use(window.markdownitFootnote) // 启用脚注 458 | .use(window.markdownitTaskLists, { // 启用任务列表 459 | enabled: true, 460 | label: true, 461 | labelAfter: true 462 | }); 463 | 464 | // 初始化灯箱效果 465 | const zoom = mediumZoom('[data-zoomable]', { 466 | margin: 48, 467 | background: 'rgba(0, 0, 0, 0.9)', 468 | scrollOffset: 0, 469 | container: document.body, 470 | template: null, 471 | transition: { 472 | duration: 400, 473 | timing: 'cubic-bezier(0.4, 0, 0.2, 1)' 474 | } 475 | }); 476 | 477 | // 自定义图片渲染规则 478 | md.renderer.rules.image = function (tokens, idx, options, env, slf) { 479 | const token = tokens[idx]; 480 | const src = token.attrGet('src'); 481 | const alt = token.content || ''; 482 | const title = token.attrGet('title') || ''; 483 | 484 | return `${alt}`; 485 | }; 486 | 487 | // 添加视频链接解析规则 488 | function parseVideoUrl(url) { 489 | // 普通视频文件扩展名 490 | const videoExtensions = /\.(mp4|mkv|webm|avi|mov|wmv|flv)$/i; 491 | if (videoExtensions.test(url)) { 492 | return { 493 | type: 'video', 494 | url: url 495 | }; 496 | } 497 | 498 | // YouTube(支持普通视频和shorts) 499 | const youtubeMatch = url.match(/(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^?&\s]+)/); 500 | if (youtubeMatch) { 501 | return { 502 | type: 'youtube', 503 | id: youtubeMatch[1], 504 | embed: `https://www.youtube.com/embed/${youtubeMatch[1]}` 505 | }; 506 | } 507 | 508 | // 哔哩哔哩 509 | const bilibiliMatch = url.match(/(?:bilibili\.com\/video\/)([^?&\s/]+)/); 510 | if (bilibiliMatch) { 511 | return { 512 | type: 'bilibili', 513 | id: bilibiliMatch[1], 514 | embed: `//player.bilibili.com/player.html?bvid=${bilibiliMatch[1]}&page=1` 515 | }; 516 | } 517 | 518 | return null; 519 | } 520 | 521 | // 自定义链接渲染规则 522 | md.renderer.rules.link_open = function (tokens, idx, options, env, self) { 523 | const token = tokens[idx]; 524 | const href = token.attrGet('href'); 525 | 526 | if (href) { 527 | const video = parseVideoUrl(href); 528 | if (video) { 529 | // 设置标记,告诉 link_close 这是一个视频链接 530 | token.video = video; 531 | return ''; // 不渲染开始标签 532 | } 533 | // 为普通链接添加新标签页打开属性 534 | token.attrPush(['target', '_blank']); 535 | token.attrPush(['rel', 'noopener noreferrer']); 536 | } 537 | 538 | return self.renderToken(tokens, idx, options); 539 | }; 540 | 541 | md.renderer.rules.link_close = function (tokens, idx, options, env, self) { 542 | // 检查 idx-2 是否在有效范围内 543 | if (idx >= 2 && tokens[idx - 2]) { 544 | const openToken = tokens[idx - 2]; 545 | if (openToken && openToken.video) { 546 | const video = openToken.video; 547 | if (video.type === 'youtube') { 548 | return `
549 | 554 |
`; 555 | } else if (video.type === 'bilibili') { 556 | return `
557 | 561 |
`; 562 | } else if (video.type === 'video') { 563 | return `
564 | 568 |
`; 569 | } 570 | } 571 | } 572 | 573 | return self.renderToken(tokens, idx, options); 574 | }; 575 | 576 | // 自定义代码块渲染规则 577 | md.renderer.rules.fence = function (tokens, idx, options, env, slf) { 578 | const token = tokens[idx]; 579 | const code = token.content; 580 | const lang = token.info || ''; 581 | const highlighted = Prism.highlight(code, Prism.languages[lang] || Prism.languages.plain, lang); 582 | 583 | return `
584 |
${highlighted}
585 | 586 |
`; 587 | }; 588 | 589 | // 复制代码函数 590 | window.copyCode = function (button) { 591 | const pre = button.parentElement.querySelector('pre'); 592 | const code = pre.textContent; 593 | 594 | navigator.clipboard.writeText(code).then(() => { 595 | const originalText = button.textContent; 596 | button.textContent = '已复制!'; 597 | button.style.background = '#4CAF50'; 598 | button.style.color = 'white'; 599 | 600 | setTimeout(() => { 601 | button.textContent = originalText; 602 | button.style.background = ''; 603 | button.style.color = ''; 604 | }, 2000); 605 | }).catch(err => { 606 | console.error('复制失败:', err); 607 | showToast('复制失败,请手动复制', 'error'); 608 | }); 609 | }; 610 | 611 | // 渲染内容函数 612 | function renderContents(contents) { 613 | if (!contentContainer) { 614 | contentContainer = document.getElementById('content-container'); 615 | } 616 | 617 | if (!contents || contents.length === 0) { 618 | contentContainer.innerHTML = ` 619 |
620 |
📝
621 |
还没有任何内容
622 |
点击"添加内容"开始创建
623 |
624 | `; 625 | return; 626 | } 627 | 628 | // 使用DocumentFragment提升性能 629 | const fragment = document.createDocumentFragment(); 630 | contents.forEach(content => { 631 | const section = document.createElement('section'); 632 | section.className = 'text-block'; 633 | 634 | let contentHtml = ''; 635 | let downloadButton = ''; 636 | 637 | try { 638 | if (content.type === 'image' || content.type === 'file') { 639 | if (content.type === 'image') { 640 | contentHtml = `
${content.title}
`; 641 | } else { 642 | const fileIcon = getFileIcon(content.title); 643 | const fileType = getFileTypeDescription(content.title); 644 | contentHtml = ` 645 |
646 | 647 |
648 |
${content.title}
649 |
${fileType}
650 |
651 |
`; 652 | } 653 | downloadButton = ``; 654 | } else if (content.type === 'code') { 655 | const escapedContent = content.content 656 | .replace(/&/g, '&') 657 | .replace(//g, '>') 659 | .replace(/"/g, '"') 660 | .replace(/'/g, '''); 661 | contentHtml = `
${escapedContent}
`; 662 | } else if (content.type === 'poetry') { 663 | contentHtml = content.content 664 | .split('\n') 665 | .map(line => `

${line.replace(/&/g, '&') 666 | .replace(//g, '>') 668 | .replace(/"/g, '"') 669 | .replace(/'/g, ''')}

`) 670 | .join(''); 671 | } else { 672 | contentHtml = md.render(content.content); 673 | } 674 | } catch (error) { 675 | console.error('Card rendering error:', content.id, error); 676 | contentHtml = `
内容渲染失败
`; 677 | } 678 | 679 | const encodedContent = encodeContent(content.content); 680 | const modifiedDate = formatDate(content.updatedAt || content.createdAt || Date.now()); 681 | 682 | section.innerHTML = ` 683 |
684 |

${content.title}

685 |
686 | 修改于 ${modifiedDate} 687 |
688 |
689 |
690 | ${contentHtml} 691 |
692 |
693 | 694 | ${downloadButton} 695 | 696 | 697 |
698 | `; 699 | 700 | fragment.appendChild(section); 701 | }); 702 | 703 | // 一次性更新DOM 704 | contentContainer.innerHTML = ''; 705 | contentContainer.appendChild(fragment); 706 | 707 | // 初始化功能 708 | requestAnimationFrame(() => { 709 | Prism.highlightAll(); 710 | // 重新绑定灯箱效果 711 | zoom.detach(); 712 | zoom.attach('[data-zoomable]'); 713 | }); 714 | } 715 | 716 | // 修改加载内容函数 717 | async function loadContents(showLoading = true) { 718 | if (!contentContainer) { 719 | contentContainer = document.getElementById('content-container'); 720 | } 721 | 722 | try { 723 | // 首先尝试从本地缓存加载 724 | const cachedContent = localStorage.getItem(CONTENT_CACHE_KEY); 725 | const cacheExpiry = localStorage.getItem(CONTENT_CACHE_EXPIRY_KEY); 726 | 727 | if (cachedContent && cacheExpiry && new Date().getTime() < parseInt(cacheExpiry)) { 728 | // 如果缓存有效,使用缓存的内容 729 | const newContent = JSON.parse(cachedContent); 730 | // 只在第一次加载或内容为空时渲染 731 | if (!contentCache || contentCache.length === 0) { 732 | contentCache = newContent; 733 | await renderContents(contentCache); 734 | console.log('从本地缓存加载内容'); 735 | } 736 | 737 | // 在后台更新内容 738 | fetchAndUpdateContent(false); 739 | return; 740 | } 741 | 742 | // 如果没有有效缓存,从服务器获取 743 | await fetchAndUpdateContent(showLoading); 744 | 745 | } catch (error) { 746 | console.error('加载内容失败:', error); 747 | if (showLoading) { 748 | showError(`加载内容失败: ${error.message}`); 749 | } 750 | } 751 | } 752 | 753 | // 从服务器获取并更新内容的函数 754 | async function fetchAndUpdateContent(showLoading = true) { 755 | const response = await fetch(API_BASE_URL, { 756 | headers: { 757 | 'Accept': 'application/json' 758 | } 759 | }); 760 | 761 | if (!response.ok) { 762 | const data = await response.json(); 763 | throw new Error(data.details || data.error || '加载失败'); 764 | } 765 | 766 | const data = await response.json(); 767 | 768 | // 比较新旧内容是否真的发生了变化 769 | const hasContentChanged = JSON.stringify(contentCache) !== JSON.stringify(data); 770 | 771 | if (hasContentChanged) { 772 | console.log('检测到内容变化,更新界面'); 773 | contentCache = data || []; 774 | await renderContents(contentCache); 775 | 776 | // 更新本地缓存 777 | const expiryDate = new Date(); 778 | expiryDate.setDate(expiryDate.getDate() + CACHE_EXPIRY_DAYS); 779 | localStorage.setItem(CONTENT_CACHE_KEY, JSON.stringify(contentCache)); 780 | localStorage.setItem(CONTENT_CACHE_EXPIRY_KEY, expiryDate.getTime().toString()); 781 | console.log('内容已更新并缓存'); 782 | } else { 783 | console.log('内容未发生变化,保持当前显示'); 784 | } 785 | 786 | lastUpdateTime = Date.now(); 787 | } 788 | 789 | // 修改删除内容函数,删除后同时更新缓存 790 | window.deleteContent = async function (id) { 791 | const confirmed = await showConfirmDialog( 792 | '确认删除', 793 | '确定要删除这条内容吗?此操作无法撤销。' 794 | ); 795 | 796 | if (confirmed) { 797 | try { 798 | const response = await fetch(`${API_BASE_URL}/${id}`, { 799 | method: 'DELETE', 800 | headers: { 801 | 'Accept': 'application/json' 802 | } 803 | }); 804 | 805 | if (!response.ok) { 806 | const data = await response.json(); 807 | throw new Error(data.error || '删除失败'); 808 | } 809 | 810 | // 更新内容缓存 811 | contentCache = contentCache.filter(item => item.id !== id); 812 | renderContents(contentCache); 813 | 814 | // 更新本地存储 815 | localStorage.setItem(CONTENT_CACHE_KEY, JSON.stringify(contentCache)); 816 | 817 | showToast('删除成功!'); 818 | } catch (error) { 819 | console.error('删除失败:', error); 820 | showToast(error.message, 'error'); 821 | } 822 | } 823 | } 824 | 825 | // 类型切换函数 826 | window.handleTypeChange = function (type) { 827 | const contentGroup = document.getElementById('contentGroup'); 828 | const imageGroup = document.getElementById('imageGroup'); 829 | const fileGroup = document.getElementById('fileGroup'); 830 | const editContent = document.getElementById('editContent'); 831 | const editImage = document.getElementById('editImage'); 832 | const editFile = document.getElementById('editFile'); 833 | const titleInput = document.getElementById('editTitle'); 834 | const titleGroup = document.getElementById('titleGroup'); 835 | const fileInfo = document.querySelector('.file-info'); 836 | 837 | contentGroup.style.display = 'none'; 838 | imageGroup.style.display = 'none'; 839 | fileGroup.style.display = 'none'; 840 | titleGroup.style.display = 'block'; 841 | editContent.required = false; 842 | editImage.required = false; 843 | editFile.required = false; 844 | titleInput.required = false; 845 | 846 | if (type === 'image') { 847 | imageGroup.style.display = 'block'; 848 | editImage.required = true; 849 | titleGroup.style.display = 'none'; 850 | } else if (type === 'file') { 851 | fileGroup.style.display = 'block'; 852 | editFile.required = true; 853 | 854 | // 如果没有选择文件,显示默认的文件信息 855 | if (!editFile.files || !editFile.files[0]) { 856 | fileInfo.innerHTML = ` 857 |
858 | 859 |
860 |
支持所有类型的文件
861 |
862 |
863 | `; 864 | } 865 | } else { 866 | contentGroup.style.display = 'block'; 867 | editContent.required = true; 868 | } 869 | } 870 | 871 | // 编辑内容函数 872 | window.editContent = function (id) { 873 | const content = contentCache.find(item => item.id === id); 874 | if (!content) return; 875 | 876 | const form = document.createElement('form'); 877 | form.className = 'edit-form'; 878 | form.innerHTML = ` 879 |
880 | 881 | 882 |
883 |
884 | 885 | 890 |
891 |
892 | 893 | 894 |
895 |
896 | 897 | 898 |
899 | `; 900 | 901 | currentEditId = content.id; 902 | document.getElementById('editType').value = content.type; 903 | document.getElementById('editTitle').value = content.title; 904 | document.getElementById('editContent').value = content.content; 905 | 906 | // 如果是图片类型,显示预览 907 | if (content.type === 'image') { 908 | const preview = document.getElementById('imagePreview'); 909 | preview.innerHTML = `预览`; 910 | } 911 | 912 | handleTypeChange(content.type); 913 | document.getElementById('editModal').style.display = 'block'; 914 | } 915 | 916 | // 初始化返回顶部按钮 917 | function initBackToTop() { 918 | const backToTop = document.querySelector('.back-to-top'); 919 | const scrollThreshold = 400; // 滚动多少像素后显示按钮 920 | 921 | // 监听滚动事件 922 | window.addEventListener('scroll', () => { 923 | if (window.scrollY > scrollThreshold) { 924 | backToTop.classList.add('visible'); 925 | } else { 926 | backToTop.classList.remove('visible'); 927 | } 928 | }); 929 | 930 | // 点击返回顶部 931 | backToTop.addEventListener('click', () => { 932 | window.scrollTo({ 933 | top: 0, 934 | behavior: 'smooth' 935 | }); 936 | }); 937 | } 938 | 939 | // 清空全部内容 940 | window.clearAllContent = async function () { 941 | const confirmDialog = document.createElement('div'); 942 | confirmDialog.innerHTML = ` 943 |
944 |
945 |

确认清空

946 |

此操作将清空所有内容,包括:

947 |
    948 |
  • 所有文本、代码和诗歌
  • 949 |
  • 所有上传的图片
  • 950 |
  • 所有上传的文件
  • 951 |
952 |

此操作不可恢复,请确认!

953 |
954 | 955 | 956 |
957 |
958 | `; 959 | document.body.appendChild(confirmDialog); 960 | }; 961 | 962 | // 执行清空操作 963 | async function executeContentClear(button) { 964 | try { 965 | button.disabled = true; 966 | button.innerHTML = '清空中... '; 967 | 968 | // 清空数据库内容 969 | const response = await fetch('/clear-all', { 970 | method: 'POST', 971 | headers: { 972 | 'Content-Type': 'application/json' 973 | } 974 | }); 975 | 976 | if (!response.ok) { 977 | throw new Error('清空失败'); 978 | } 979 | 980 | // 清空本地缓存 981 | contentCache = []; 982 | 983 | // 重新渲染内容(显示空状态) 984 | renderContents([]); 985 | 986 | // 关闭确认对话框 987 | button.closest('.confirm-dialog').parentElement.remove(); 988 | 989 | showToast('已清空所有内容'); 990 | } catch (error) { 991 | console.error('清空失败:', error); 992 | showToast('清空失败: ' + error.message, 'error'); 993 | button.disabled = false; 994 | button.textContent = '确认清空'; 995 | } 996 | } 997 | 998 | // 开始更新检查 999 | function startUpdateCheck() { 1000 | // 先清除可能存在的旧定时器 1001 | if (updateCheckInterval) { 1002 | clearInterval(updateCheckInterval); 1003 | } 1004 | updateCheckInterval = setInterval(() => loadContents(false), syncInterval); 1005 | } 1006 | 1007 | // 停止更新检查 1008 | function stopUpdateCheck() { 1009 | if (updateCheckInterval) { 1010 | clearInterval(updateCheckInterval); 1011 | updateCheckInterval = null; 1012 | } 1013 | } 1014 | 1015 | // 添加页面卸载时的清理 1016 | window.addEventListener('beforeunload', () => { 1017 | stopUpdateCheck(); 1018 | }); 1019 | 1020 | // 添加页面可见性变化的处理 1021 | document.addEventListener('visibilitychange', () => { 1022 | if (document.hidden) { 1023 | // 页面不可见时停止检查 1024 | stopUpdateCheck(); 1025 | } else { 1026 | // 页面可见时重新开始检查 1027 | startUpdateCheck(); 1028 | // 立即检查一次更新 1029 | loadContents(false); 1030 | } 1031 | }); 1032 | 1033 | // DOM元素 1034 | document.addEventListener('DOMContentLoaded', async () => { 1035 | // 检查密码保护 1036 | await checkPasswordProtection(); 1037 | 1038 | // 初始化前先获取同步间隔 1039 | await getSyncInterval(); 1040 | 1041 | contentContainer = document.getElementById('content-container'); 1042 | const editModal = document.getElementById('editModal'); 1043 | const editForm = document.getElementById('editForm'); 1044 | const addNewBtn = document.getElementById('addNewBtn'); 1045 | const editImage = document.getElementById('editImage'); 1046 | 1047 | // 初始化 1048 | await loadContents(true); 1049 | setupEventListeners(); 1050 | startUpdateCheck(); // 开始定时检查 1051 | initBackToTop(); 1052 | 1053 | // 设置事件监听器 1054 | function setupEventListeners() { 1055 | if (addNewBtn) { 1056 | addNewBtn.addEventListener('click', () => openModal()); 1057 | } 1058 | editForm.addEventListener('submit', handleFormSubmit); 1059 | editImage.addEventListener('change', handleImagePreview); 1060 | 1061 | // 添加全局粘贴事件监听 1062 | document.addEventListener('paste', handlePaste); 1063 | } 1064 | 1065 | // 处理粘贴事件 1066 | async function handlePaste(event) { 1067 | // 检查粘贴事件的目标元素 1068 | const target = event.target; 1069 | if (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT' || target.isContentEditable) { 1070 | return; // 如果是在输入框中粘贴,不触发全局粘贴处理 1071 | } 1072 | 1073 | const items = event.clipboardData?.items; 1074 | if (!items) return; 1075 | 1076 | for (const item of items) { 1077 | console.log('粘贴类型:', item.type); 1078 | 1079 | // 处理图片 1080 | if (item.type.indexOf('image') !== -1) { 1081 | const file = item.getAsFile(); 1082 | if (file) { 1083 | // 创建一个新的 FileList 对象 1084 | const dataTransfer = new DataTransfer(); 1085 | dataTransfer.items.add(file); 1086 | 1087 | // 重置表单 1088 | currentEditId = null; 1089 | const editType = document.getElementById('editType'); 1090 | const editTitle = document.getElementById('editTitle'); 1091 | const editImage = document.getElementById('editImage'); 1092 | const imagePreview = document.getElementById('imagePreview'); 1093 | 1094 | editType.value = 'image'; 1095 | editTitle.value = `粘贴的图片_${new Date().getTime()}.png`; 1096 | editImage.files = dataTransfer.files; 1097 | 1098 | // 预览图片 1099 | const reader = new FileReader(); 1100 | reader.onload = function (e) { 1101 | imagePreview.innerHTML = `预览`; 1102 | }; 1103 | reader.readAsDataURL(file); 1104 | 1105 | handleTypeChange('image'); 1106 | document.getElementById('editModal').style.display = 'block'; 1107 | return; 1108 | } 1109 | } 1110 | 1111 | // 处理文件 1112 | else if (item.kind === 'file' && !item.type.startsWith('image/')) { 1113 | const file = item.getAsFile(); 1114 | if (file) { 1115 | // 创建一个新的 FileList 对象 1116 | const dataTransfer = new DataTransfer(); 1117 | dataTransfer.items.add(file); 1118 | 1119 | // 重置表单 1120 | currentEditId = null; 1121 | const editType = document.getElementById('editType'); 1122 | const editTitle = document.getElementById('editTitle'); 1123 | const editFile = document.getElementById('editFile'); 1124 | 1125 | editType.value = 'file'; 1126 | editTitle.value = file.name; 1127 | editFile.files = dataTransfer.files; 1128 | 1129 | handleTypeChange('file'); 1130 | 1131 | // 使用统一的文件信息显示函数 1132 | updateFileInfo(file); 1133 | 1134 | document.getElementById('editModal').style.display = 'block'; 1135 | return; 1136 | } 1137 | } 1138 | 1139 | // 处理文本 1140 | else if (item.type === 'text/plain') { 1141 | item.getAsString(async (text) => { 1142 | // 检测是否为代码 1143 | const isCode = detectCodeContent(text); 1144 | 1145 | currentEditId = null; 1146 | document.getElementById('editType').value = isCode ? 'code' : 'text'; 1147 | document.getElementById('editTitle').value = ''; 1148 | document.getElementById('editContent').value = text; 1149 | 1150 | handleTypeChange(isCode ? 'code' : 'text'); 1151 | document.getElementById('editModal').style.display = 'block'; 1152 | }); 1153 | return; 1154 | } 1155 | } 1156 | } 1157 | 1158 | // 检文本是否为代码 1159 | function detectCodeContent(text) { 1160 | // 代码检测规则 1161 | const codePatterns = [ 1162 | /^(const|let|var|function|class|import|export|if|for|while)\s/m, // 常见的代码关键字 1163 | /{[\s\S]*}/m, // 包含花括号的代码块 1164 | /\(\s*\)\s*=>/m, // 头函数 1165 | /\b(function|class)\s+\w+\s*\(/m, // 函数或类声明 1166 | /\b(if|for|while)\s*\([^)]*\)/m, // 控制结构 1167 | /\b(return|break|continue)\s/m, // 控制流关键字 1168 | /[{};]\s*$/m, // 行尾的分号或花括号 1169 | /^\s*(public|private|protected)\s/m, // 访问修饰符 1170 | /\b(try|catch|finally)\s*{/m, // 异常处理 1171 | /\b(async|await|Promise)\b/m, // 异步编程关键字 1172 | /\b(import|export)\s+.*\bfrom\s+['"][^'"]+['"]/m, // ES6 模块语法 1173 | /\b(const|let|var)\s+\w+\s*=\s*require\s*\(/m, // CommonJS 模块语法 1174 | ]; 1175 | 1176 | // 如果文本匹配任何一个代码模式,就认为是代码 1177 | return codePatterns.some(pattern => pattern.test(text)); 1178 | } 1179 | 1180 | // 处理图片预览和标题 1181 | function handleImagePreview(event) { 1182 | const file = event.target.files[0]; 1183 | if (file) { 1184 | // 立即设置标题 1185 | const titleInput = document.getElementById('editTitle'); 1186 | titleInput.value = file.name; 1187 | 1188 | const reader = new FileReader(); 1189 | reader.onload = function (e) { 1190 | const preview = document.getElementById('imagePreview'); 1191 | preview.innerHTML = `预览`; 1192 | }; 1193 | reader.readAsDataURL(file); 1194 | } 1195 | } 1196 | 1197 | // 处理文件选择和标题 1198 | window.handleFileSelect = function (event) { 1199 | const file = event.target.files[0]; 1200 | if (file) { 1201 | // 立即设置标题 1202 | const titleInput = document.getElementById('editTitle'); 1203 | titleInput.value = file.name; 1204 | 1205 | // 使用统一的文件信息显示函数 1206 | updateFileInfo(file); 1207 | } 1208 | } 1209 | 1210 | // 统一的文件信息更新函数 1211 | function updateFileInfo(file) { 1212 | const fileInfo = document.querySelector('.file-info'); 1213 | const fileIcon = getFileIcon(file.name); 1214 | fileInfo.innerHTML = ` 1215 |
1216 | 1217 |
1218 |
${file.name}
1219 |
${getFileTypeDescription(file.name)}
1220 |
${formatFileSize(file.size)}
1221 |
1222 |
1223 | `; 1224 | } 1225 | 1226 | // 打开模态框 1227 | window.openModal = function () { 1228 | currentEditId = null; 1229 | const editForm = document.getElementById('editForm'); 1230 | const editType = document.getElementById('editType'); 1231 | const editTitle = document.getElementById('editTitle'); 1232 | const editContent = document.getElementById('editContent'); 1233 | const imagePreview = document.getElementById('imagePreview'); 1234 | const editImage = document.getElementById('editImage'); 1235 | const editFile = document.getElementById('editFile'); 1236 | const fileInfo = document.querySelector('.file-info'); 1237 | 1238 | // 重置所有表单元素 1239 | editForm.reset(); 1240 | editType.value = 'text'; 1241 | editTitle.value = ' '; // 预填充空格 1242 | editTitle.required = true; // 保持必填属性 1243 | // 添加失去焦点事件,如果用户清空了内容,重新填充空格 1244 | editTitle.onblur = function () { 1245 | if (!this.value.trim()) { 1246 | this.value = ' '; 1247 | } 1248 | }; 1249 | editContent.value = ''; 1250 | 1251 | // 清除图片预览 1252 | imagePreview.innerHTML = ''; 1253 | 1254 | // 重置文件信息为默认状态 1255 | if (fileInfo) { 1256 | fileInfo.innerHTML = ` 1257 |
1258 | 1259 |
1260 |
支持所有类型的文件
1261 |
1262 |
1263 | `; 1264 | } 1265 | 1266 | // 清除文件输入框的值 1267 | if (editImage) { 1268 | editImage.value = ''; 1269 | } 1270 | if (editFile) { 1271 | editFile.value = ''; 1272 | } 1273 | 1274 | handleTypeChange('text'); 1275 | document.getElementById('editModal').style.display = 'block'; 1276 | } 1277 | 1278 | // 关闭模态框 1279 | window.closeModal = function () { 1280 | document.getElementById('editModal').style.display = 'none'; 1281 | document.getElementById('editForm').reset(); 1282 | document.getElementById('imagePreview').innerHTML = ''; 1283 | currentEditId = null; 1284 | } 1285 | 1286 | // 处理表单提交 1287 | async function handleFormSubmit(event) { 1288 | event.preventDefault(); 1289 | 1290 | const submitButton = event.submitter; 1291 | submitButton.disabled = true; 1292 | const originalText = submitButton.textContent; 1293 | submitButton.innerHTML = '保存中... '; 1294 | 1295 | try { 1296 | const type = document.getElementById('editType').value; 1297 | const titleInput = document.getElementById('editTitle'); 1298 | let title = titleInput.value.trim(); // 去除首尾空格 1299 | 1300 | // 如果标题为空,则使用一个空格作为默认标题 1301 | if (!title) { 1302 | title = ' '; 1303 | titleInput.value = ' '; 1304 | } 1305 | 1306 | let content = ''; 1307 | 1308 | if (type === 'image') { 1309 | const imageFile = document.getElementById('editImage').files[0]; 1310 | const existingContent = document.getElementById('editContent').value; 1311 | 1312 | if (!imageFile && existingContent) { 1313 | content = existingContent; 1314 | } else if (imageFile) { 1315 | // 确保设置标题 1316 | if (!title) { 1317 | document.getElementById('editTitle').value = imageFile.name; 1318 | } 1319 | 1320 | const formData = new FormData(); 1321 | formData.append('image', imageFile); 1322 | 1323 | const uploadResponse = await fetch(IMAGES_API_URL, { 1324 | method: 'POST', 1325 | body: formData 1326 | }); 1327 | 1328 | if (!uploadResponse.ok) { 1329 | const errorData = await uploadResponse.json(); 1330 | throw new Error(errorData.error || '图片上传失败'); 1331 | } 1332 | 1333 | const { url } = await uploadResponse.json(); 1334 | content = url; 1335 | } else { 1336 | throw new Error('请选择图片文件'); 1337 | } 1338 | } else if (type === 'file') { 1339 | const file = document.getElementById('editFile').files[0]; 1340 | const existingContent = document.getElementById('editContent').value; 1341 | 1342 | if (!file && existingContent) { 1343 | content = existingContent; 1344 | } else if (file) { 1345 | // 确保设置标题 1346 | if (!title) { 1347 | document.getElementById('editTitle').value = file.name; 1348 | } 1349 | 1350 | const formData = new FormData(); 1351 | formData.append('file', file); 1352 | 1353 | console.log('开始上传文件:', file.name); 1354 | const uploadResponse = await fetch(FILES_UPLOAD_URL, { 1355 | method: 'POST', 1356 | body: formData 1357 | }); 1358 | 1359 | console.log('上传响应状态:', uploadResponse.status); 1360 | const responseText = await uploadResponse.text(); 1361 | console.log('上传响应内容:', responseText); 1362 | 1363 | let responseData; 1364 | try { 1365 | responseData = JSON.parse(responseText); 1366 | } catch (e) { 1367 | console.error('解析响应失败:', e); 1368 | throw new Error('服务器响应格式错误'); 1369 | } 1370 | 1371 | if (!uploadResponse.ok) { 1372 | throw new Error(responseData.error || '文件上传失败'); 1373 | } 1374 | 1375 | if (!responseData.url) { 1376 | console.error('响应数据:', responseData); 1377 | throw new Error('上传成功但未返回文件URL'); 1378 | } 1379 | 1380 | content = responseData.url; 1381 | console.log('文件上传成功:', content); 1382 | } else { 1383 | throw new Error('请选择文件'); 1384 | } 1385 | } else { 1386 | content = document.getElementById('editContent').value; 1387 | } 1388 | 1389 | // 重新获取标题,因为可能在上传过程中被设置 1390 | const finalTitle = document.getElementById('editTitle').value; 1391 | 1392 | if (!type || !finalTitle || !content) { 1393 | throw new Error('请填写所有必要字段'); 1394 | } 1395 | 1396 | const formData = { type, title: finalTitle, content }; 1397 | 1398 | if (currentEditId) { 1399 | await updateContent(currentEditId, formData); 1400 | } else { 1401 | await createContent(formData); 1402 | } 1403 | 1404 | closeModal(); 1405 | await loadContents(false); 1406 | showToast('保存成功!'); 1407 | } catch (error) { 1408 | console.error('保存失败:', error); 1409 | showToast(error.message, 'error'); 1410 | } finally { 1411 | submitButton.disabled = false; 1412 | submitButton.textContent = originalText; 1413 | } 1414 | } 1415 | 1416 | // 创建新内容 1417 | async function createContent(data) { 1418 | const response = await fetch(API_BASE_URL, { 1419 | method: 'POST', 1420 | headers: { 1421 | 'Content-Type': 'application/json', 1422 | 'Accept': 'application/json' 1423 | }, 1424 | body: JSON.stringify(data) 1425 | }); 1426 | 1427 | if (!response.ok) { 1428 | const errorData = await response.json(); 1429 | throw new Error(errorData.error || '创建内容失败'); 1430 | } 1431 | 1432 | return await response.json(); 1433 | } 1434 | 1435 | // 更新内容 1436 | async function updateContent(id, data) { 1437 | const response = await fetch(`${API_BASE_URL}/${id}`, { 1438 | method: 'PUT', 1439 | headers: { 1440 | 'Content-Type': 'application/json', 1441 | 'Accept': 'application/json' 1442 | }, 1443 | body: JSON.stringify(data) 1444 | }); 1445 | 1446 | if (!response.ok) { 1447 | const errorData = await response.json(); 1448 | throw new Error(errorData.error || '更新内容败'); 1449 | } 1450 | 1451 | return await response.json(); 1452 | } 1453 | }); -------------------------------------------------------------------------------- /js/未压缩-theme.js: -------------------------------------------------------------------------------- 1 | // 主题切换功能 2 | function initTheme() { 3 | // 创建悬停区域 4 | const hoverArea = document.createElement('div'); 5 | hoverArea.className = 'theme-toggle-hover-area'; 6 | 7 | // 创建主题切换按钮 8 | const themeToggle = document.createElement('button'); 9 | themeToggle.className = 'theme-toggle'; 10 | themeToggle.setAttribute('aria-label', '切换深色模式'); 11 | 12 | // 将按钮添加到悬停区域中 13 | hoverArea.appendChild(themeToggle); 14 | document.body.appendChild(hoverArea); 15 | 16 | // 从localStorage读取主题设置 17 | const savedTheme = localStorage.getItem('theme') || 'light'; 18 | document.documentElement.setAttribute('data-theme', savedTheme); 19 | 20 | // 切换主题 21 | themeToggle.addEventListener('click', () => { 22 | const currentTheme = document.documentElement.getAttribute('data-theme'); 23 | const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; 24 | 25 | document.documentElement.setAttribute('data-theme', newTheme); 26 | localStorage.setItem('theme', newTheme); 27 | }); 28 | } 29 | 30 | // 确保DOM加载完成后再初始化主题 31 | if (document.readyState === 'loading') { 32 | document.addEventListener('DOMContentLoaded', initTheme); 33 | } else { 34 | initTheme(); 35 | } -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1143520/dropbox/98b651416d79b16eefefbaa2d2747cfcfe8ac913/logo.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropbox", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler pages dev --compatibility-date=2023-01-01", 7 | "deploy": "wrangler pages deploy --compatibility-date=2023-01-01 ." 8 | }, 9 | "devDependencies": { 10 | "wrangler": "^3.0.0" 11 | } 12 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | require('cssnano')({ 5 | preset: ['default', { 6 | discardComments: { 7 | removeAll: true, 8 | }, 9 | normalizeWhitespace: true, 10 | minifyFontValues: true, 11 | minifyGradients: true, 12 | minifySelectors: true, 13 | minifyParams: true 14 | }] 15 | }) 16 | ] 17 | }; -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS content_blocks; 2 | CREATE TABLE content_blocks ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | type TEXT NOT NULL, 5 | title TEXT NOT NULL, 6 | content TEXT NOT NULL, 7 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 8 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 9 | ); -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------