Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

后台魔改~(美化/批量处理/分页/排序 - 解决方案) #137

Open
DJChanahCJD opened this issue Jun 14, 2024 · 13 comments
Open

Comments

@DJChanahCJD
Copy link

DJChanahCJD commented Jun 14, 2024

目前只修改了admin.html,希望作者能够开放源代码,提供自定义前台的指南。

主要修改:

  • 优化界面,将照片做成卡片
  • 删除了黑/白名单
  • 增加了批量删除/复制链接
  • 增加了按时间倒序排序
  • 增加了分页功能

说明:

  • 点击图片可触发preview功能
  • 默认按上传时间排序
  • 批量上传功能暂时无法实现,推荐使用PicGo(这也解决了上传后需要手动访问一次才能在后台显示的Bug,另外Markdown图片链接也可以在PicGo上复制)

效果如图:
image-20240614191231653

@DJChanahCJD
Copy link
Author

DJChanahCJD commented Jun 14, 2024

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/[email protected]/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/[email protected]/dist/vue.min.js"></script>
  <!-- Import JavaScript -->
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/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>

@woshichenghaibo
Copy link

后台是什么样的?无所谓,反正传上去之后拿到链接,后台大半年都上不去一次

@DJChanahCJD DJChanahCJD changed the title 后台魔改~ 后台魔改~(美化/批量处理/分页/排序 - 解决方案) Jun 15, 2024
@DJChanahCJD
Copy link
Author

后台是什么样的?无所谓,反正传上去之后拿到链接,后台大半年都上不去一次

如果一直用下去的话,还是会在乎的吧哈哈哈(反正我是这样🤣)

@woshichenghaibo
Copy link

这个项目维护很少,缺乏图像压缩

@DJChanahCJD
Copy link
Author

这个项目维护很少,缺乏图像压缩

可以用PicGo的压缩插件,但好像也会导致图片需要手动访问一次才能同步到后台的问题

@woshichenghaibo
Copy link

woshichenghaibo commented Jun 17, 2024 via email

xmt3061123 added a commit to xmt3061123/Telegraph-Image that referenced this issue Jun 22, 2024
更新了后台管理界面cf-pages#137 (comment)
@cf-pages
Copy link
Owner

cf-pages commented Jul 4, 2024

@DJChanahCJD

感谢提供后台解决方案,目前已将该代码合并到仓库当中(PR:#145),后序如果有新的更改,欢迎通过提交pull request的方式提交,谢谢!

目前原本的后台管理页面和你魔改后的页面同时存在,用户就自行选择使用哪一个版本的后台管理页面

用户如果需要访问原版后台,可以使用 example.com/admin 如果需要访问魔改后的后台,可以使用 example.com/admin-ImgTC 需要使用魔改版后台的用户需要更新到最新版本的代码

@leonWangFearless
Copy link

@DJChanahCJD

感谢提供后台解决方案,目前已将该代码合并到仓库当中(PR:#145),后序如果有新的更改,欢迎通过提交pull request的方式提交,谢谢!

目前原本的后台管理页面和你魔改后的页面同时存在,用户就自行选择使用哪一个版本的后台管理页面

用户如果需要访问原版后台,可以使用 example.com/admin 如果需要访问魔改后的后台,可以使用 example.com/admin-ImgTC 需要使用魔改版后台的用户需要更新到最新版本的代码

admin-ImgTC这个路径在输入地址栏之后会变成 admin-imgTC, 是不是路径匹配错误,导致无法跳转到新的后台页面?还是说发生了一些在我预知之外的事情,希望得到解答,感谢。

@cf-pages
Copy link
Owner

cf-pages commented Jul 4, 2024

@DJChanahCJD
感谢提供后台解决方案,目前已将该代码合并到仓库当中(PR:#145),后序如果有新的更改,欢迎通过提交pull request的方式提交,谢谢!
目前原本的后台管理页面和你魔改后的页面同时存在,用户就自行选择使用哪一个版本的后台管理页面
用户如果需要访问原版后台,可以使用 example.com/admin 如果需要访问魔改后的后台,可以使用 example.com/admin-ImgTC 需要使用魔改版后台的用户需要更新到最新版本的代码

admin-ImgTC这个路径在输入地址栏之后会变成 admin-imgTC, 是不是路径匹配错误,导致无法跳转到新的后台页面?还是说发生了一些在我预知之外的事情,希望得到解答,感谢。

请问你使用的是哪一种浏览器?我这边在Google Chrome下测试是没有问题的,Chrome不会转换大小写

@leonWangFearless
Copy link

leonWangFearless commented Jul 5, 2024

@DJChanahCJD
感谢提供后台解决方案,目前已将该代码合并到仓库当中(PR:#145),后序如果有新的更改,欢迎通过提交pull request的方式提交,谢谢!
目前原本的后台管理页面和你魔改后的页面同时存在,用户就自行选择使用哪一个版本的后台管理页面
用户如果需要访问原版后台,可以使用 example.com/admin 如果需要访问魔改后的后台,可以使用 example.com/admin-ImgTC 需要使用魔改版后台的用户需要更新到最新版本的代码

admin-ImgTC这个路径在输入地址栏之后会变成 admin-imgTC, 是不是路径匹配错误,导致无法跳转到新的后台页面?还是说发生了一些在我预知之外的事情,希望得到解答,感谢。

请问你使用的是哪一种浏览器?我这边在Google Chrome下测试是没有问题的,Chrome不会转换大小写

经你提醒,刚刚使用edge访问能正常匹配了, 此前是chrome浏览器无法正常访问,应该是有一些配置或者规则的问题,我去找找

@SokWith
Copy link

SokWith commented Aug 23, 2024

#133 (comment)
@DJChanahCJD 谢谢。是我想要的相册的模样,所以就借鉴在我的相册里面了。

@SokWith
Copy link

SokWith commented Aug 23, 2024

@DJChanahCJD 大佬,好像移动体验需要改进

@DJChanahCJD
Copy link
Author

DJChanahCJD commented Oct 19, 2024

@DJChanahCJD 大佬,好像移动体验需要改进

已知晓,优化中...(本来没想用移动端🤣)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants