├── .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 |
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 |
340 |
379 |
378 |
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 |
9 |
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 `
` }; 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 | 还没有任何内容
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 = `
51 |
` } 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 |
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 |
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 |
345 |
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 `
`;
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 | 还没有任何内容
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 = `
621 |
`;
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 |
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 |
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 |
--------------------------------------------------------------------------------