Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  • A+
所属分类:未分类

概要

最近部署了免费图床 Telegraph-Image - Github,再次体验到了白嫖开源的快乐。下面我将介绍了这种利用TelegraphCloudflare Pages无需服务器即可搭建无限空间且完全免费的图床的方法ヽ(´▽`)ノ~

Telegraph搭建步骤

  1. 前提准备

    • 拥有GitHubCloudflare账号(可能需要魔法🧙‍♀️)。

  2. Fork项目

  3. Cloudflare Pages创建项目

    • 登录Cloudflare,选择创建项目,链接到刚刚Fork的GitHub项目。Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

    • 选择对应的仓库(默认为Telegraph-Image-),其他默认配置进行部署,完成后会生成一个免费的域名Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  4. 访问部署页面

    仅仅是上传和访问图片还不能满足我们的需求,如何管理我们的图床呢?

    • 访问生成的域名Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

    • 进行图片上传,正常情况下系统会返回图片链接Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

    • 访问图片链接,若能成功加载,则表明图床功能正常˶╹ꇴ╹˶!Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  5. 后台管理配置

  • Cloudflare Pages的项目设置中,创建新的命名空间img_url

    Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  • 绑定KV命名空间Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  • 设置环境变量,添加管理用户和密码。

    注意变量名称需完全相同,且为大写英文字母

    变量名称
    BASIC_USER <后台管理页面登录用户名称>
    BASIC_PASS <后台管理页面登录用户密码>

    Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  • 开启图片审查(避免某些人上传一些奇怪的东西🤔)

    • 前往https://moderatecontent.com/ 并用邮箱注册一个免费的用于审查图像内容的 API keyTelegraph+PicGo+Typora 免费无限图床(完整图文教程)

    • 复制发到邮箱的API KeyTelegraph+PicGo+Typora 免费无限图床(完整图文教程)

    • 打开 Cloudflare Pages 的管理页面,依次点击设置环境变量添加环境变量

      添加一个变量名称ModerateContentApiKey为你刚刚第一步获得的API key,点击保存即可Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  • 重新部署程序Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  • 在图床网址后面加上/admin, 访问图床后台。若弹出登录窗口说明BASIC_USERBASIC_PASS配置成功,登录即可看到自己的图床🎉Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

图片管理

  • 在后台可查看上传的图片数量,进行复制地址、白名单、黑名单和删除操作。

  • 如果遇到上传图片后在后台不可见的情况,需通过URL访问图片一次以解决(可通过PicGo上传解决,见下文)。

如果你想要一个更好看好用的后台,可以将admin.html替换为我魔改后的版本:

<!DOCTYPE html><html lang="zh"><head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ImgTC | Admin</title>
  <!-- Import CSS -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/theme-chalk/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css">
  <style>
    body {
      background: linear-gradient(90deg, #ffd7e4 0%, #c8f1ff 100%);
      font-family: 'Arial', sans-serif;
      color: #333;
      margin: 0;
      padding: 0;
    }

    .header-content {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 10px 20px;
      background-color: rgba(255, 255, 255, 0.75);
      backdrop-filter: blur(10px);
      border-bottom: 1px solid rgba(0, 0, 0, 0.1);
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      transition: background-color 0.5s ease, box-shadow 0.5s ease;
      border-bottom-left-radius: 10px;
      border-bottom-right-radius: 10px;
    }

    .header-content:hover {
      background-color: rgba(255, 255, 255, 0.85);
      box-shadow: 0 6px 10px rgba(0, 0, 0, 0.2);
    }

    .title {
      font-size: 1.8em;
      font-weight: bold;
      cursor: pointer;
      transition: color 0.3s ease;
      color: #333;
    }

    .title:hover {
      color: #B39DDB; /* 使用柔和的淡紫色 */
    }

    .search-card {
      margin-left: auto;
      margin-right: 20px;
    }

    .stats {
      font-size: 1.2em;
      margin-right: 20px;
      display: flex;
      align-items: center;
      background: rgba(255, 255, 255, 0.9);
      padding: 5px 10px;
      border-radius: 10px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      transition: background-color 0.3s ease, box-shadow 0.3s ease;
      color: #333;
    }

    .stats .fa-database {
      margin-right: 10px;
      font-size: 1.5em;
      transition: color 0.3s ease;
      color: inherit;
    }

    .stats:hover {
      background-color: #f0eaf8; /* 使用柔和的淡紫色 */
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
      color: #B39DDB; /* 使用柔和的淡紫色 */
    }

    .stats:hover .fa-database {
      color: #B39DDB; /* 使用柔和的淡紫色 */
    }

    .header-content .actions {
      display: flex;
      align-items: center;
      gap: 15px;
    }

    .header-content .actions i {
      font-size: 1.5em;
      cursor: pointer;
      transition: color 0.3s, transform 0.3s;
      color: #333;
    }

    .header-content .actions i:hover {
      color: #B39DDB; /* 使用柔和的淡紫色 */
      transform: scale(1.2);
    }

    .header-content .actions .el-dropdown-link i {
      color: #333;
    }

    .header-content .actions .el-dropdown-link i:hover {
      color: #B39DDB; /* 使用柔和的淡紫色 */
    }

    .header-content .actions .disabled {
      color: #bbb;
      pointer-events: none;
    }

    .header-content .actions .enabled {
      color: #B39DDB; /* 使用柔和的淡紫色 */
    }

    .search-card .el-input__inner {
      border-radius: 20px;
      width: 300px;
      height: 40px;
      font-size: 1.2em;
      border: none;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
      transition: width 0.3s;
    }

    .search-card .el-input__inner:focus {
      width: 400px;
    }

    .main-container {
      display: flex;
      flex-direction: column;
      padding: 20px;
      min-height: calc(100vh - 80px);
    }

    .content {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      grid-template-rows: repeat(3, 1fr);
      gap: 20px;
      padding: 10px;
      flex-grow: 1;
    }

    .el-card {
      width: 100%;
      background: rgba(255, 255, 255, 0.6);
      border-radius: 8px;
      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
      overflow: hidden;
      position: relative;
      transition: transform 0.3s ease;
    }

    .el-card:hover {
      transform: scale(1.05);
    }

    .el-image {
      width: 100%;
      height: 200px;
      object-fit: cover;
      transition: opacity 0.3s ease;
    }

    .el-image:hover {
      opacity: 0.8;
    }

    .file-info {
      padding: 10px;
      background: rgba(0, 0, 0, 0.6);
      color: white;
      text-align: center;
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      box-sizing: border-box;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .image-overlay {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      background: rgba(0, 0, 0, 0.6);
      opacity: 0;
      transition: opacity 0.3s ease;
      pointer-events: none;
    }

    .el-card:hover .image-overlay {
      opacity: 1;
    }

    .overlay-buttons {
      display: flex;
      gap: 10px;
      pointer-events: auto;
    }

    .pagination-container {
      display: flex;
      justify-content: center;
      margin-top: 20px;
      padding-bottom: 20px;
    }

    .el-checkbox {
      position: absolute;
      top: 10px;
      right: 10px;
      transform: scale(1.5);
      z-index: 10;
    }
  </style></head><body>
  <div id="app">
    <el-container>
      <el-header>
        <div class="header-content">
          <span class="title" @click="refreshDashboard">Dashboard</span>
          <div class="search-card">
            <el-input v-model="search" size="mini" placeholder="输入关键字搜索"></el-input>
          </div>
          <span class="stats">
            <i class="fas fa-database"></i> 记录总数量: {{ Number }}          </span>
          <div class="actions">
            <el-tooltip content="排序" placement="bottom">
              <el-dropdown @command="sort" :hide-on-click="false">
                <span class="el-dropdown-link">
                  <i :class="sortIcon"></i>
                </span>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="dateDesc" :class="{ 'el-dropdown-menu__item--selected': sortOption === 'dateDesc' }">按时间倒序</el-dropdown-item>
                  <el-dropdown-item command="nameAsc" :class="{ 'el-dropdown-menu__item--selected': sortOption === 'nameAsc' }">按名称升序</el-dropdown-item>
                </el-dropdown-menu>
              </el-dropdown>
            </el-tooltip>
            <el-tooltip content="批量复制" placement="bottom">
              <i class="fas fa-link" :class="{ disabled: selectedFiles.length === 0 }" @click="handleBatchCopy"></i>
            </el-tooltip>
            <el-tooltip content="批量删除" placement="bottom">
              <i class="fas fa-trash-alt" :class="{ disabled: selectedFiles.length === 0 }" @click="handleBatchDelete"></i>
            </el-tooltip>
            <el-tooltip content="退出登录" placement="bottom">
              <i class="fas fa-home" @click="handleLogout"></i>
            </el-tooltip>
          </div>
        </div>
      </el-header>
      <el-main class="main-container">
        <div class="content">
          <template v-for="(item, index) in paginatedTableData" :key="index">
            <el-card>
              <el-checkbox v-model="item.selected"></el-checkbox>
              <el-image
                :src="'/file/' + item.name"
                :preview-src-list="['/file/' + item.name]"
                fit="cover"
                lazy></el-image>
              <div class="image-overlay">
                <div class="overlay-buttons">
                  <el-button size="mini" type="primary" @click.stop="handleCopy(index, item.name)">复制地址</el-button>
                  <el-button size="mini" type="danger" @click.stop="handleDelete(index, item.name)">删除</el-button>
                </div>
              </div>
              <div class="file-info">{{ item.name }}</div>
            </el-card>
          </template>
        </div>
        <div class="pagination-container">
          <el-pagination
            background
            layout="prev, pager, next"
            :total="filteredTableData.length"
            :page-size="pageSize"
            @current-change="handlePageChange"
            :current-page.sync="currentPage">
          </el-pagination>
        </div>
      </el-main>
    </el-container>
  </div>

  <!-- Import Vue before Element -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
  <!-- Import JavaScript -->
  <script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/index.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        Number: 0,
        showLogoutButton: false,
        tableData: [],
        search: '',
        currentPage: 1,
        pageSize: 15,
        selectedFiles: [],
        sortOption: 'dateDesc',
        isUploading: false
      },
      computed: {
        filteredTableData() {
          return this.tableData.filter(data => !this.search || data.name.toLowerCase().includes(this.search.toLowerCase()));
        },
        paginatedTableData() {
          const sortedData = this.sortData(this.filteredTableData);
          const start = (this.currentPage - 1) * this.pageSize;
          const end = start + this.pageSize;
          return sortedData.slice(start, end);
        },
        sortIcon() {
          return this.sortOption === 'dateDesc' ? 'fas fa-sort-amount-down' : 'fas fa-sort-alpha-up';
        }
      },
      watch: {
        tableData: {
          handler(newData) {
            this.selectedFiles = newData.filter(file => file.selected);
          },
          deep: true
        },
        sortOption(newOption) {
          localStorage.setItem('sortOption', newOption);
        }
      },
      methods: {
        refreshDashboard() {
          location.reload();
        },
        handleDelete(index, key) {
          this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            fetch(`./api/manage/delete/${key}`, { method: 'GET', credentials: 'include' })
              .then(response => response.ok ? this.tableData.splice(index, 1) : Promise.reject())
              .then(() => {
                this.updateStats();
                this.$message.success('删除成功!');
              })
              .catch(() => this.$message.error('删除失败,请检查网络连接'));
          }).catch(() => this.$message.info('已取消删除'));
        },
        handleBatchDelete() {
          this.$confirm('此操作将永久删除选中的文件, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            const promises = this.selectedFiles.map(file => fetch(`./api/manage/delete/${file.name}`, { method: 'GET', credentials: 'include' }));

            Promise.all(promises)
              .then(results => {
                results.forEach((response, index) => {
                  if (response.ok) {
                    const fileIndex = this.tableData.findIndex(file => file.name === this.selectedFiles[index].name);
                    if (fileIndex !== -1) {
                      this.tableData.splice(fileIndex, 1);
                    }
                  }
                });
                this.selectedFiles = [];
                this.updateStats();
                this.$message.success('批量删除成功!');
              })
              .catch(() => this.$message.error('批量删除失败,请检查网络连接'));
          }).catch(() => this.$message.info('已取消批量删除'));
        },
        handleBatchCopy() {
          const links = this.selectedFiles.map(file => `${document.location.origin}/file/${file.name}`).join('\n');
          navigator.clipboard ? navigator.clipboard.writeText(links).then(() => this.$message.success('批量复制链接成功~')) :
            this.copyToClipboardFallback(links);
        },
        copyToClipboardFallback(text) {
          const textarea = document.createElement('textarea');
          document.body.appendChild(textarea);
          textarea.style.position = 'fixed';
          textarea.style.clip = 'rect(0 0 0 0)';
          textarea.style.top = '10px';
          textarea.value = text;
          textarea.select();
          document.execCommand('copy');
          document.body.removeChild(textarea);
          this.$message.success('批量复制链接成功~');
        },
        handleLogout() {
          window.location.href = '/';
        },
        handleCopy(index, key) {
          const text = `${document.location.origin}/file/${key}`;
          navigator.clipboard ? navigator.clipboard.writeText(text).then(() => this.$message.success('复制文件链接成功~')) :
            this.copyToClipboardFallback(text);
        },
        handlePageChange(page) {
          this.currentPage = page;
        },
        updateStats() {
          this.Number = this.tableData.length;
        },
        sort(command) {
          this.sortOption = command;
        },
        sortData(data) {
          return this.sortOption === 'nameAsc' ? data.sort((a, b) => a.name.localeCompare(b.name)) :
            data.sort((a, b) => b.metadata.TimeStamp - a.metadata.TimeStamp);
        }
      },
      mounted() {
        fetch("./api/manage/check", { method: 'GET', credentials: 'include' })
          .then(response => response.text())
          .then(result => result === "true" ? this.showLogoutButton = true : window.location.href = "./api/manage/login")
          .catch(() => this.$message.error('同步数据时出错,请检查网络连接'));

        fetch("./api/manage/list", { method: 'GET', credentials: 'include' })
          .then(response => response.json())
          .then(result => {
            this.tableData = result.map(file => ({ ...file, selected: false }));
            this.updateStats();
            const savedSortOption = localStorage.getItem('sortOption');
            if (savedSortOption) {
              this.sortOption = savedSortOption;
            }
            this.sortData(this.tableData);
          })
          .catch(() => this.$message.error('同步数据时出错,请检查网络连接'));
      }
    });
  </script></body></html>123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449


效果如图😯:

Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

注意事项

  1. 每日限制

    • 使用Cloudflare的免费资源,每日读取次数有限制(100,000次),删除操作有限制(1,000次)。

  2. 图片大小限制

    • Telegraph限制上传的图片大小最大为5MB

  3. 更新程序

    • 当项目作者更新代码后,通过Sync fork->Update branch进行同步更新,Cloudflare会自动重新部署。

  4. 其他


Typora使用图床

为了在Typora中方便地使用我们搭建的图床,可以将PicGo与Typora结合使用,实现自动上传图片至Telegraph,并生成图片链接。

配置PicGo

  1. 下载并安装PicGo

  2. 打开PicGo,选择“插件设置”,搜索并安装插件telegraph-image-uploader

    Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  3. 点击“图床设置”,选择刚刚下载的插件telegraph-image,将URL设为你的图床网址(如"https:// xxx.pages.dev",无需后缀),点击确定并设为默认图床Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

即使你不使用Typora,也可以使用PicGo的窗口上传图片,上传的图片无需手动访问即可在后台显示,且支持批量上传及图片Markdown格式的复制。


配置Typora

  1. 打开Typora,选择文件->偏好设置->图像

  2. 勾选“上传图片”,选择“PicGo(app)”,并填入PicGo的安装路径(即以PicGo.exe的文件路径),完成后重启Typora(请通过在Typora粘贴图片的方式测试是否成功上传,这样图片会自动加载一遍,稍候片刻就可以在后台看到)Telegraph+PicGo+Typora 免费无限图床(完整图文教程)

  3. 现在,您在Typora中插入图片时,PicGo会自动将图片上传到Telegraph,并返回图片链接🎉

  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin
avatar

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: