beta:管理端重构

This commit is contained in:
MarSeventh
2024-09-12 23:01:43 +08:00
parent 21a8a83f4d
commit eecb696ade
28 changed files with 64 additions and 497 deletions

View File

@@ -344,6 +344,7 @@ API格式
8. ~~支持URL粘贴上传2024.8.23已完成)~~
9. ~~支持大于5MB的图片上传前自动压缩2024.8.26已完成)~~
10. 支持自定义压缩(上传前+存储端)
11. 重构管理端,认证+显示效果优化
### 4.2Fix Bugs👻

View File

@@ -34,7 +34,7 @@
position: absolute;
right: 0px;
" v-if="showLogoutButton">
<a href="./admin.html">
<a href="./admin">
<el-button
size="mini"
type="warning"

View File

@@ -1,492 +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/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">
<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;
}
.video-preview {
width: 100%;
height: 200px;
display: block;
}
</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-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>
<!-- 如果文件类型是视频,使用 <video> 标签显示视频预览 -->
<video v-if="item.metadata?.FileType?.includes('video')"
:src="'/file/' + item.name"
autoplay
muted
loop
class="video-preview">
你的浏览器不支持视频播放
</video>
<!-- 如果文件类型不是视频,显示图片预览 -->
<el-image
v-else
: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>
</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 => {
if (response.ok) {
const fileIndex = this.tableData.findIndex(file => file.name === key);
if (fileIndex !== -1) {
this.tableData.splice(fileIndex, 1);
}
} else {
return 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 = '/';
},
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);
}
},
mounted() {
fetch("./api/manage/check", { method: 'GET', credentials: 'include' })
.then(response => response.text())
.then(result => {
if(result == "true"){
this.showLogoutButton=true;
// 在 check 成功后再执行 list 的 fetch 请求
return fetch("./api/manage/list", { method: 'GET', credentials: 'include' });
} else if(result=="Not using basic auth."){
return fetch("./api/manage/list", { method: 'GET', credentials: 'include' });
}
else{
window.location.href="./api/manage/login";
return Promise.reject();
}
})
.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
css/124.c3895f5a.css Normal file

File diff suppressed because one or more lines are too long

BIN
css/124.c3895f5a.css.gz Normal file

Binary file not shown.

1
css/51.2e8181d4.css Normal file

File diff suppressed because one or more lines are too long

BIN
css/51.2e8181d4.css.gz Normal file

Binary file not shown.

1
css/740.06c6d424.css Normal file
View File

@@ -0,0 +1 @@
.login[data-v-7d05a9bf]{display:flex;justify-content:center;align-items:center;height:100vh;background:linear-gradient(90deg,#ffd7e4,#c8f1ff)}.login-container[data-v-7d05a9bf]{display:flex;flex-direction:column;justify-content:space-around;align-items:center;height:40vh;width:40vw;border-radius:12px;box-shadow:0 0 12px rgba(0,0,0,.12);background-color:hsla(0,0%,100%,.6);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:all .3s ease}.login-container[data-v-7d05a9bf]:hover{box-shadow:0 0 12px 4px rgba(0,0,0,.24);transform:translateY(-5px)}.input-container[data-v-7d05a9bf]{display:flex;align-items:center;width:35vw}.input-name[data-v-7d05a9bf]{width:12vw}.submit[data-v-7d05a9bf]{margin-top:10px}

BIN
css/740.06c6d424.css.gz Normal file

Binary file not shown.

View File

@@ -81,9 +81,7 @@ async function errorHandling(context) {
}else{
if (context.request.headers.has('Authorization')) {
// Throws exception when authorization fails.
const { user, pass } = basicAuthentication(context.request);
const { user, pass } = basicAuthentication(context.request);
if (context.env.BASIC_USER !== user || context.env.BASIC_PASS !== pass) {
return UnauthorizedException('Invalid credentials.');
}else{
@@ -96,6 +94,7 @@ async function errorHandling(context) {
headers: {
// Prompts the user for credentials.
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
// 'WWW-Authenticate': 'None',
},
});
}

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/app.3e48b5e4.js"></script><link href="/css/app.def7ef9b.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/app.a8680267.js"></script><link href="/css/app.def7ef9b.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>

Binary file not shown.

2
js/124.c2ac0f68.js Normal file

File diff suppressed because one or more lines are too long

BIN
js/124.c2ac0f68.js.gz Normal file

Binary file not shown.

1
js/124.c2ac0f68.js.map Normal file

File diff suppressed because one or more lines are too long

BIN
js/124.c2ac0f68.js.map.gz Normal file

Binary file not shown.

2
js/51.94814ec2.js Normal file

File diff suppressed because one or more lines are too long

BIN
js/51.94814ec2.js.gz Normal file

Binary file not shown.

1
js/51.94814ec2.js.map Normal file

File diff suppressed because one or more lines are too long

BIN
js/51.94814ec2.js.map.gz Normal file

Binary file not shown.

2
js/740.2caf32ef.js Normal file
View File

@@ -0,0 +1,2 @@
"use strict";(self["webpackChunksanyue_imghub"]=self["webpackChunksanyue_imghub"]||[]).push([[740],{2740:function(e,s,a){a.r(s),a.d(s,{default:function(){return k}});var n=a(47),t=(a(5331),a(9648),a(2105)),o=(a(9092),a(6768)),r=a(5130);const i={class:"login"},l={class:"login-container"},u={class:"input-container"},c={class:"input-container"};function d(e,s,a,d,m,p){const h=t.WK,k=n.S2;return(0,o.uX)(),(0,o.CE)("div",i,[(0,o.Lk)("div",l,[s[5]||(s[5]=(0,o.Lk)("h1",null,"AdminLogin",-1)),(0,o.Lk)("div",u,[s[2]||(s[2]=(0,o.Lk)("a",{class:"input-name"},"用户名:",-1)),(0,o.bF)(h,{modelValue:m.username,"onUpdate:modelValue":s[0]||(s[0]=e=>m.username=e),placeholder:"请输入用户名"},null,8,["modelValue"])]),(0,o.Lk)("div",c,[s[3]||(s[3]=(0,o.Lk)("a",{class:"input-name"},"密码:",-1)),(0,o.bF)(h,{modelValue:m.password,"onUpdate:modelValue":s[1]||(s[1]=e=>m.password=e),placeholder:"请输入密码",type:"password","show-password":"",onKeyup:(0,r.jR)(p.login,["enter","native"])},null,8,["modelValue","onKeyup"])]),(0,o.bF)(k,{class:"submit",type:"primary",onClick:p.login},{default:(0,o.k6)((()=>s[4]||(s[4]=[(0,o.eW)("登录")]))),_:1},8,["onClick"])])])}a(4114),a(4979);var m={data(){return{password:"",username:""}},methods:{async login(){const e=btoa(`${this.username}:${this.password}`);try{const s=await fetch("/api/manage/check",{method:"GET",headers:{Authorization:`Basic ${e}`},credentials:"include"});401===s.status?this.$message.error("用户名或密码错误"):200===s.status?(this.$store.commit("setCredentials",e),this.$router.push("/admin")):this.$message.error("用户名或密码错误")}catch(s){this.$message.error("服务器错误")}}}},p=a(1241);const h=(0,p.A)(m,[["render",d],["__scopeId","data-v-7d05a9bf"]]);var k=h}}]);
//# sourceMappingURL=740.2caf32ef.js.map

BIN
js/740.2caf32ef.js.gz Normal file

Binary file not shown.

1
js/740.2caf32ef.js.map Normal file

File diff suppressed because one or more lines are too long

BIN
js/740.2caf32ef.js.map.gz Normal file

Binary file not shown.

46
js/app.a8680267.js Normal file

File diff suppressed because one or more lines are too long

BIN
js/app.a8680267.js.gz Normal file

Binary file not shown.

1
js/app.a8680267.js.map Normal file

File diff suppressed because one or more lines are too long

BIN
js/app.a8680267.js.map.gz Normal file

Binary file not shown.