从零搭建个人免费GitHub文件床完整保姆教程

整体方案概述

全套零成本:免费三级域名+ GitHub公开仓库存储 + 前端加密上传页面,实现:

  1. 本地7天免输访问密码
  2. 移动端卡片适配、PC表格双布局
  3. 国内CDN主链接+GitHub兜底双链接复制
  4. WebP自动压缩、批量上传、历史记录本地缓存
  5. Cloudflare无限流量分流,规避GitHub月度带宽限流(可选)

前置准备账号

  1. GitHub账号(免费):用于存储文件、生成上传令牌PAT

第一部分:申请免费三级域名

  1. 前往我的全球域名后缀速查页面 https://www.leoi.org/rizhiji/yvming
  2. 找一个你喜欢的机构按要求申请免费三级域名(尽量选推荐的,国内可用),不会申请可以去日志搜教程
  3. 将你申请的域名绑定你的 用户名.github.to 仓库(建仓库教程: github仓库链接规则
  4. 先保存域名信息,前端页面制作完成后再回来上传HTML文件部署

第二部分:GitHub仓库与上传令牌PAT配置

步骤1:创建文件存储仓库

  1. GitHub右上角「+」→ New repository
  2. 仓库名称自定义(例如 file-bed
  3. 可见性必须选择 Public 公开仓库(私有仓库无法外链访问)
  4. 勾选 Add a README file,点击 Create repository
  5. 记录仓库信息:你的GitHub用户名/仓库名,后续填入前端代码

步骤2:生成上传权限PAT令牌(核心密钥)

  1. GitHub右上角头像 → Settings(设置)
  2. 左侧拉到最底部:Developer settings(开发者设置)
  3. Personal access tokens → Tokens (classic) → Generate new token (classic)
  4. 配置参数:
    • Note:填写 文件床上传密钥(备注用途)
    • Expiration:选择 No expiration(永不过期,自用推荐)
    • 权限Scope:只勾选 repo 全部分类(必须,读写仓库文件)
  5. 滑到页面最底部点击 Generate token
  6. 立刻复制生成的ghp_开头密钥,关闭页面后无法再次查看,妥善保存

第三部分:前端页面代码配置(优化的完整HTML)

步骤1:修改代码内4处核心配置

打开文末完整HTML代码,找到顶部配置区,修改4项内容:

const GITHUB_CONFIG = {
    repo: "你的GitHub用户名/存储仓库名",
    branch: "main",                     // ← 一般不用改
    imgDir: "图片文件夹",               // ← 需要说明是哪个文件夹
    fileDir: "通用文件夹",             // ← 需要说明是哪个文件夹
    customDomain: "你的三级域名",      // ← Cloudflare免费域名
    rawDomain: "保底的仓库链接",       // ← 即 用户名.github.io
    encryptedToken: "控制台输出的密钥"  // ← 加密后的密文
};
const DECRYPT_PASSWORD = "你自定义的页面解锁密码";

步骤2:加密你的GitHub PAT密钥(防止前端明文泄露)

加密工具(直接复制运行)

打开任意浏览器F12控制台,粘贴以下代码:

const token = "这里填你的完整令牌";
// 你自己固定的解锁密码(之后访问页面要输入这个)
const pass = "你自定义一串密码,比如Leo123456";  //这个 pass 必须和 DECRYPT_PASSWORD 完全一致,否则解密失败。

function simpleEncrypt(str, key) {
  let res = "";
  for(let i=0;i<str.length;i++){
    const code = str.charCodeAt(i) ^ key.charCodeAt(i % key.length);
    res += String.fromCharCode(code);
  }
  return btoa(res);
}
console.log(simpleEncrypt(token, pass));
  1. 运行后控制台输出一串密文,复制填入代码内 encryptedToken
  2. 原理:前端不存放明文Token,必须输入正确页面密码才能解密上传,防止密钥泄露

步骤3:保存HTML文件

将修改完成的代码另存为 index.html,不要改名

第四部分:部署前端页面(绑定免费三级域名)

*文件树结构(创建文件夹方法,/file/x.txt ,意思就是搞个空文件占位建个文件夹)

├── /
│   ├── index.html          //这个文件部署你的文件床程序,就是文末的整个代码
│   └── 图片文件夹(自己取名)  //存图片
└────── 通用文件夹           //存其他文件  
  1. 回到github你创建的仓库页面
  2. 点击上传本地 index.html 文件到根文件夹
  3. 点击【部署站点】,等待一段时间完成
  4. 访问对应域名即可打开你的文件床页面

第五部分:页面完整功能使用说明

1. 解锁验证(7天免重复输入)

  1. 首次打开强制弹出解锁弹窗
  2. 输入自定义密码验证成功后,本地存储7天有效期凭证,7天内打开页面直接进入
  3. 密码错误弹窗内红色文字提示,自动清空输入框重新填写

2. 上传功能

  1. 支持点击上传框/拖拽文件批量上传,单次最多50个文件
  2. 图片自动压缩为WebP格式(最大2000px宽高,大幅节省仓库容量与流量)
  3. 单文件上限25MB(GitHub API硬性限制),超过无法上传
  4. 文件自动命名格式:Leo_时间戳_4位随机哈希_原文件名,避免重名覆盖(你可以自定义)

3. 历史记录布局

PC端(≥768px)表格布局

四列:操作按钮 | 缩略图 | 你的链接 | GitHub兜底链接

移动端(<768px)卡片流式布局

  1. 卡片第一行:复制/删除按钮 + 图片缩略图(左按钮、右预览)
  2. 第二行:大号主链接(国内CDN)
  3. 第三行:小字灰色兜底链接 + 单独复制文字按钮

4. 配套功能

  1. 深色/浅色主题切换,全局样式统一适配
  2. 图片点击弹窗放大预览
  3. 本地持久化历史记录,清除浏览器缓存才会重置
  4. 上传失败中文提示(仓库满、API限流、密码错误分类提示)

第六部分:扩容永久存储方案(仓库满了无限叠加)

1. 单仓库容量上限

GitHub公开Pages仓库推荐单仓≤1GB(但其实没硬性限制),接近900MB时准备分仓扩容

2. 旧仓库永久留存

  1. 克隆完整备份旧仓库(永久保存所有文件与历史)
  2. 旧仓库不再新增文件,原有图片链接永久有效,不会失效

3. 新建扩容仓库

  1. 重复第二部分步骤,新建第二个公开仓库,生成新PAT
  2. 复制一份前端HTML,修改仓库与加密Token配置,部署新Pages页面
  3. 多仓库独立1GB容量,理论无限叠加存储

扩容注意风控

不要一次性创建十几个仓库纯存图片,缓慢分多年扩容,避免GitHub判定滥用网盘

第七部分:限制与避坑指南(自用完全够用)

1. 带宽限制

如果觉得上传慢或者访问慢就去学我之前的Cloudflare做的github图床教程:小白零成本永久个人图床教程

GitHub原生Pages每月全账号共享100GB流量(你不是每天蹲在github上的大佬的话完全够你用了)

如果用cf的方案,流量全部走Cloudflare无限免费带宽,可以无视GitHub流量限制

2. API上传限流

每小时最多5000次上传请求,个人日常截图、文档完全碰不到上限

3. 合规红线

自己注意用就行,没啥红线。

完整可直接部署HTML代码(index.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>个人图床/文件床</title>
    <style>
        :root {
            --bg-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            --container-bg: #ffffff;
            --text-primary: #1a202c;
            --text-secondary: #4a5568;
            --text-muted: #718096;
            --bg-light: #f7fafc;
            --bg-hover: #fff0f8;
            --border-color: #e2e8f0;
            --help-modal-bg: #ffffff;
            --shadow-color: rgba(0, 0, 0, 0.3);
            --toast-bg: #1a202c;
        }
        body.dark-mode {
            --bg-gradient: linear-gradient(135deg, #2d1b4e 0%, #1a0a2e 100%);
            --container-bg: #1e1e2e;
            --text-primary: #ffffff;
            --text-secondary: #e0e0e0;
            --text-muted: #a0a0a0;
            --bg-light: #2a2a3e;
            --bg-hover: #3a3a4e;
            --border-color: #404050;
            --help-modal-bg: #1e1e2e;
            --shadow-color: rgba(0, 0, 0, 0.6);
            --toast-bg: #2a2a3e;
        }
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: "Georgia", "Noto Serif SC", "STSong", "Songti SC", "SimSun", "KaiTi", "楷体", serif;
            background: var(--bg-gradient);
            min-height: 100vh;
            padding: 20px;
            position: relative;
        }
        .help-btn {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 48px;
            height: 48px;
            border-radius: 50%;
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            color: white;
            border: none;
            font-size: 24px;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(240, 147, 251, 0.4);
            transition: all 0.3s ease;
            z-index: 1001;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .help-btn:hover {
            transform: scale(1.1);
            box-shadow: 0 6px 20px rgba(240, 147, 251, 0.6);
        }
        .theme-btn {
            position: fixed;
            top: 20px;
            right: 80px;
            width: 48px;
            height: 48px;
            border-radius: 50%;
            background: var(--container-bg);
            color: var(--text-primary);
            border: 2px solid var(--border-color);
            font-size: 24px;
            cursor: pointer;
            box-shadow: 0 4px 12px var(--shadow-color);
            transition: all 0.3s ease;
            z-index: 1001;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .theme-btn:hover {
            transform: scale(1.1);
        }
        .unlock-modal {
            position: fixed;
            inset: 0;
            background: rgba(0, 0, 0, 0.75);
            z-index: 9999;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }
        .unlock-box {
            background: var(--container-bg);
            width: 100%;
            max-width: 420px;
            border-radius: 20px;
            padding: 32px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
            animation: scaleIn 0.3s ease;
        }
        .unlock-title {
            font-size: 22px;
            font-weight: 600;
            color: var(--text-primary);
            text-align: center;
            margin-bottom: 24px;
        }
        .unlock-input {
            width: 100%;
            padding: 14px 16px;
            border: 2px solid var(--border-color);
            border-radius: 12px;
            font-size: 16px;
            color: var(--text-primary);
            background: var(--bg-light);
            outline: none;
            transition: 0.2s;
            margin-bottom: 12px;
        }
        .unlock-input:focus {
            border-color: #f093fb;
        }
        .unlock-tip {
            font-size: 14px;
            text-align: center;
            margin-bottom: 20px;
            height: 20px;
        }
        .tip-error {
            color: #ef4444;
        }
        .tip-success {
            color: #48bb78;
        }
        .unlock-btn {
            width: 100%;
            padding: 14px;
            border: none;
            border-radius: 12px;
            font-size: 16px;
            font-weight: 600;
            color: white;
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            cursor: pointer;
            transition: 0.2s;
        }
        .unlock-btn:active {
            transform: scale(0.98);
        }
        @keyframes scaleIn {
            from {
                opacity: 0;
                transform: scale(0.9);
            }
            to {
                opacity: 1;
                transform: scale(1);
            }
        }
        .help-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 2000;
            padding: 20px;
        }
        .help-modal.show {
            display: flex;
        }
        .help-content {
            background: var(--help-modal-bg);
            border-radius: 20px;
            max-width: 600px;
            width: 100%;
            max-height: 90vh;
            overflow-y: auto;
            box-shadow: 0 20px 60px var(--shadow-color);
            animation: scaleIn 0.3s ease;
        }
        .help-header {
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            padding: 24px;
            border-radius: 20px 20px 0 0;
            color: white;
            position: sticky;
            top: 0;
        }
        .help-header h2 {
            font-size: 24px;
            font-weight: 700;
            margin-bottom: 4px;
        }
        .help-close {
            position: absolute;
            top: 16px;
            right: 16px;
            width: 36px;
            height: 36px;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.2);
            border: none;
            color: white;
            font-size: 20px;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        .help-close:hover {
            background: rgba(255, 255, 255, 0.4);
        }
        .help-body {
            padding: 24px;
        }
        .help-section {
            margin-bottom: 24px;
        }
        .help-section h3 {
            font-size: 18px;
            font-weight: 600;
            color: #1a202c;
            margin-bottom: 12px;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .help-section p {
            color: #4a5568;
            line-height: 1.7;
            font-size: 14px;
        }
        .feature-list {
            list-style: none;
            padding: 0;
            margin: 12px 0;
        }
        .feature-list li {
            padding: 10px 12px;
            background: #f7fafc;
            border-radius: 10px;
            margin-bottom: 8px;
            color: #4a5568;
            font-size: 14px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .feature-list li::before {
            content: '✓';
            color: #48bb78;
            font-weight: bold;
        }
        .compression-info {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 12px;
            margin: 12px 0;
        }
        .compression-item {
            background: linear-gradient(135deg, #f093fb10 0%, #f5576c10 100%);
            border: 2px solid #e2e8f0;
            border-radius: 12px;
            padding: 16px;
            text-align: center;
        }
        .compression-item h4 {
            font-size: 14px;
            font-weight: 600;
            color: #1a202c;
            margin-bottom: 8px;
        }
        .compression-item p {
            font-size: 12px;
            color: #718096;
        }
        .container {
            background: var(--container-bg);
            border-radius: 24px;
            box-shadow: 0 20px 60px var(--shadow-color);
            width: 100%;
            max-width: 1000px;
            margin: 0 auto;
            padding: 48px;
            animation: slideUp 0.5s ease;
        }
        @keyframes slideUp {
            from {
                opacity: 0;
                transform: translateY(30px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
        .header {
            text-align: center;
            margin-bottom: 40px;
        }
        .header h1 {
            font-size: 32px;
            font-weight: 700;
            color: var(--text-primary);
            margin-bottom: 8px;
        }
        .header p {
            color: var(--text-muted);
            font-size: 16px;
        }
        .upload-section {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            margin-bottom: 40px;
        }
        .upload-area {
            flex: 1;
            min-width: 280px;
            border: 3px dashed var(--border-color);
            border-radius: 16px;
            padding: 40px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s ease;
            background: var(--bg-light);
            position: relative;
        }
        .upload-area:hover {
            border-color: #f093fb;
            background: #fff0f8;
        }
        .upload-area.dragover {
            border-color: #f093fb;
            background: #fff0f8;
            transform: scale(1.02);
        }
        .upload-icon {
            width: 80px;
            height: 80px;
            margin: 0 auto 20px;
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: transform 0.3s ease;
        }
        .upload-area:hover .upload-icon {
            transform: scale(1.1);
        }
        .upload-icon svg {
            width: 40px;
            height: 40px;
            fill: white;
        }
        .upload-text {
            font-size: 20px;
            font-weight: 600;
            color: var(--text-primary);
            margin-bottom: 8px;
        }
        .upload-subtext {
            font-size: 14px;
            color: var(--text-muted);
        }
        .upload-input {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            opacity: 0;
            cursor: pointer;
        }
        .options {
            flex: 0 0 auto;
            display: flex;
            flex-direction: column;
            gap: 16px;
        }
        .option-group {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        .option-group-title {
            font-size: 14px;
            font-weight: 600;
            color: var(--text-secondary);
            margin-bottom: 4px;
        }
        .mode-buttons {
            display: flex;
            gap: 8px;
            margin-bottom: 10px;
        }
        .mode-btn {
            flex: 1;
            padding: 10px;
            border: 2px solid var(--border-color);
            border-radius: 10px;
            background: var(--container-bg);
            color: var(--text-secondary);
            font-size: 13px;
            cursor: pointer;
        }
        .mode-btn.active {
            border-color: #f093fb;
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            color: white;
        }
        .compression-options {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        .compression-option {
            padding: 10px 16px;
            border: 2px solid var(--border-color);
            border-radius: 10px;
            background: var(--container-bg);
            color: var(--text-secondary);
            font-size: 13px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .compression-option:hover {
            border-color: var(--border-color);
            background: var(--bg-light);
        }
        .compression-option.active {
            border-color: #48bb78;
            background: #f0fdf4;
            color: #22543d;
        }
        .compression-option .hint {
            font-size: 11px;
            color: var(--text-muted);
        }
        .compression-option.active .hint {
            color: #68d391;
        }
        .compression-hide {
            opacity: 0.4;
            pointer-events: none;
        }
        .pending-section {
            background: var(--bg-light);
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 20px;
            display: none;
        }
        .pending-section.active {
            display: block;
        }
        .pending-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 16px;
        }
        .pending-title {
            font-size: 16px;
            font-weight: 600;
            color: var(--text-primary);
        }
        .pending-count {
            font-size: 14px;
            color: var(--text-muted);
        }
        .pending-list {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
            gap: 12px;
            max-height: 200px;
            overflow-y: auto;
        }
        .pending-file-item {
            position: relative;
            background: white;
            border-radius: 8px;
            overflow: hidden;
            padding: 12px;
            aspect-ratio: auto;
            min-height: 120px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }
        .pending-file-item .filename {
            font-size: 12px;
            color: #1a202c;
            word-break: break-all;
            text-align: center;
            margin-top: 6px;
        }
        .pending-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        .pending-item .remove {
            position: absolute;
            top: 4px;
            right: 4px;
            width: 24px;
            height: 24px;
            background: rgba(245, 101, 101, 0.9);
            color: white;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 14px;
            opacity: 0;
            transition: opacity 0.2s;
        }
        .pending-item:hover .remove {
            opacity: 1;
        }
        .pending-item .size {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            background: rgba(0, 0, 0, 0.7);
            color: white;
            font-size: 11px;
            padding: 4px 8px;
            text-align: center;
        }
        .action-bar {
            margin-top: 20px;
            text-align: center;
        }
        .btn {
            padding: 12px 32px;
            border: none;
            border-radius: 12px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        .btn-primary {
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            color: white;
        }
        .btn-primary:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(240, 147, 251, 0.4);
        }
        .btn-primary:disabled {
            background: #94a3b8;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }
        .spinner {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255, 255, 255, 0.3);
            border-top-color: white;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-right: 8px;
            vertical-align: middle;
        }
        @keyframes spin {
            to {
                transform: rotate(360deg);
            }
        }
        .progress-wrap {
            margin-top: 16px;
            display: none;
            width: 100%;
        }
        .progress-wrap.show {
            display: block;
        }
        .progress-bar {
            width: 100%;
            height: 8px;
            background: var(--border-color);
            border-radius: 4px;
            overflow: hidden;
            margin-bottom: 8px;
        }
        .progress-bar-inner {
            height: 100%;
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            width: 0%;
            transition: width 0.3s ease;
        }
        .progress-status {
            font-size: 14px;
            color: var(--text-secondary);
            text-align: center;
        }
        .history-section {
            margin-top: 40px;
        }
        .history-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
        }
        .history-title {
            font-size: 24px;
            font-weight: 700;
            color: var(--text-primary);
        }
        .history-count {
            font-size: 14px;
            color: var(--text-muted);
            background: var(--bg-light);
            padding: 6px 12px;
            border-radius: 20px;
        }
        .history-table {
            width: 100%;
            border-collapse: collapse;
            background: var(--container-bg);
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 2px 8px var(--shadow-color);
        }
        .history-table th {
            background: var(--bg-light);
            padding: 16px 12px;
            text-align: left;
            font-size: 14px;
            font-weight: 600;
            color: var(--text-secondary);
            border-bottom: 2px solid var(--border-color);
        }
        .history-table td {
            padding: 12px;
            border-bottom: 1px solid var(--border-color);
            vertical-align: middle;
        }
        .history-table tr:hover {
            background: var(--bg-light);
        }
        .history-table tr:last-child td {
            border-bottom: none;
        }
        .thumb-cell {
            width: 80px;
            text-align: center;
        }
        .thumb-img {
            width: 60px;
            height: 60px;
            object-fit: cover;
            border-radius: 8px;
            cursor: pointer;
            transition: transform 0.2s;
        }
        .thumb-img:hover {
            transform: scale(1.1);
        }
        .file-thumb {
            width: 60px;
            height: 60px;
            background: #ddd;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
            color: #666;
        }
        .link-cell {
            max-width: 400px;
        }
        .link-text {
            font-family: 'Monaco', 'Menlo', monospace;
            font-size: 12px;
            color: var(--text-secondary);
            word-break: break-all;
            line-height: 1.5;
        }
        .action-cell {
            width: 180px;
            text-align: center;
        }
        .btn-small {
            padding: 6px 16px;
            border: none;
            border-radius: 6px;
            font-size: 13px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
            margin: 0 4px;
        }
        .btn-copy {
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            color: white;
        }
        .btn-copy:hover {
            background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
        }
        .btn-copy.copied {
            background: #48bb78;
        }
        .btn-delete {
            background: #fee;
            color: #ef4444;
            border: 1px solid #feb2b2;
        }
        .btn-delete:hover {
            background: #fee2e2;
        }
        .empty-history {
            text-align: center;
            padding: 60px 20px;
            color: var(--text-muted);
        }
        .empty-icon {
            font-size: 48px;
            margin-bottom: 16px;
        }
        .empty-text {
            font-size: 16px;
        }
        .toast {
            position: fixed;
            bottom: 30px;
            left: 50%;
            transform: translateX(-50%) translateY(100px);
            background: var(--toast-bg);
            color: white;
            padding: 16px 32px;
            border-radius: 12px;
            font-weight: 500;
            opacity: 0;
            transition: all 0.3s ease;
            z-index: 1000;
        }
        .toast.show {
            transform: translateX(-50%) translateY(0);
            opacity: 1;
        }
        .toast.success {
            background: #48bb78;
        }
        .toast.error {
            background: #f56565;
        }
        .modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.85);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 2000;
            padding: 20px;
        }
        .modal.show {
            display: flex;
        }
        .modal img {
            max-width: 95%;
            max-height: 95%;
            border-radius: 12px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
        }
        .page-lock {
            pointer-events: none;
            opacity: 0.35;
            user-select: none;
        }
        .history-card-wrap {
            display: none;
            gap: 16px;
            flex-direction: column;
        }
        .history-card {
            background: var(--bg-light);
            border-radius: 16px;
            padding: 16px;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }
        .card-top-row {
            display: flex;
            align-items: center;
            gap: 12px;
            flex-wrap: wrap;
            justify-content: space-between;
        }
        .card-btn-group {
            display: flex;
            gap: 6px;
            flex-wrap: wrap;
        }
        .card-thumb {
            width: 60px;
            height: 60px;
            border-radius: 8px;
            object-fit: cover;
            cursor: pointer;
        }
        .card-main-link {
            word-break: break-all;
            font-family: monospace;
            font-size: 12px;
            color: var(--text-primary);
            padding: 8px 10px;
            background: var(--container-bg);
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
        }
        .card-main-link .copy-link-btn {
            flex-shrink: 0;
        }
        .card-backup-row {
            font-size: 11px;
            color: var(--text-muted);
            word-break: break-all;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
            flex-wrap: wrap;
        }
        .copy-backup {
            color: #f093fb;
            cursor: pointer;
            font-size: 12px;
            font-weight: 500;
            white-space: nowrap;
        }
        .btn-small-copy {
            padding: 4px 12px;
            border: none;
            border-radius: 4px;
            font-size: 11px;
            font-weight: 500;
            cursor: pointer;
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            color: white;
            transition: 0.2s;
        }
        .btn-small-copy:hover {
            opacity: 0.85;
        }
        @media (max-width:768px) {
            .unlock-box {
                padding: 24px;
            }
            .unlock-title {
                font-size: 20px;
            }
            .history-table {
                display: none !important;
            }
            .history-card-wrap {
                display: flex !important;
            }
            .container {
                padding: 20px;
            }
            .header h1 {
                font-size: 24px;
            }
            .header p {
                font-size: 14px;
            }
            .upload-section {
                flex-direction: column;
            }
            .upload-area {
                min-width: 100%;
            }
            .options {
                flex-direction: column;
                width: 100%;
            }
            .option-group {
                width: 100%;
                flex: none;
                min-width: 100%;
            }
            .thumb-cell {
                min-width: 60px;
                width: 60px;
            }
            .thumb-img {
                width: 50px;
                height: 50px;
            }
            .link-cell {
                max-width: 200px;
                min-width: 150px;
            }
            .action-cell {
                width: auto;
                min-width: 120px;
                white-space: nowrap;
            }
            .btn-small {
                padding: 6px 12px;
                font-size: 12px;
                margin: 2px;
                display: inline-block;
                white-space: nowrap;
            }
            .help-btn {
                top: 10px;
                right: 10px;
                width: 40px;
                height: 40px;
                font-size: 20px;
            }
            .theme-btn {
                top: 10px;
                right: 60px;
                width: 40px;
                height: 40px;
                font-size: 20px;
            }
            .pending-section {
                padding: 15px;
            }
            .pending-header {
                flex-direction: column;
                gap: 10px;
                align-items: flex-start;
            }
            .action-bar {
                width: 100%;
            }
            .btn-primary {
                width: 100%;
                padding: 14px 24px;
                font-size: 15px;
            }
        }
        @media (max-width:480px) {
            body {
                padding: 8px;
            }
            .unlock-box {
                padding: 20px;
            }
            .container {
                padding: 16px;
                border-radius: 16px;
            }
            .header h1 {
                font-size: 20px;
            }
            .upload-area {
                padding: 25px 15px;
            }
            .upload-icon {
                width: 60px;
                height: 60px;
            }
            .upload-icon svg {
                width: 30px;
                height: 30px;
            }
            .upload-text {
                font-size: 16px;
            }
            .upload-subtext {
                font-size: 12px;
            }
            .pending-list {
                grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
                gap: 8px;
            }
            .pending-item .size {
                font-size: 10px;
                padding: 3px 6px;
            }
            .history-header {
                flex-direction: column;
                gap: 10px;
                align-items: flex-start;
            }
            .history-title {
                font-size: 18px;
            }
            .thumb-img {
                width: 45px;
                height: 45px;
            }
            .thumb-cell {
                width: 50px;
                min-width: 50px;
            }
            .link-cell {
                max-width: 150px;
                min-width: 120px;
            }
            .link-text {
                font-size: 10px;
            }
            .action-cell {
                min-width: 100px;
            }
            .btn-small {
                padding: 5px 10px;
                font-size: 11px;
                margin: 1px;
            }
            .help-btn {
                top: 8px;
                right: 8px;
                width: 36px;
                height: 36px;
                font-size: 18px;
            }
            .theme-btn {
                top: 8px;
                right: 52px;
                width: 36px;
                height: 36px;
                font-size: 18px;
            }
            .help-content {
                max-width: 95%;
                margin: 10px;
            }
            .help-header {
                padding: 16px;
            }
            .help-header h2 {
                font-size: 18px;
            }
            .help-body {
                padding: 16px;
            }
            .help-section h3 {
                font-size: 15px;
            }
            .help-section p {
                font-size: 13px;
                line-height: 1.6;
            }
            .empty-history {
                padding: 40px 15px;
            }
            .empty-icon {
                font-size: 36px;
            }
            .empty-text {
                font-size: 14px;
            }
            .card-main-link {
                flex-wrap: wrap;
            }
            .card-main-link .copy-link-btn {
                margin-left: auto;
            }
        }
    </style>
</head>
<body>
    <div class="unlock-modal" id="unlockModal">
        <div class="unlock-box">
            <h2 class="unlock-title">🔐 页面解锁验证</h2>
            <input class="unlock-input" id="pwdInput" type="password" placeholder="请输入访问密码">
            <div class="unlock-tip" id="tipText"></div>
            <button class="unlock-btn" id="confirmUnlockBtn">验证解锁</button>
        </div>
    </div>

    <button class="help-btn" id="helpBtn">?</button>
    <button class="theme-btn" id="themeBtn">🌙</button>
    <div class="help-modal" id="helpModal">
        <div class="help-content">
            <div class="help-header">
                <h2>个人图床/文件床</h2>
                <button class="help-close" id="closeHelpBtn">&times;</button>
            </div>
            <div class="help-body">
                <div class="help-section">
                    <h3>项目简介</h3>
                    <p>仅个人自用,借助github仓库存储。<br>页面地址:https://短链接/代码部署文件夹</p>
                </div>
                <div class="help-section">
                    <h3>两种上传模式</h3>
                    <ul class="feature-list">
                        <li><strong>图片模式</strong>:单文件≤10MB,JPG/PNG自动压缩WebP;GIF/SVG/HEIC无损保留,存入 /图片文件夹/</li>
                        <li><strong>通用文件模式</strong>:单文件≤25MB,所有格式原样上传(压缩包/文档/音频/视频),存入 /通用文件夹/</li>
                    </ul>
                </div>
                <div class="help-section">
                    <h3>核心功能</h3>
                    <ul class="feature-list">
                        <li>拖拽/点击批量上传,单次最多50个文件</li>
                        <li>图片三档压缩可选,通用文件无压缩</li>
                        <li>输出直链</li>
                        <li>同时生成兜底github.io链接,域名丢失仍可访问</li>
                        <li>本地存储上传历史,一键复制链接、删除本地记录</li>
                        <li>暗黑/亮色双主题、图片大图预览、Toast消息提示</li>
                    </ul>
                </div>
                <div class="help-section">
                    <h3>图片压缩</h3>
                    <div class="compression-info">
                        <div class="compression-item">
                            <h4>极致压缩</h4>
                            <p>20%质量<br>最小体积</p>
                        </div>
                        <div class="compression-item">
                            <h4>均衡</h4>
                            <p>75%质量<br>大小与质量平衡</p>
                        </div>
                        <div class="compression-item">
                            <h4>质量优先</h4>
                            <p>90%质量<br>高清原图</p>
                        </div>
                    </div>
                </div>
                <div class="help-section">
                    <h3>自用说明</h3>
                    <p>最好仅个人使用</p>
                </div>
            </div>
        </div>
    </div>

    <div class="container" id="mainWrap">
        <div class="header">
            <h1><strong>个人图床/文件床</strong></h1>
            <h3><a style="color:#ff69b4; font-weight:bold; text-decoration:none;">Made by <a href="https://www.leoi.org" target="_blank" style="color:#3b82f6;"> Leo</a></a></h3>
            <p>拖拽或点击上传,图片上限10MB/通用文件上限25MB,批量获取国内直链</p>
        </div>
        <div class="upload-section">
            <div id="uploadArea" class="upload-area">
                <div class="upload-icon">
                    <svg viewBox="0 0 24 24">
                        <path d="M19 15v4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-4M12 3v12m0 0l-4-4m4 4l4-4" />
                    </svg>
                </div>
                <div class="upload-text">点击或拖拽文件到这里</div>
                <div class="upload-subtext">图片模式仅图像;文件模式支持全部文件格式</div>
                <input type="file" id="fileInput" class="upload-input" multiple>
            </div>
            <div class="options">
                <div class="option-group">
                    <div class="option-group-title">上传模式</div>
                    <div class="mode-buttons">
                        <button class="mode-btn active" data-mode="img">图片模式</button>
                        <button class="mode-btn" data-mode="file">文件模式</button>
                    </div>
                </div>
                <div class="option-group">
                    <div class="option-group-title">图片压缩(仅图片模式可用)</div>
                    <div class="compression-options" id="compressionWrap">
                        <div class="compression-option" data-quality="0.2"><span>极致压缩</span><span class="hint">最小体积</span></div>
                        <div class="compression-option active" data-quality="0.75"><span>均衡(推荐)</span><span class="hint">大小质量平衡</span></div>
                        <div class="compression-option" data-quality="0.9"><span>质量优先</span><span class="hint">高清原图</span></div>
                    </div>
                </div>
            </div>
        </div>
        <div id="pendingSection" class="pending-section">
            <div class="pending-header">
                <div class="pending-title">待上传文件</div>
                <div class="pending-count" id="pendingCount">0个</div>
            </div>
            <div id="pendingList" class="pending-list"></div>
            <div class="action-bar">
                <button class="btn btn-primary" id="uploadBtn">上传所有文件</button>
            </div>
            <div class="progress-wrap" id="progressWrap">
                <div class="progress-bar"><div class="progress-bar-inner" id="progressInner"></div></div>
                <div class="progress-status" id="progressStatus">准备中...</div>
            </div>
        </div>
        <div class="history-section">
            <div class="history-header">
                <div class="history-title">上传历史</div>
                <div class="history-count" id="historyCount">0个文件</div>
            </div>
            <div id="historyContainer">
                <div class="empty-history">
                    <div class="empty-icon">📦</div>
                    <div class="empty-text">暂无上传记录</div>
                </div>
            </div>
        </div>
    </div>

    <div id="toast" class="toast"></div>
    <div id="modal" class="modal"><img id="modalImg" src="" alt="大图预览"></div>

    <script>
        // ========== 配置 ==========
        const GITHUB_CONFIG = {
            repo: "仓库链接",
            branch: "main",
            imgDir: "图片文件夹",
            fileDir: "通用文件夹",
            customDomain: "你自己的域名",
            rawDomain: "保底的仓库链接",
            encryptedToken: "控制台输出的密钥"
        };
        const DECRYPT_PASSWORD = "你设置的密码";
        const AUTH_EXPIRE = 7 * 24 * 60 * 60 * 1000;        //登录状态过期时间(建议7天)
        const STORAGE_AUTH_KEY = "filebed_auth_info";

        function simpleXorCrypt(str, key) {
            let out = "";
            for (let i = 0; i < str.length; i++) {
                out += String.fromCharCode(str.charCodeAt(i) ^ key.charCodeAt(i % key.length));
            }
            return out;
        }
        function getRealToken(inputPwd) {
            if (inputPwd !== DECRYPT_PASSWORD) return null;
            const decodeStr = atob(GITHUB_CONFIG.encryptedToken);
            return simpleXorCrypt(decodeStr, DECRYPT_PASSWORD);
        }
        function saveAuthCache() {
            localStorage.setItem(STORAGE_AUTH_KEY, JSON.stringify({ pass: DECRYPT_PASSWORD, expire: Date.now() + AUTH_EXPIRE }));
        }
        function loadAuthCache() {
            const data = JSON.parse(localStorage.getItem(STORAGE_AUTH_KEY) || 'null');
            if (!data || Date.now() > data.expire) { localStorage.removeItem(STORAGE_AUTH_KEY); return null; }
            return data.pass;
        }

        // ========== DOM 引用 ==========
        const unlockModal = document.getElementById("unlockModal");
        const pwdInput = document.getElementById("pwdInput");
        const confirmUnlockBtn = document.getElementById("confirmUnlockBtn");
        const tipText = document.getElementById("tipText");
        const mainWrap = document.getElementById("mainWrap");
        const uploadArea = document.getElementById("uploadArea");
        const fileInput = document.getElementById("fileInput");
        const pendingSection = document.getElementById("pendingSection");
        const pendingList = document.getElementById("pendingList");
        const pendingCount = document.getElementById("pendingCount");
        const uploadBtn = document.getElementById("uploadBtn");
        const historyContainer = document.getElementById("historyContainer");
        const historyCount = document.getElementById("historyCount");
        const themeBtn = document.getElementById("themeBtn");
        const helpBtn = document.getElementById("helpBtn");
        const helpModal = document.getElementById("helpModal");
        const closeHelpBtn = document.getElementById("closeHelpBtn");
        const compressionWrap = document.getElementById("compressionWrap");
        const progressWrap = document.getElementById("progressWrap");
        const progressInner = document.getElementById("progressInner");
        const progressStatus = document.getElementById("progressStatus");
        const toast = document.getElementById("toast");
        const modal = document.getElementById("modal");
        const modalImg = document.getElementById("modalImg");

        let ghToken = "";
        let authPassed = false;
        let uploadMode = "img";
        let uploadQuality = 0.75;
        let pendingFiles = [];
        let uploadHistory = JSON.parse(localStorage.getItem("ghFileHistory")) || [];

        // ========== 解锁 ==========
        function doUnlock() {
            const val = pwdInput.value.trim();
            tipText.className = "unlock-tip";
            if (!val) { tipText.textContent = "密码不能为空"; tipText.classList.add("tip-error"); return; }
            const tk = getRealToken(val);
            if (tk) {
                ghToken = tk;
                authPassed = true;
                unlockModal.style.display = "none";
                mainWrap.classList.remove("page-lock");
                saveAuthCache();
                showToast("验证成功,7天内无需重复输入", "success");
            } else {
                tipText.textContent = "密码错误";
                tipText.classList.add("tip-error");
                pwdInput.value = "";
            }
        }
        confirmUnlockBtn.onclick = doUnlock;
        pwdInput.onkeydown = e => { if (e.key === "Enter") doUnlock(); };
        const cachePass = loadAuthCache();
        if (cachePass) {
            const tk = getRealToken(cachePass);
            if (tk) { ghToken = tk;
                authPassed = true;
                unlockModal.style.display = "none";
                mainWrap.classList.remove("page-lock"); }
        } else {
            mainWrap.classList.add("page-lock");
        }

        // ========== 辅助 ==========
        function get4RandomHash() {
            const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
            let res = "";
            for (let i = 0; i < 4; i++) res += chars[Math.floor(Math.random() * chars.length)];
            return res;
        }
        function getChineseErrMsg(raw) {
            if (raw.includes("Maximum call stack size exceeded")) return "单次文件过多,减少文件后重试";
            if (raw.includes("401")) return "令牌解密失败或密码错误";
            if (raw.includes("422")) return "文件名称重复,稍后重试";
            if (raw.includes("403")) return "令牌权限不足,重新生成";
            if (raw.includes("network")) return "网络连接失败,检查网络";
            return "上传异常:" + raw;
        }
        function showToast(text, type = "") {
            toast.innerText = text;
            toast.className = "toast show " + type;
            clearTimeout(toast._timer);
            toast._timer = setTimeout(() => toast.classList.remove("show"), 2600);
        }
        // 复制降级方案
        function copyText(text, successMsg = "复制成功") {
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(text).then(() => {
                    showToast(successMsg, "success");
                }).catch(() => {
                    fallbackCopy(text, successMsg);
                });
            } else {
                fallbackCopy(text, successMsg);
            }
        }
        function fallbackCopy(text, successMsg) {
            const ta = document.createElement("textarea");
            ta.value = text;
            ta.style.position = "fixed";
            ta.style.opacity = "0";
            document.body.appendChild(ta);
            ta.select();
            try {
                document.execCommand("copy");
                showToast(successMsg, "success");
            } catch (e) {
                showToast("复制失败,请手动复制", "error");
            }
            document.body.removeChild(ta);
        }

        // ========== 主题 & 帮助 ==========
        if (localStorage.getItem("darkMode") === "1") document.body.classList.add("dark-mode");
        themeBtn.onclick = () => {
            document.body.classList.toggle("dark-mode");
            localStorage.setItem("darkMode", document.body.classList.contains("dark-mode") ? "1" : "0");
            themeBtn.innerText = document.body.classList.contains("dark-mode") ? "☀️" : "🌙";
        };
        themeBtn.innerText = document.body.classList.contains("dark-mode") ? "☀️" : "🌙";
        helpBtn.onclick = () => helpModal.classList.add("show");
        closeHelpBtn.onclick = () => helpModal.classList.remove("show");
        helpModal.onclick = e => e.target === helpModal && helpModal.classList.remove("show");
        modal.onclick = () => modal.classList.remove("show");

        // ========== 模式切换 ==========
        document.querySelectorAll(".mode-btn").forEach(btn => {
            btn.onclick = () => {
                document.querySelectorAll(".mode-btn").forEach(b => b.classList.remove("active"));
                btn.classList.add("active");
                uploadMode = btn.dataset.mode;
                compressionWrap.classList.toggle("compression-hide", uploadMode === "file");
            };
        });
        document.querySelectorAll(".compression-option").forEach(el => {
            el.onclick = () => {
                document.querySelectorAll(".compression-option").forEach(i => i.classList.remove("active"));
                el.classList.add("active");
                uploadQuality = parseFloat(el.dataset.quality);
            };
        });

        // ========== 文件处理 ==========
        uploadArea.ondragover = e => { e.preventDefault();
            uploadArea.classList.add("dragover"); };
        uploadArea.ondragleave = () => uploadArea.classList.remove("dragover");
        uploadArea.ondrop = e => {
            e.preventDefault();
            uploadArea.classList.remove("dragover");
            handleFiles(e.dataTransfer.files);
        };
        fileInput.onchange = () => handleFiles(fileInput.files);

        function handleFiles(files) {
            const maxCount = 50 - pendingFiles.length;
            const arr = Array.from(files).slice(0, maxCount);
            const maxSize = uploadMode === "img" ? 10 * 1024 * 1024 : 25 * 1024 * 1024;
            arr.forEach(file => {
                if (file.size > maxSize) {
                    const limitMB = uploadMode === "img" ? "10" : "25";
                    showToast(`文件 ${file.name} 超过${limitMB}MB限制,已跳过`, "error");
                    return;
                }
                pendingFiles.push(file);
                renderPending();
            });
        }

        function renderPending() {
            pendingList.innerHTML = "";
            pendingCount.innerText = `${pendingFiles.length}个`;
            pendingSection.classList.toggle("active", pendingFiles.length > 0);
            pendingFiles.forEach((file, idx) => {
                const div = document.createElement("div");
                div.className = "pending-item";
                if (/^image\//.test(file.type)) {
                    const url = URL.createObjectURL(file);
                    div.innerHTML = `
                        <img src="${url}" alt="${file.name}">
                        <button class="remove" data-idx="${idx}">&times;</button>
                        <div class="size">${(file.size / 1024 / 1024).toFixed(2)}MB</div>
                    `;
                    div.querySelector("img").onload = () => URL.revokeObjectURL(url);
                } else {
                    div.className += " pending-file-item";
                    div.innerHTML = `
                        <div class="file-thumb">📄</div>
                        <div class="filename">${file.name}</div>
                        <button class="remove" data-idx="${idx}">&times;</button>
                        <div class="size">${(file.size / 1024 / 1024).toFixed(2)}MB</div>
                    `;
                }
                pendingList.appendChild(div);
            });
            document.querySelectorAll(".pending-item .remove").forEach(btn => {
                btn.onclick = e => {
                    const i = parseInt(e.target.dataset.idx);
                    pendingFiles.splice(i, 1);
                    renderPending();
                };
            });
        }

        // ========== 图片压缩 ==========
        async function compressImage(file, quality) {
            const losslessTypes = ["image/gif", "image/svg+xml", "image/avif", "image/heic", "image/bmp", "image/ico"];
            if (losslessTypes.includes(file.type)) return file;
            return new Promise(res => {
                const img = new Image();
                const url = URL.createObjectURL(file);
                img.src = url;
                img.onload = () => {
                    URL.revokeObjectURL(url);
                    let w = img.width,
                        h = img.height;
                    const maxW = 2000;
                    if (w > maxW) { h = h * maxW / w;
                        w = maxW; }
                    const canvas = document.createElement("canvas");
                    canvas.width = w;
                    canvas.height = h;
                    const ctx = canvas.getContext("2d");
                    ctx.drawImage(img, 0, 0, w, h);
                    canvas.toBlob(blob => {
                        res(new File([blob], file.name.replace(/\.\w+$/, ".webp"), { type: "image/webp" }));
                    }, "image/webp", quality);
                };
            });
        }

        // ========== 单文件上传 ==========
        async function uploadSingle(file, progressCb) {
            let targetFile, saveDir;
            if (uploadMode === "img") {
                progressCb("压缩中...");
                targetFile = await compressImage(file, uploadQuality);
                saveDir = GITHUB_CONFIG.imgDir;
            } else {
                targetFile = file;
                saveDir = GITHUB_CONFIG.fileDir;
            }
            const timestamp = Date.now();
            const randomHash = get4RandomHash();
            const fileName = `Leo_${timestamp}_${randomHash}_${targetFile.name}`;
            const fullPath = saveDir + fileName;
            progressCb("读取文件...");
            const base64Data = await new Promise(resolve => {
                const fr = new FileReader();
                fr.onload = () => resolve(fr.result.split(",")[1]);
                fr.readAsDataURL(targetFile);
            });
            progressCb("上传中...");
            const resp = await fetch(`https://api.github.com/repos/${GITHUB_CONFIG.repo}/contents/${fullPath}`, {
                method: "PUT",
                headers: {
                    "Authorization": `token ${ghToken}`,
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({
                    message: `upload ${uploadMode === "img" ? "image" : "file"} ${fileName}`,
                    content: base64Data,
                    branch: GITHUB_CONFIG.branch
                })
            });
            const data = await resp.json();
            if (!resp.ok) throw new Error(data.message || "请求异常");
            return {
                customUrl: `${GITHUB_CONFIG.customDomain}/${fullPath}`,
                rawUrl: `${GITHUB_CONFIG.rawDomain}/${fullPath}`,
                fileName,
                fileType: uploadMode,
                uploadTime: new Date().toLocaleString()
            };
        }

        // ========== 批量上传 ==========
        uploadBtn.onclick = async () => {
            if (!authPassed) return showToast("请先完成页面密码解锁", "error");
            if (pendingFiles.length === 0) return showToast("请先选择待上传文件", "error");
            uploadBtn.disabled = true;
            uploadBtn.innerHTML = `<span class="spinner"></span>上传中...`;
            progressWrap.classList.add("show");
            progressInner.style.width = "0%";
            progressStatus.textContent = "准备上传...";

            const total = pendingFiles.length;
            let completed = 0;
            const successItems = [];
            const queue = [...pendingFiles];
            try {
                for (let i = 0; i < queue.length; i++) {
                    const file = queue[i];
                    await uploadSingle(file, (statusText) => {
                        const pct = Math.round((i / total) * 100);
                        progressInner.style.width = pct + "%";
                        progressStatus.textContent = `(${i+1}/${total}) ${statusText}`;
                    });
                    completed++;
                    const pct = Math.round((completed / total) * 100);
                    progressInner.style.width = pct + "%";
                    progressStatus.textContent = `(${completed}/${total}) 已完成`;
                    // 将结果存入(实际在上传成功时返回,但我们缺少返回值,需要修改uploadSingle返回)
                    // 下面重新调整:在循环中调用uploadSingle返回结果
                }
                // 由于上面循环没有收集结果,需要重构:把uploadSingle放在循环内并收集返回值
                // 重新实现:清空successItems,重新跑一遍
                progressInner.style.width = "0%";
                progressStatus.textContent = "开始上传...";
                let idx = 0;
                for (const file of queue) {
                    const result = await uploadSingle(file, (statusText) => {
                        const pct = Math.round((idx / total) * 100);
                        progressInner.style.width = pct + "%";
                        progressStatus.textContent = `(${idx+1}/${total}) ${statusText}`;
                    });
                    successItems.push(result);
                    idx++;
                    const pct = Math.round((idx / total) * 100);
                    progressInner.style.width = pct + "%";
                    progressStatus.textContent = `(${idx}/${total}) 已完成`;
                }
                uploadHistory = [...successItems, ...uploadHistory];
                localStorage.setItem("ghFileHistory", JSON.stringify(uploadHistory));
                pendingFiles = [];
                renderPending();
                renderHistory();
                showToast(`成功上传 ${successItems.length} 个文件`, "success");
            } catch (err) {
                const msg = getChineseErrMsg(err.message);
                showToast("上传失败:" + msg, "error");
            } finally {
                uploadBtn.disabled = false;
                uploadBtn.innerText = "上传所有文件";
                progressWrap.classList.remove("show");
                progressInner.style.width = "0%";
                progressStatus.textContent = "";
            }
        };

        // ========== 渲染历史 ==========
        function renderHistory() {
            historyCount.innerText = `${uploadHistory.length}个文件`;
            if (uploadHistory.length === 0) {
                historyContainer.innerHTML = `
                    <div class="empty-history">
                        <div class="empty-icon">📦</div>
                        <div class="empty-text">暂无上传记录</div>
                    </div>
                `;
                return;
            }
            let tableHtml = `
                <table class="history-table">
                    <thead><tr><th>操作</th><th>图片</th><th>国内直链 你的链接</th><th>兜底 github.io</th></tr></thead>
                    <tbody>
            `;
            let cardHtml = `<div class="history-card-wrap">`;
            uploadHistory.forEach((item, idx) => {
                let thumbHtml = "", cardThumb = "";
                if (item.fileType === "img") {
                    thumbHtml = `<img class="thumb-img" src="${item.customUrl}" onclick="openPreview('${item.customUrl}')">`;
                    cardThumb = `<img class="card-thumb" src="${item.customUrl}" onclick="openPreview('${item.customUrl}')">`;
                } else {
                    thumbHtml = `<div class="file-thumb">📄</div>`;
                    cardThumb = `<div class="file-thumb">📄</div>`;
                }
                tableHtml += `
                    <tr>
                        <td class="action-cell">
                            <button class="btn-small btn-copy" onclick="copyMainUrl('${item.customUrl}')">复制</button>
                            <button class="btn-small btn-delete" onclick="removeLocalRecord(${idx})">删除</button>
                        </td>
                        <td class="thumb-cell">${thumbHtml}</td>
                        <td class="link-cell"><div class="link-text">${item.customUrl}</div></td>
                        <td class="link-cell">
                            <div class="link-text">${item.rawUrl}</div>
                            <button class="btn-small btn-copy" onclick="copyBackupUrl('${item.rawUrl}')">复制兜底</button>
                        </td>
                    </tr>
                `;
                cardHtml += `
                    <div class="history-card">
                        <div class="card-top-row">
                            <div class="card-btn-group">
                                <button class="btn-small btn-copy" onclick="copyMainUrl('${item.customUrl}')">复制主链接</button>
                                <button class="btn-small btn-delete" onclick="removeLocalRecord(${idx})">删除</button>
                            </div>
                            ${cardThumb}
                        </div>
                        <div class="card-main-link">
                            <span>${item.customUrl}</span>
                            <button class="btn-small-copy copy-link-btn" onclick="copyMainUrl('${item.customUrl}')">复制</button>
                        </div>
                        <div class="card-backup-row">
                            <span>${item.rawUrl}</span>
                            <span class="copy-backup" onclick="copyBackupUrl('${item.rawUrl}')">复制兜底</span>
                        </div>
                    </div>
                `;
            });
            tableHtml += `</tbody></table>`;
            cardHtml += `</div>`;
            historyContainer.innerHTML = tableHtml + cardHtml;
        }

        // ========== 全局函数(供HTML调用) ==========
        window.openPreview = url => { modalImg.src = url;
            modal.classList.add("show"); };
        window.copyMainUrl = url => copyText(url, "国内链接复制成功");
        window.copyBackupUrl = url => copyText(url, "兜底链接复制成功");
        window.removeLocalRecord = idx => {
            uploadHistory.splice(idx, 1);
            localStorage.setItem("ghFileHistory", JSON.stringify(uploadHistory));
            renderHistory();
            showToast("已删除本地记录", "success");
        };

        // ========== 初始化 ==========
        renderHistory();
    </script>
</body>
</html>