切换上传渠道为telegram

This commit is contained in:
MarSeventh
2024-09-07 17:55:33 +08:00
parent 6132c9ade1
commit f9171600de
13 changed files with 945 additions and 799 deletions
+32 -8
View File
@@ -1,6 +1,6 @@
# CloudFlare-ImgBed
免费图片托管解决方案,基于 Cloudflare Pages 和 telegra.ph(图片通过压缩,不受5MB大小限制)
免费图片托管解决方案,基于 Cloudflare Pages 和 Telegram
**体验地址**[Sanyue ImgHub (demo-cloudflare-imgbed.pages.dev)](https://demo-cloudflare-imgbed.pages.dev/)
@@ -16,7 +16,7 @@
## 1.Introduction
免费图片托管解决方案,保留原版全部功能的基础上,实现了**登录鉴权**、**页面自定义**、**上传图片预览**、**一键切换上传方式**(**拖拽上传**、**粘贴上传**)、**多文件上传**、**整体复制**、**多格式复制**、**上传前自动压缩**(可上传>5MB图片)等功能。
免费图片托管解决方案,保留原版全部功能的基础上,实现了**登录鉴权**、**页面自定义**、**上传图片预览**、**一键切换上传方式**(**拖拽上传**、**粘贴上传**)、**多文件上传**、**整体复制**、**多格式复制**、**上传前自动压缩**(提升加载性能)等功能。
![](https://alist.sanyue.site/d/imgbed/202408191757569.png)
@@ -26,6 +26,8 @@
![](https://alist.sanyue.site/d/imgbed/202408211614780.png)
![](https://alist.sanyue.site/d/imgbed/202409071737881.png)
## 2.Features
- **开源**
@@ -41,7 +43,7 @@
- 支持批量上传(不限同时选择文件数量,但为了保证稳定性,同时处于上传状态的文件最多为10个)
- 上传显示实时上传进度
- **上传后图片无需手动点击,可直接展示在管理页面中**
- **突破5MB大小限制,超过5MB的图片上传前会自动压缩**(超过5MB的视频暂不支持)
- **大于5MB在前端进行压缩,提升上传稳定性和加载性能**
- **多样化复制**
@@ -78,13 +80,31 @@
### 3.1直接使用
**部署方式**与**环境变量**和原仓库保持一致。(**修改完环境变量,重新部署才能生效**,见[3.1章最后一节](#3.1.10注意!!!)
**注意修改完环境变量,重新部署才能生效**,见[3.1章最后一节](#3.1.10注意!!!)
#### 3.1.1提前准备
- **Telegram的`BOT_TOKEN``CHAT_ID`**
首先需要拥有一个Telegram账户,然后按照以下步骤获取`BOT_TOKEN``CHAT_ID`
1. 向[@BotFather](https://t.me/BotFather)发送`/newbot`,按照提示输入bot的备注、用户名等信息。成功创建后获得`BOT_TOKEN`
![](https://alist.sanyue.site/d/imgbed/202409071744569.png)
2. 创建一个新的频道(Channel),进入新建的频道,选择频道管理,将刚才创建的机器人设为频道管理员。
<img src="C:\Users\King.xx\AppData\Roaming\Typora\typora-user-images\image-20240907174731039.png" style="zoom:50%;" />
<img src="C:\Users\King.xx\AppData\Roaming\Typora\typora-user-images\image-20240907174830310.png" alt="image-20240907174830310" style="zoom:50%;" />
3. 向[@VersaToolsBot](https://t.me/VersaToolsBot)发消息,按步骤操作获取`CHAT_ID`(频道ID
![](https://alist.sanyue.site/d/imgbed/202409071751619.png)
- **部署于Cloudflare**
需准备一个**Cloudflare账户**,然后按照[3.1.2.1节](#3.1.2.1部署于Cloudflare)的步骤即可完成部署。
需准备一个**Cloudflare账户**,然后按照[3.1.2.1节](#3.1.2.1部署于Cloudflare)的步骤即可完成部署。
- **部署于服务器**
@@ -96,7 +116,7 @@
##### 3.1.2.1部署于Cloudflare
依托于CF的强大能力,只需简单 3 步,即可部署本项目,拥有自己的图床。
依托于CF的强大能力,只需简单步,即可部署本项目,拥有自己的图床。
1. Fork 本仓库 (注意:必须使用 Git 或者 Wrangler 命令行工具部署后才能正常使用,[文档](https://developers.cloudflare.com/pages/functions/get-started/#deploy-your-function))
@@ -104,7 +124,9 @@
![1](https://alist.sanyue.site/d/imgbed/202407201047300.png)
3. 按照页面提示输入项目名称,选择需要连接的 git 仓库,点击`部署站点`即可完成部署
3. 按照页面提示输入项目名称,选择需要连接的 git 仓库,点击`部署站点`
3. 将3.1.1中获取的`BOT_TOKEN``CHAT_ID`分别添加到环境变量中,对应**环境变量名为`TG_BOT_TOKEN``TG_CHAT_ID`**。
3. `重试部署`,此时项目即可正常使用
##### 3.1.2.2部署于服务器
@@ -112,7 +134,7 @@
2. 切换到项目根目录,运行`npm install`,安装所需依赖。
3. 在项目根目录下新建`wrangler.toml`配置文件,其内容为项目名称,环境变量等,可根据后文环境变量配置进行个性化修改。(详情参见官方文档[Configuration - Wrangler (cloudflare.com)](https://developers.cloudflare.com/workers/wrangler/configuration/)
3. 在项目根目录下新建`wrangler.toml`配置文件,其内容为项目名称,环境变量**包括`TG_BOT_TOKEN``TG_CHAT_ID`等必填参数**等,可根据后文环境变量配置进行个性化修改。(详情参见官方文档[Configuration - Wrangler (cloudflare.com)](https://developers.cloudflare.com/workers/wrangler/configuration/)
> 配置文件样例:
>
@@ -125,6 +147,8 @@
> AllowRandom = "true"
> BASIC_USER = "user"
> BASIC_PASS = "pass"
> TG_BOT_TOKEN = "your_bot_token"
> TG_CHAT_ID = "your_bot_id"
> ```
4. 在项目根目录下运行`npm run start`,至此,正常情况下项目已经成功部署。
+341
View File
@@ -0,0 +1,341 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- import CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/lib/theme-chalk/index.css" integrity="sha256-ghr1zmXTODLKl1HULQd6fq1MIe7m3FJiNTOCT8sddLM=" crossorigin="anonymous">
<style>
.el-image__inner.el-image__inner {
width: 100%;
height: 90px;
}
.el-image {
text-align: center;
}
.el-button+.el-button {
margin: 0;
}
</style>
<script src="https://js.sentry-cdn.com/219f636ac7bde5edab2c3e16885cb535.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<div style="
margin: auto;
line-height: 60px;
font-size: xx-large;
position: relative;
">Dashboard
<span style="
position: absolute;
right: 0px;
" v-if="showLogoutButton">
<a href="./admin.html">
<el-button
size="mini"
type="warning"
>主管理页</el-button>
</a>
<a href="./admin-waterfall.html">
<el-button
size="mini"
type="primary"
>瀑布流</el-button>
</a>
<el-button
size="mini"
type="info"
@click="handleLogout()">退出登录</el-button></span></div>
</el-header>
<el-main><el-row :gutter="12">
<el-col :span="24">
<el-card shadow="always">
记录总数量:
{{ Number }}
</el-card>
</el-col>
<!--<el-col :span="8">
<el-card shadow="hover">
<el-tooltip class="item" effect="dark" content="白名单数量" placement="top-start">
</el-tooltip>
白名单数量:{{ WhiteList }}
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<el-tooltip class="item" effect="dark" content="黑名单数量" placement="top-start">
</el-tooltip>
黑名单数量:{{ BlackList }}
</el-card>
</el-col>-->
</el-row>
<template>
<el-table
:data="tableData.filter(data => !search || data.name.toLowerCase().includes(search.toLowerCase()))"
style="width: 100%">
<el-table-column
label="name"
prop="name">
</el-table-column>
<el-table-column
label="preview"
prop="preview"
align="center">
<template slot-scope="scope">
<video v-if="scope.row.name.indexOf('.mp4')>0" style="width: 100%; height: 180px;" controls>
<source :src="'/file/'+scope.row.name" type="video/mp4">
</video>
<el-image
v-else
style="width: 100%; height: 100%;"
:src="'/file/'+scope.row.name"
:zoom-rate="1.2"
:preview-src-list="['/file/'+scope.row.name]"
fit="cover"
lazy
/>
</template>
</el-table-column>
<el-table-column
label="data"
prop="data">
<template slot-scope="scope">
<el-popover trigger="hover" placement="top">
<p>{{ scope.row.metadata }}</p>
<div slot="reference" class="name-wrapper">
<el-tag size="medium">{{ scope.row.metadata }}</el-tag>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column
align="right">
<template slot="header" slot-scope="scope">
<el-input
v-model="search"
size="mini"
placeholder="输入关键字搜索"/>
</template>
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="handleCopy(scope.$index,scope.row.name)">复制地址</el-button>
<el-button
size="mini"
type="primary"
@click="handleWhite(scope.$index,scope.row.name)">白名单</el-button>
<el-button
size="mini"
type="info"
@click="handleBlock(scope.$index,scope.row.name)">黑名单</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index,scope.row.name)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
</el-main>
</el-container>
</div>
</body>
<!-- import Vue before Element -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js" integrity="sha256-kXTEJcRFN330VirZFl6gj9+UM6gIKW195fYZeR3xDhc=" crossorigin="anonymous"></script>
<!-- import JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.js" integrity="sha256-OFVFYfqhQ9nDnKh+NfIsefpy/fnjTwkK909ZYgo45nw=" crossorigin="anonymous"></script>
<script>
var app=new Vue({
el: '#app',
data: {
Number:0,
WhiteList:0,
BlackList:0,
showLogoutButton:false,
tableData: [],
dialogFormVisible: false,
formLabelWidth: '120px',
form: {
name: '',
id: ''
},
search: '',
password:'123456'
},
methods: {
handleBlock(index,key) {
console.log(key);
if (confirm("确认加入黑名单吗?")) {
console.log("Yes")
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/block/"+key, requestOptions)
.then(response => response.text())
.then(result => {console.log(result);
this.tableData[index].metadata=result;})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
} else {
console.log("No")
}
},
handleDelete(index,key) {
console.log(key);
if (confirm("确认删除该条记录吗?")) {
console.log("Yes")
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/delete/"+key, requestOptions)
.then(response => response.text())
.then(result => {console.log(result);this.tableData.remove(index);})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
} else {
console.log("No")
}
},
handleWhite(index,key) {
console.log(key);
if (confirm("确认加入白名单吗?")) {
console.log("Yes")
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/white/"+key, requestOptions)
.then(response => response.text())
.then(result => {console.log(result);this.tableData[index].metadata=result;})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
} else {
console.log("No")
}
},
handleLogout(){
window.location.href="./api/manage/logout";
},
handleCopy(index, key) {
const text = `${document.location.origin}/file/${key}`;
if (navigator.clipboard) {
// clipboard api 复制
navigator.clipboard.writeText(text);
} else {
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', true);
// 移除输入框
document.body.removeChild(textarea);
}
this.$message({
message: '复制文件链接成功~',
type: 'success'
});
},
},
mounted () {
//check if the user is logged in
//read the basic auth credientials from the browser
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/check", requestOptions)
.then(response => response.text())
.then(result => {console.log(result);
if(result=="true"){
this.showLogoutButton=true;
}else if(result=="Not using basic auth."){
}
else{
window.location.href="./api/manage/login";
}
})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
Array.prototype.remove = function(from, to) {
var rest = this.slice((to || from) + 1 || this.length);
this.length = from < 0 ? this.length + from : from;
return this.push.apply(this, rest);
};
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/list", requestOptions)
//判断是否需要登录
.then(response => {
if(response.status==401){
alert("请先登录");
window.location.href="./api/manage/login";
}
else{
return response;
}
})
.then(response => response.text())
.then(result => {this.tableData=JSON.parse(result);console.log(result);this.Number=this.tableData.length})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
}
})
</script><!-- Hotjar Tracking Code -->
<script>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:2531461,hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "7t5ai7agat");
</script>
</html>
-450
View File
@@ -1,450 +0,0 @@
<!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">
<script src="https://js.sentry-cdn.com/219f636ac7bde5edab2c3e16885cb535.min.js" crossorigin="anonymous"></script>
<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>
+431 -316
View File
@@ -1,341 +1,456 @@
<!DOCTYPE html>
<html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<!-- import CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/lib/theme-chalk/index.css" integrity="sha256-ghr1zmXTODLKl1HULQd6fq1MIe7m3FJiNTOCT8sddLM=" crossorigin="anonymous">
<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">
<script src="https://js.sentry-cdn.com/219f636ac7bde5edab2c3e16885cb535.min.js" crossorigin="anonymous"></script>
<style>
.el-image__inner.el-image__inner {
width: 100%;
height: 90px;
}
.el-image {
text-align: center;
}
.el-button+.el-button {
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>
<script src="https://js.sentry-cdn.com/219f636ac7bde5edab2c3e16885cb535.min.js" crossorigin="anonymous"></script>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<div style="
margin: auto;
line-height: 60px;
font-size: xx-large;
position: relative;
">Dashboard
<span style="
position: absolute;
right: 0px;
" v-if="showLogoutButton">
<a href="./admin-imgtc.html">
<el-button
size="mini"
type="warning"
>网格视图</el-button>
</a>
<a href="./admin-waterfall.html">
<el-button
size="mini"
type="primary"
>瀑布流</el-button>
</a>
<el-button
size="mini"
type="info"
@click="handleLogout()">退出登录</el-button></span></div>
</el-header>
<el-main><el-row :gutter="12">
<el-col :span="24">
<el-card shadow="always">
记录总数量:
{{ Number }}
</el-card>
</el-col>
<!--<el-col :span="8">
<el-card shadow="hover">
<el-tooltip class="item" effect="dark" content="白名单数量" placement="top-start">
</el-tooltip>
白名单数量:{{ WhiteList }}
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<el-tooltip class="item" effect="dark" content="黑名单数量" placement="top-start">
</el-tooltip>
黑名单数量:{{ BlackList }}
</el-card>
</el-col>-->
</el-row>
<template>
<el-table
:data="tableData.filter(data => !search || data.name.toLowerCase().includes(search.toLowerCase()))"
style="width: 100%">
<el-table-column
label="name"
prop="name">
</el-table-column>
<el-table-column
label="preview"
prop="preview"
align="center">
<template slot-scope="scope">
<video v-if="scope.row.name.indexOf('.mp4')>0" style="width: 100%; height: 180px;" controls>
<source :src="'/file/'+scope.row.name" type="video/mp4">
</video>
<el-image
v-else
style="width: 100%; height: 100%;"
:src="'/file/'+scope.row.name"
:zoom-rate="1.2"
:preview-src-list="['/file/'+scope.row.name]"
fit="cover"
lazy
/>
</template>
</el-table-column>
<el-table-column
label="data"
prop="data">
<template slot-scope="scope">
<el-popover trigger="hover" placement="top">
<p>{{ scope.row.metadata }}</p>
<div slot="reference" class="name-wrapper">
<el-tag size="medium">{{ scope.row.metadata }}</el-tag>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column
align="right">
<template slot="header" slot-scope="scope">
<el-input
v-model="search"
size="mini"
placeholder="输入关键字搜索"/>
</template>
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="handleCopy(scope.$index,scope.row.name)">复制地址</el-button>
<el-button
size="mini"
type="primary"
@click="handleWhite(scope.$index,scope.row.name)">白名单</el-button>
<el-button
size="mini"
type="info"
@click="handleBlock(scope.$index,scope.row.name)">黑名单</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index,scope.row.name)">删除</el-button>
</template>
</el-table-column>
</el-table>
<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-user-cog" @click="handleGoToAdmin"></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.metadata?.FileName || item.name }}</div>
</el-card>
</template>
</el-main>
</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>
</body>
<!-- import Vue before Element -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js" integrity="sha256-kXTEJcRFN330VirZFl6gj9+UM6gIKW195fYZeR3xDhc=" crossorigin="anonymous"></script>
<!-- import JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.js" integrity="sha256-OFVFYfqhQ9nDnKh+NfIsefpy/fnjTwkK909ZYgo45nw=" crossorigin="anonymous"></script>
<!-- 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>
var app=new Vue({
new Vue({
el: '#app',
data: {
Number:0,
WhiteList:0,
BlackList:0,
showLogoutButton:false,
Number: 0,
showLogoutButton: false,
tableData: [],
dialogFormVisible: false,
formLabelWidth: '120px',
form: {
name: '',
id: ''
},
search: '',
password:'123456'
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: {
handleBlock(index,key) {
console.log(key);
if (confirm("确认加入黑名单吗?")) {
console.log("Yes")
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
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' }));
fetch("./api/manage/block/"+key, requestOptions)
.then(response => response.text())
.then(result => {console.log(result);
this.tableData[index].metadata=result;})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
} else {
console.log("No")
}
},
handleDelete(index,key) {
console.log(key);
if (confirm("确认删除该条记录吗?")) {
console.log("Yes")
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/delete/"+key, requestOptions)
.then(response => response.text())
.then(result => {console.log(result);this.tableData.remove(index);})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
} else {
console.log("No")
}
},
handleWhite(index,key) {
console.log(key);
if (confirm("确认加入白名单吗?")) {
console.log("Yes")
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/white/"+key, requestOptions)
.then(response => response.text())
.then(result => {console.log(result);this.tableData[index].metadata=result;})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
} else {
console.log("No")
}
},
handleLogout(){
window.location.href="./api/manage/logout";
},
handleCopy(index, key) {
const text = `${document.location.origin}/file/${key}`;
if (navigator.clipboard) {
// clipboard api 复制
navigator.clipboard.writeText(text);
} else {
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', true);
// 移除输入框
document.body.removeChild(textarea);
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 = '/';
},
handleGoToAdmin() {
window.location.href = '/admin-detail';
},
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);
}
this.$message({
message: '复制文件链接成功~',
type: 'success'
});
},
},
mounted () {
//check if the user is logged in
//read the basic auth credientials from the browser
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/check", requestOptions)
.then(response => response.text())
.then(result => {console.log(result);
if(result=="true"){
this.showLogoutButton=true;
}else if(result=="Not using basic auth."){
}
else{
window.location.href="./api/manage/login";
}
})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
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('同步数据时出错,请检查网络连接'));
Array.prototype.remove = function(from, to) {
var rest = this.slice((to || from) + 1 || this.length);
this.length = from < 0 ? this.length + from : from;
return this.push.apply(this, rest);
};
var requestOptions = {
method: 'GET',
redirect: 'follow',
//include authorization credientials
credentials: 'include'
};
fetch("./api/manage/list", requestOptions)
//判断是否需要登录
.then(response => {
if(response.status==401){
alert("请先登录");
window.location.href="./api/manage/login";
}
else{
return response;
}
})
.then(response => response.text())
.then(result => {this.tableData=JSON.parse(result);console.log(result);this.Number=this.tableData.length})
.catch(error => {alert("An error occurred while synchronizing data with the server, please check the network connection");console.log('error', error)});
}
})
</script><!-- Hotjar Tracking Code -->
<script>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:2531461,hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "7t5ai7agat");
</script>
</html>
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>
+1
View File
@@ -0,0 +1 @@
@keyframes breathe-2e593758{50%{box-shadow:0 0 10px 5px #409eff;opacity:.8}}.upload-form[data-v-2e593758],.upload-list-card[data-v-2e593758]{display:flex;flex-direction:column;justify-content:center;align-items:center}.upload-list-card[data-v-2e593758]{width:55vw;height:7vh;margin-top:10px;border-radius:15px;opacity:.8;background-color:hsla(0,0%,100%,.7);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.upload-list-container[data-v-2e593758]{width:55vw;height:7vh}.upload-list-card.upload-list-busy[data-v-2e593758],.upload-list-container.upload-list-busy[data-v-2e593758]{height:35vh}.upload-list-item[data-v-2e593758]{display:flex;align-items:center;justify-content:space-between;margin:5px;border:1px solid #a5bef7;padding:5px;border-radius:15px}.upload-list-item-name[data-v-2e593758]{font-size:small;font-weight:700;width:28vw;margin-bottom:5px}.upload-list-item-content[data-v-2e593758]{display:flex;flex-direction:column;margin-left:10px}.upload-list-item-url-text[data-v-2e593758]{width:28vw}.upload-list-item-url-row[data-v-2e593758]{display:flex;flex-direction:row;align-items:center}.upload-list-item-progress[data-v-2e593758]{margin-top:3px;width:28vw}.upload-list-item-action[data-v-2e593758]{display:flex;flex-direction:column;align-items:center}.upload-list-item-action-button[data-v-2e593758]{margin:2px}.upload-card[data-v-2e593758]{width:55vw;padding:20px;background:none}.upload-card-busy[data-v-2e593758] .el-upload-dragger{height:25vh}.paste-mode[data-v-2e593758] .el-upload{pointer-events:none}[data-v-2e593758] .el-upload-dragger{display:flex;flex-direction:column;justify-content:center;align-items:center;height:45vh;border-radius:15px;border:3px dashed #409eff;opacity:.7;background-color:hsla(0,0%,100%,.6);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);transition:all .3s ease}[data-v-2e593758] .el-upload-dragger.is-dragover,[data-v-2e593758] .el-upload-dragger:hover{opacity:.8;box-shadow:0 0 10px 5px #409eff}.is-uploading[data-v-2e593758] .el-upload-dragger{animation:breathe-2e593758 3s infinite}.el-upload__text[data-v-2e593758]{font-weight:700;font-size:medium;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-upload__tip[data-v-2e593758]{font-size:small;color:#faebd7;-webkit-user-select:none;-moz-user-select:none;user-select:none}.upload-list-dashboard[data-v-2e593758]{display:flex;justify-content:space-between;align-items:center;padding:10px}.upload-list-dashboard-title[data-v-2e593758]{font-size:medium;font-weight:700}.page-footer[data-v-7c801e22]{position:fixed;bottom:0;display:flex;justify-content:center;align-items:center;width:100vw;color:#f0f8ff;font-size:large;-webkit-user-select:none;-moz-user-select:none;user-select:none}.footer-name[data-v-7c801e22]{color:#faebd7;font-weight:700;text-decoration:none}.toolbar[data-v-124b8fea]{position:fixed;bottom:8vh;right:1.5vw;display:flex;flex-direction:column;align-items:center;z-index:100}.toolbar-button[data-v-124b8fea]{border:none;transition:all .3s ease;margin-bottom:10px;margin-left:0}.toolbar-button[data-v-124b8fea]:hover{box-shadow:0 0 10px 0 rgba(0,0,0,.1);transform:translateY(-3px)}[data-v-124b8fea] .el-dialog{border-radius:12px;background-color:hsla(0,0%,100%,.7);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);box-shadow:0 0 10px 2px rgba(0,0,0,.1)}.dialog-action[data-v-124b8fea]{display:flex;justify-content:center;margin-top:20px}.header[data-v-124b8fea]{display:flex;justify-content:center;align-items:center;padding:15px;position:fixed;top:5vh;color:#ffebcd;-webkit-user-select:none;-moz-user-select:none;user-select:none;text-decoration:none}.main-title[data-v-124b8fea]{background:linear-gradient(90deg,#effac3,#f3a060);-webkit-background-clip:text;background-clip:text;color:transparent;text-decoration:none}.logo[data-v-124b8fea]{height:80px;width:80px;margin-right:5px}.upload-home[data-v-124b8fea]{display:flex;flex-direction:column;justify-content:center;align-items:center;transition:background-image 1s ease-in-out;background-size:cover;background-attachment:fixed;height:100vh}.upload[data-v-124b8fea]{position:fixed;top:20vh}.background-image1[data-v-124b8fea],.background-image2[data-v-124b8fea]{position:fixed;top:0;left:0;width:100%;height:100%;-o-object-fit:cover;object-fit:cover;z-index:-1;opacity:0;transition:all 1s ease-in-out}
+21 -9
View File
@@ -27,14 +27,23 @@ export async function onRequest(context) { // Contents of context object
return Response.redirect(new URL("/block-img.html", request.url).href, 302); // Ensure URL is correctly formed
}
}
const imgRecord = await env.img_url.getWithMetadata(params.id);
const response = fetch('https://telegra.ph/' + url.pathname + url.search, {
let targetUrl = '';
if (imgRecord.metadata?.Channel === 'Telegram') {
targetUrl = `https://api.telegram.org/file/bot${env.TG_BOT_TOKEN}/${imgRecord.metadata.TgFilePath}`;
} else {
targetUrl = 'https://telegra.ph/' + url.pathname + url.search;
}
const fileName = imgRecord.metadata?.FileName || 'file';
const encodedFileName = encodeURIComponent(fileName);
const fileType = imgRecord.metadata?.FileType || 'image/jpeg';
const response = await fetch(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
}).then(async (response) => {
console.log(response.ok); // true if the response status is 2xx
console.log(response.status); // 200
if (response.ok) {
// Referer header equal to the admin page
console.log(url.origin + "/admin")
@@ -46,8 +55,6 @@ export async function onRequest(context) { // Contents of context object
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") { } else {
//check the record from kv
const record = await env.img_url.getWithMetadata(params.id);
console.log("record")
console.log(record)
if (record.metadata === null) {
} else {
@@ -93,7 +100,6 @@ export async function onRequest(context) { // Contents of context object
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
console.log("Not enbaled KV")
} else {
//add image to kv
await env.img_url.put(params.id, "", {
@@ -102,7 +108,7 @@ export async function onRequest(context) { // Contents of context object
}
} else {
await fetch(`https://api.moderatecontent.com/moderate/?key=` + apikey + `&url=https://telegra.ph/` + url.pathname + url.search).
await fetch(`https://api.moderatecontent.com/moderate/?key=` + apikey + `&url=${targetUrl}`).
then(async (response) => {
let moderate_data = await response.json();
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") { } else {
@@ -121,6 +127,12 @@ export async function onRequest(context) { // Contents of context object
return response;
});
return response;
const headers = new Headers(response.headers);
headers.set('Content-Disposition', `inline; filename="${encodedFileName}"`);
headers.set('Content-Type', fileType);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
+112 -14
View File
@@ -30,7 +30,30 @@ function getCookieValue(cookies, name) {
export async function onRequestPost(context) { // Contents of context object
const { request, env, params, waitUntil, next, data } = context;
// await errorHandling(context);
// telemetryData(context);
const url = new URL(request.url);
const clonedRequest = await request.clone();
const formdata = await request.formData();
const fileType = formdata.get('file').type;
const fileName = formdata.get('file').name;
const fileTypeMap = {
'image/': {'url': 'sendPhoto', 'type': 'photo'},
'video/': {'url': 'sendVideo', 'type': 'video'},
'audio/': {'url': 'sendAudio', 'type': 'audio'},
'application/pdf': {'url': 'sendDocument', 'type': 'document'},
};
const defaultType = {'url': 'sendDocument', 'type': 'document'};
const sendFunction = Object.keys(fileTypeMap).find(key => fileType.startsWith(key))
? fileTypeMap[Object.keys(fileTypeMap).find(key => fileType.startsWith(key))]
: defaultType;
// 优先从请求 URL 获取 authCode
let authCode = url.searchParams.get('authCode');
// 如果 URL 中没有 authCode,从 Referer 中获取
@@ -59,11 +82,14 @@ export async function onRequestPost(context) { // Contents of context object
if (isAuthCodeDefined(env.AUTH_CODE) && !isValidAuthCode(env.AUTH_CODE, authCode)) {
return new UnauthorizedException("error");
}
const clonedRequest = request.clone();
await errorHandling(context);
telemetryData(context);
// 构建目标 URL 时剔除 authCode 参数
const targetUrl = new URL(url.pathname, 'https://telegra.ph');
// const targetUrl = new URL(url.pathname, 'https://telegra.ph'); // telegraph接口,已失效,缅怀
const targetUrl = new URL(`https://api.telegram.org/bot${env.TG_BOT_TOKEN}/${sendFunction.url}`); // telegram接口
let newFormdata = new FormData();
newFormdata.append('chat_id', env.TG_CHAT_ID);
newFormdata.append(sendFunction.type, formdata.get('file'));
url.searchParams.forEach((value, key) => {
if (key !== 'authCode') {
targetUrl.searchParams.append(key, value);
@@ -72,16 +98,33 @@ export async function onRequestPost(context) { // Contents of context object
// 复制请求头并剔除 authCode
const headers = new Headers(clonedRequest.headers);
headers.delete('authCode');
const response = await fetch(targetUrl.href, {
method: clonedRequest.method,
headers: headers,
body: clonedRequest.body,
});
let res = new Response('upload error, check your environment params!', { status: 400 });
try {
const response = await fetch(targetUrl.href, {
method: clonedRequest.method,
headers: {
"User-Agent": " Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0"
},
body: newFormdata,
});
const clonedRes = await response.clone().json(); // 等待响应克隆和解析完成
const fileInfo = getFile(clonedRes);
const filePath = await getFilePath(env, fileInfo.file_id);
// 若上传成功,将响应返回给客户端
if (response.ok) {
res = new Response(
JSON.stringify([{ 'src': `/file/${fileInfo.file_id}` }]),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}
const time = new Date().getTime();
const src = clonedRes[0].src;
const id = src.split('/').pop();
// const src = clonedRes[0].src;
// const id = src.split('/').pop();
const id = fileInfo.file_id;
const img_url = env.img_url;
const apikey = env.ModerateContentApiKey;
@@ -94,13 +137,13 @@ export async function onRequestPost(context) { // Contents of context object
});
} else {
try {
const fetchResponse = await fetch(`https://api.moderatecontent.com/moderate/?key=${apikey}&url=https://telegra.ph/${src}`);
const fetchResponse = await fetch(`https://api.moderatecontent.com/moderate/?key=${apikey}&url=https://api.telegram.org/file/bot${env.TG_BOT_TOKEN}/${filePath}`);
if (!fetchResponse.ok) {
throw new Error(`HTTP error! status: ${fetchResponse.status}`);
}
const moderate_data = await fetchResponse.json();
await env.img_url.put(id, "", {
metadata: { ListType: "None", Label: moderate_data.rating_label, TimeStamp: time },
metadata: { FileName: fileName, FileType: fileType, ListType: "None", Label: moderate_data.rating_label, TimeStamp: time, Channel: "Telegram", TgFilePath: filePath },
});
} catch (error) {
console.error('Moderate Error:', error);
@@ -112,6 +155,61 @@ export async function onRequestPost(context) { // Contents of context object
} catch (error) {
console.error('Error:', error);
} finally {
return response;
return res;
}
}
function getFile(response) {
try {
if (!response.ok) {
return null;
}
const getFileDetails = (file) => ({
file_id: file.file_id,
file_name: file.file_name || file.file_unique_id
});
if (response.result.photo) {
const largestPhoto = response.result.photo.reduce((prev, current) =>
(prev.file_size > current.file_size) ? prev : current
);
return getFileDetails(largestPhoto);
}
if (response.result.video) {
return getFileDetails(response.result.video);
}
if (response.result.document) {
return getFileDetails(response.result.document);
}
return null;
} catch (error) {
console.error('Error getting file id:', error.message);
return null;
}
}
async function getFilePath(env, file_id) {
try {
const url = `https://api.telegram.org/bot${env.TG_BOT_TOKEN}/getFile?file_id=${file_id}`;
const res = await fetch(url, {
method: 'GET',
headers: {
"User-Agent": " Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome"
},
})
let responseData = await res.json();
if (responseData.ok) {
const file_path = responseData.result.file_path
return file_path
} else {
return null;
}
} catch (error) {
return null;
}
}
-1
View File
@@ -14,7 +14,6 @@ export async function errorHandling(context) {
}
} catch (e) { console.log(e) }
const sampleRate = env.sampleRate || remoteSampleRate;
console.log("sampleRate", sampleRate);
return sentryPlugin({
dsn: "https://44b7b443108ec6d298044b125ff89d28@o4507644548022272.ingest.us.sentry.io/4507644555100160",
tracesSampleRate: sampleRate,
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><title>Sanyue ImgHub</title><script defer="defer" src="/js/chunk-vendors.204e01e6.js"></script><script defer="defer" src="/js/app.437dccab.js"></script><link href="/css/chunk-vendors.b85f6a1a.css" rel="stylesheet"><link href="/css/app.93429def.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><title>Sanyue ImgHub</title><script defer="defer" src="/js/chunk-vendors.204e01e6.js"></script><script defer="defer" src="/js/app.99e6b921.js"></script><link href="/css/chunk-vendors.b85f6a1a.css" rel="stylesheet"><link href="/css/app.93429def.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long