mirror of
https://github.com/katelya77/K-Vault.git
synced 2026-05-06 22:10:57 +08:00
1024 lines
50 KiB
HTML
1024 lines
50 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>K-Vault | 管理后台</title>
|
||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||
<link rel="apple-touch-icon" href="/logo.png">
|
||
<link rel="shortcut icon" href="/favicon.ico">
|
||
<link rel="stylesheet" href="/theme.css">
|
||
<script src="/theme.js?v=20260305"></script>
|
||
<!-- Import CSS -->
|
||
<link rel="stylesheet" href="./admin-imgtc.css">
|
||
<link rel="stylesheet" href="/mobile-refactor.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>
|
||
</head>
|
||
<body>
|
||
<div id="app" v-cloak>
|
||
<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" role="status" aria-live="polite">
|
||
<i class="fas fa-chart-line stats-icon"></i>
|
||
<span class="stats-text">总记录: {{ totalCount || Number }}</span>
|
||
<small class="stats-loaded">已加载 {{ Number }}</small>
|
||
</span>
|
||
<div class="actions">
|
||
<el-tooltip content="文件类型" placement="bottom">
|
||
<el-dropdown @command="switchFileType" :hide-on-click="false">
|
||
<span class="el-dropdown-link"><i :class="fileTypeIcon"></i></span>
|
||
<el-dropdown-menu slot="dropdown">
|
||
<el-dropdown-item command="image" :class="{ 'el-dropdown-menu__item--selected': fileType === 'image' }"><i :class="fileConfig.image.icon"></i> 图片</el-dropdown-item>
|
||
<el-dropdown-item command="video" :class="{ 'el-dropdown-menu__item--selected': fileType === 'video' }"><i :class="fileConfig.video.icon"></i> 视频</el-dropdown-item>
|
||
<el-dropdown-item command="audio" :class="{ 'el-dropdown-menu__item--selected': fileType === 'audio' }"><i :class="fileConfig.audio.icon"></i> 音频</el-dropdown-item>
|
||
<el-dropdown-item command="document" :class="{ 'el-dropdown-menu__item--selected': fileType === 'document' }"><i :class="fileConfig.document.icon"></i> 文件</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</el-dropdown>
|
||
</el-tooltip>
|
||
<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' }"><i class="fas fa-sort-amount-down"></i> 按时间倒序</el-dropdown-item>
|
||
<el-dropdown-item command="nameAsc" :class="{ 'el-dropdown-menu__item--selected': sortOption === 'nameAsc' }"><i class="fas fa-sort-alpha-up"></i> 按名称升序</el-dropdown-item>
|
||
<el-dropdown-item command="sizeDesc" :class="{ 'el-dropdown-menu__item--selected': sortOption === 'sizeDesc' }"><i class="fas fa-sort-amount-down"></i> 按大小倒序</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</el-dropdown>
|
||
</el-tooltip>
|
||
<el-tooltip content="筛选" placement="bottom">
|
||
<el-dropdown @command="filter" :hide-on-click="false">
|
||
<span class="el-dropdown-link"><i :class="filterIcon"></i></span>
|
||
<el-dropdown-menu slot="dropdown">
|
||
<el-dropdown-item command="all" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'all' }"><i class="fas fa-filter"></i> 全部</el-dropdown-item>
|
||
<el-dropdown-item command="favorites" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'favorites' }"><i class="fas fa-bookmark"></i> 收藏</el-dropdown-item>
|
||
<el-dropdown-item command="blocked" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'blocked' }"><i class="fas fa-lock"></i> 黑名单</el-dropdown-item>
|
||
<el-dropdown-item command="unblocked" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'unblocked' }"><i class="fas fa-unlock"></i> 白名单</el-dropdown-item>
|
||
<el-dropdown-item command="adult" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'adult' }"><i class="fas fa-user-secret"></i> NSFW</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</el-dropdown>
|
||
</el-tooltip>
|
||
<el-tooltip content="批量操作" placement="bottom">
|
||
<el-dropdown @command="handleBatchOperation" :hide-on-click="false">
|
||
<span class="el-dropdown-link"><i class="fas fa-tasks"></i></span>
|
||
<el-dropdown-menu slot="dropdown">
|
||
<el-dropdown-item command="copy" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-link"></i> 批量复制</el-dropdown-item>
|
||
<el-dropdown-item command="copyMarkdown" :class="{ disabled: selectedFiles.length === 0 }"><i class="fab fa-markdown"></i> 复制Markdown</el-dropdown-item>
|
||
<el-dropdown-item command="copyHtml" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-code"></i> 复制HTML</el-dropdown-item>
|
||
<el-dropdown-item command="delete" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-trash-alt"></i> 批量删除</el-dropdown-item>
|
||
<el-dropdown-item command="download" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-download"></i> 批量下载</el-dropdown-item>
|
||
<el-dropdown-item command="block" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-lock"></i> 加入黑名单</el-dropdown-item>
|
||
<el-dropdown-item command="unblock" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-unlock"></i> 加入白名单</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</el-dropdown>
|
||
</el-tooltip>
|
||
<el-tooltip content="快捷方式" placement="bottom">
|
||
<el-dropdown @command="handleWebsite">
|
||
<span class="el-dropdown-link"><i class="fas fa-link"></i></span>
|
||
<el-dropdown-menu slot="dropdown">
|
||
<template v-for="site in quickWebsites">
|
||
<el-dropdown-item :command="site.url">
|
||
<i :class="site.icon"></i> {{ site.name }}
|
||
</el-dropdown-item>
|
||
</template>
|
||
</el-dropdown-menu>
|
||
</el-dropdown>
|
||
</el-tooltip>
|
||
<el-tooltip content="工具箱" placement="bottom">
|
||
<el-dropdown @command="handleToolkit" :hide-on-click="false">
|
||
<span class="el-dropdown-link"><i class="fas fa-toolbox"></i></span>
|
||
<el-dropdown-menu slot="dropdown">
|
||
<el-dropdown-item command="selectAllInPage"><i class="fas fa-check-square"></i> 全选当前页</el-dropdown-item>
|
||
<el-dropdown-item command="checkBrokenFiles"><i class="fas fa-wrench"></i> 检测失效文件</el-dropdown-item>
|
||
<el-dropdown-item command="editWebsites"><i class="fas fa-edit"></i> 编辑快捷方式</el-dropdown-item>
|
||
<el-dropdown-item command="openUploader"><i class="fas fa-cloud-upload-alt"></i> 打开上传中心</el-dropdown-item>
|
||
<el-dropdown-item command="exportLinks"><i class="fas fa-file-export"></i> 导出全部链接</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</el-dropdown>
|
||
</el-tooltip>
|
||
<el-tooltip content="退出登录" placement="bottom"><i class="fas fa-sign-out-alt" @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">
|
||
<!-- 图片 -->
|
||
<template v-if="fileType === 'image'">
|
||
<el-card class="image-card">
|
||
<span class="collect-icon" @click.stop="toggleLike(index, item.name)">
|
||
<i :class="item.metadata.liked ? 'fa-solid fa-bookmark liked' : 'fa-regular fa-bookmark not-liked'"></i>
|
||
</span>
|
||
<el-checkbox v-model="item.selected" :ref="'checkbox-' + index"></el-checkbox>
|
||
<el-image :src="'/file/' + item.name" :preview-src-list="['/file/' + item.name]" fit="cover" lazy="true"></el-image>
|
||
<div class="image-overlay">
|
||
<div class="overlay-buttons">
|
||
<el-dropdown @command="(cmd) => handleQuickCopy(cmd, item.name)" trigger="click" size="mini">
|
||
<el-button size="mini" type="primary" @click.stop>复制 <i class="el-icon-arrow-down"></i></el-button>
|
||
<el-dropdown-menu slot="dropdown">
|
||
<el-dropdown-item command="url">直链 URL</el-dropdown-item>
|
||
<el-dropdown-item command="markdown">Markdown</el-dropdown-item>
|
||
<el-dropdown-item command="html">HTML</el-dropdown-item>
|
||
<el-dropdown-item command="bbcode">BBCode</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</el-dropdown>
|
||
<el-button size="mini" type="info" @click.stop="handleEditName(item)">编辑</el-button>
|
||
<el-button size="mini" type="danger" @click.stop="handleDelete(index, item.name)">删除</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="card-footer">
|
||
<el-popover
|
||
trigger="click"
|
||
placement="top"
|
||
popper-class="custom-popover">
|
||
<template #default>
|
||
<p v-html="formattedFileDetails(item)"></p>
|
||
</template>
|
||
<template #reference>
|
||
<span :style="{ color: item.metadata.ListType !== 'Block' ? '#fff' : '#aaa' }">
|
||
{{ item.metadata.fileName || item.name }}
|
||
</span>
|
||
</template>
|
||
</el-popover>
|
||
</div>
|
||
</el-card>
|
||
</template>
|
||
<!-- 视频 -->
|
||
<template v-else-if="fileType === 'video'">
|
||
<el-card class="video-card" :class="{ 'selected': item.selected }">
|
||
<div class="video-content">
|
||
<video :src="'/file/' + item.name" controls style="width: 100%; height: 100%; object-fit: cover;"></video>
|
||
<div class="video-title">
|
||
<el-popover
|
||
trigger="click"
|
||
placement="top"
|
||
popper-class="custom-popover">
|
||
<template #default>
|
||
<p v-html="formattedFileDetails(item)"></p>
|
||
</template>
|
||
<template #reference>
|
||
<span :style="{ color: item.metadata.ListType !== 'Block' ? '#fff' : '#aaa' }">{{ item.metadata.fileName || item.name }}</span>
|
||
</template>
|
||
</el-popover>
|
||
</div>
|
||
<!-- 控制按钮区域 -->
|
||
<div class="video-controls">
|
||
<button class="control-btn like-btn" @click.stop="toggleLike(index, item.name)"><i :class="item.metadata.liked ? 'fas fa-heart liked' : 'far fa-heart'"></i></button>
|
||
<button class="control-btn edit-btn" @click.stop="handleEditName(item)"><i class="fas fa-edit"></i></button>
|
||
<button class="control-btn select-btn" @click.stop="toggleSelect(index, item.name)"><i :class="item.selected ? 'fas fa-square-check selected' : 'far fa-square'"></i></button>
|
||
<button class="control-btn" @click.stop="handleCopy(index, item.name)"><i class="fas fa-link"></i></button>
|
||
<button class="control-btn" @click.stop="handleDelete(index, item.name)"><i class="fas fa-trash-alt"></i></button>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</template>
|
||
<!-- 音频 -->
|
||
<template v-else-if="fileType === 'audio'">
|
||
<el-card class="audio-card" :class="{ 'selected': item.selected }">
|
||
<div class="audio-content">
|
||
<!-- 音频标题区域 -->
|
||
<div class="audio-header">
|
||
<div class="audio-avatar">
|
||
<img src="./music.svg" alt="Music">
|
||
</div>
|
||
<div class="audio-info">
|
||
<div class="audio-title">
|
||
<el-popover
|
||
trigger="click"
|
||
placement="top"
|
||
popper-class="custom-popover">
|
||
<template #default>
|
||
<p v-html="formattedFileDetails(item)"></p>
|
||
</template>
|
||
<template #reference>
|
||
<span :style="{ color: item.metadata.ListType !== 'Block' ? '#fff' : '#aaa' }">{{ item.metadata.fileName || item.name }}</span>
|
||
</template>
|
||
</el-popover>
|
||
</div>
|
||
<div class="audio-subtitle">{{ getFileType(item.name) }}</div>
|
||
</div>
|
||
</div>
|
||
<audio
|
||
class="custom-audio-player"
|
||
:src="'/file/' + item.name"
|
||
controls
|
||
preload="metadata">
|
||
当前浏览器不支持音频播放
|
||
</audio>
|
||
<!-- 控制按钮区域 -->
|
||
<div class="audio-controls">
|
||
<button class="control-btn like-btn" @click.stop="toggleLike(index, item.name)"><i :class="item.metadata.liked ? 'fas fa-heart liked' : 'far fa-heart'"></i></button>
|
||
<button class="control-btn edit-btn" @click.stop="handleEditName(item)"><i class="fas fa-edit"></i></button>
|
||
<button class="control-btn select-btn" @click.stop="toggleSelect(index, item.name)"><i :class="item.selected ? 'fas fa-square-check selected' : 'far fa-square'"></i></button>
|
||
<button class="control-btn" @click.stop="handleCopy(index, item.name)"><i class="fas fa-copy"></i></button>
|
||
<button class="control-btn" @click.stop="handleDelete(index, item.name)"><i class="fas fa-trash-alt"></i></button>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</template>
|
||
<!-- 文件 -->
|
||
<template v-else>
|
||
<el-card class="file-card" :class="{ 'selected': item.selected }">
|
||
<div class="file-content">
|
||
<!-- 文件标题区域 -->
|
||
<div class="file-header">
|
||
<div class="file-avatar">
|
||
<i :class="getFileIcon(item.name)" style="font-size: 32px;"></i>
|
||
</div>
|
||
<div class="file-info">
|
||
<div class="file-title">
|
||
<el-popover
|
||
trigger="click"
|
||
placement="top"
|
||
popper-class="custom-popover">
|
||
<template #default>
|
||
<p v-html="formattedFileDetails(item)"></p>
|
||
</template>
|
||
<template #reference>
|
||
<span :style="{ color: item.metadata.ListType !== 'Block' ? '#fff' : '#aaa' }">{{ item.metadata.fileName || item.name }}</span>
|
||
</template>
|
||
</el-popover>
|
||
</div>
|
||
<div class="file-subtitle">{{ getFileType(item.name) }}</div>
|
||
</div>
|
||
</div>
|
||
<!-- 控制按钮区域 -->
|
||
<div class="file-controls">
|
||
<button class="control-btn like-btn" @click.stop="toggleLike(index, item.name)"><i :class="item.metadata.liked ? 'fas fa-heart liked' : 'far fa-heart'"></i></button>
|
||
<button class="control-btn edit-btn" @click.stop="handleEditName(item)"><i class="fas fa-edit"></i></button>
|
||
<button class="control-btn select-btn" @click.stop="toggleSelect(index, item.name)"><i :class="item.selected ? 'fas fa-square-check selected' : 'far fa-square'"></i></button>
|
||
<button class="control-btn" @click.stop="handleCopy(index, item.name)"><i class="fas fa-copy"></i></button>
|
||
<button class="control-btn" @click.stop="handleDelete(index, item.name)"><i class="fas fa-trash-alt"></i></button>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</template>
|
||
</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" />
|
||
</div>
|
||
<div style="text-align:center;margin-top:16px;">
|
||
<el-button v-if="nextCursor" :loading="isLoadingMore" :disabled="isLoadingMore" @click="loadMore">
|
||
{{ isLoadingMore ? '加载中...' : ('加载更多 (' + Number + '/' + (totalCount || Number) + ')') }}
|
||
</el-button>
|
||
<div v-else-if="(totalCount || Number) > 0 && Number >= (totalCount || Number)" class="load-more-end">
|
||
全部加载完成,共 {{ totalCount || Number }} 条
|
||
</div>
|
||
</div>
|
||
<el-footer class="footer">
|
||
<div>Powered By K-Vault</div>
|
||
<a href="https://github.com/katelya77/K-Vault" target="_blank" rel="noopener noreferrer">
|
||
<div><i class="fa-brands fa-github"></i> K-Vault</div>
|
||
</a>
|
||
</el-footer>
|
||
</el-main>
|
||
</el-container>
|
||
</div>
|
||
</body>
|
||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script> <!-- Vue -->
|
||
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/index.js"></script> <!-- ElementUI -->
|
||
<script>
|
||
new Vue({
|
||
el: '#app',
|
||
data: {
|
||
baseURL: document.location.origin,
|
||
Number: 0,
|
||
totalCount: 0,
|
||
fileConfig: {
|
||
image: {
|
||
name: '图片',
|
||
exts: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'ico', 'svg', 'heic', 'heif', 'avif'],
|
||
icon: 'fas fa-image',
|
||
count: 0
|
||
},
|
||
video: {
|
||
name: '视频',
|
||
exts: ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v', '3gp', 'ts'],
|
||
icon: 'fas fa-video',
|
||
count: 0
|
||
},
|
||
audio: {
|
||
name: '音频',
|
||
exts: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'ape', 'opus'],
|
||
icon: 'fas fa-music',
|
||
count: 0
|
||
},
|
||
document: {
|
||
name: '文件',
|
||
exts: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'zip', 'rar', '7z', 'tar', 'gz', 'exe', 'apk', 'dmg', 'iso', 'msi', 'deb', 'rpm', 'json', 'xml', 'csv', 'sql', 'html', 'css', 'js', 'py', 'java', 'go', 'rs', 'cpp', 'c', 'h'],
|
||
icon: 'fas fa-folder-open',
|
||
count: 0
|
||
}
|
||
},
|
||
uploadConfig: {
|
||
maxSize: 20 * 1024 * 1024, // 最大上传20MB
|
||
maxConcurrent: 3
|
||
},
|
||
tableData: [],
|
||
nextCursor: null,
|
||
isLoadingMore: false,
|
||
search: '',
|
||
currentPage: 1,
|
||
pageSize: 15,
|
||
selectedFiles: [],
|
||
sortOption: 'dateDesc',
|
||
filterOption: 'all',
|
||
fileType: 'image',
|
||
quickWebsites: [
|
||
{ name: '上传中心', url: './', icon: 'fas fa-cloud-upload-alt' },
|
||
{ name: '图片浏览', url: './gallery.html', icon: 'fas fa-images' },
|
||
{ name: 'Movavi', url: 'https://www.movavi.com/zh/movavi-video-converter.html', icon: 'fas fa-file-video' },
|
||
{ name: 'FreeConvert', url: 'https://www.freeconvert.com/zh/video-compressor', icon: 'fas fa-file' },
|
||
{ name: 'YouCompress', url: 'https://www.youcompress.com/zh-cn/', icon: 'fas fa-file-zipper' },
|
||
{ name: 'Cloudinary', url: 'https://console.cloudinary.com/', icon: 'fas fa-cloud' },
|
||
],
|
||
},
|
||
computed: {
|
||
filteredTableData() {
|
||
return this.tableData.filter(data => {
|
||
// 搜索匹配
|
||
const searchLower = this.search.toLowerCase();
|
||
const matchesSearch = !searchLower || [
|
||
(data.metadata.fileName || '').toLowerCase(),
|
||
data.name?.toLowerCase(),
|
||
].some(field => field?.includes(searchLower));
|
||
|
||
// 筛选匹配
|
||
const matchesFilter = {
|
||
'all': true,
|
||
'favorites': data.metadata.liked,
|
||
'blocked': data.metadata.ListType === 'Block',
|
||
'unblocked': data.metadata.ListType === 'White',
|
||
'adult': data.metadata.Label?.toLowerCase() === 'adult',
|
||
}[this.filterOption] ?? true;
|
||
|
||
// 文件类型匹配
|
||
const ext = data.name.split('.').pop().toLowerCase();
|
||
const matchesType = this.fileType === 'document' ?
|
||
!Object.keys(this.fileConfig).some(type =>
|
||
type !== 'document' && this.fileConfig[type].exts.includes(ext) // 匹配其他所有文件
|
||
) :
|
||
this.fileConfig[this.fileType].exts.includes(ext);
|
||
return matchesSearch && matchesFilter && matchesType;
|
||
});
|
||
},
|
||
paginatedTableData() {
|
||
return this.sortData(this.filteredTableData)
|
||
.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
|
||
},
|
||
sortIcon() { return `fas fa-sort-${this.sortOption === 'dateDesc' ? 'amount-down' : 'alpha-up'}`; },
|
||
filterIcon() {
|
||
return this.filterOption === 'all' ? 'fas fa-filter' :
|
||
this.filterOption === 'favorites' ? 'fas fa-bookmark' :
|
||
this.filterOption === 'blocked' ? 'fas fa-lock' :
|
||
this.filterOption === 'unblocked' ? 'fas fa-unlock' :
|
||
this.filterOption === 'adult' ? 'fas fa-user-secret' : '';
|
||
},
|
||
fileTypeIcon() {
|
||
return this.fileType === 'image' ? 'fas fa-image' :
|
||
this.fileType === 'video' ? 'fas fa-video' :
|
||
this.fileType === 'audio' ? 'fas fa-music' :
|
||
this.fileType === 'document' ? 'fas fa-folder-open' : '';
|
||
}
|
||
},
|
||
watch: { // 监听数据变化
|
||
tableData: {
|
||
handler(newData) {
|
||
this.selectedFiles = newData.filter(file => file.selected);
|
||
},
|
||
deep: true
|
||
},
|
||
sortOption(newOption) { localStorage.setItem('sortOption', newOption); },
|
||
filterOption(newOption) { localStorage.setItem('filterOption', newOption); }
|
||
},
|
||
methods: {
|
||
refreshDashboard() {location.reload();}, // 刷新页面
|
||
async handleLogout() { // 退出登录
|
||
try {
|
||
await fetch('./api/auth/logout', { method: 'POST', credentials: 'include' });
|
||
} catch (e) {}
|
||
window.location.href = './login.html';
|
||
},
|
||
handlePageChange(page) { this.currentPage = page; }, // 切换页面
|
||
normalizeListItem(file) {
|
||
return {
|
||
...file,
|
||
selected: false,
|
||
metadata: {
|
||
...file.metadata,
|
||
liked: file.metadata?.liked ?? false,
|
||
fileName: file.metadata?.fileName ?? file.name,
|
||
fileSize: file.metadata?.fileSize ?? 0,
|
||
},
|
||
};
|
||
},
|
||
mergeListData(files) {
|
||
const map = new Map(this.tableData.map((item) => [item.name, item]));
|
||
files.forEach((item) => {
|
||
if (!item || !item.name) return;
|
||
const prev = map.get(item.name);
|
||
map.set(item.name, prev ? { ...prev, ...item, selected: prev.selected || item.selected } : item);
|
||
});
|
||
this.tableData = Array.from(map.values());
|
||
},
|
||
sort(command) { this.sortOption = command; }, // 切换排序方式
|
||
filter(command) { this.filterOption = command; }, // 切换筛选方式
|
||
async loadMore() {
|
||
if (this.isLoadingMore || !this.nextCursor) return;
|
||
this.isLoadingMore = true;
|
||
try {
|
||
const result = await fetch(`./api/manage/list?cursor=${encodeURIComponent(this.nextCursor)}`, {
|
||
method: 'GET',
|
||
credentials: 'include',
|
||
}).then((r) => r.json());
|
||
|
||
const mapped = (result.keys || []).map((file) => this.normalizeListItem(file));
|
||
this.mergeListData(mapped);
|
||
this.nextCursor = result.list_complete ? null : result.cursor;
|
||
if (Number.isFinite(result?.stats?.total)) {
|
||
this.totalCount = result.stats.total;
|
||
}
|
||
this.updateStats();
|
||
} catch {
|
||
this.$message.error('加载更多失败,请稍后重试');
|
||
} finally {
|
||
this.isLoadingMore = false;
|
||
}
|
||
},
|
||
sortData(data) {
|
||
return this.sortOption === 'nameAsc' ? data.sort((a, b) => a.name.localeCompare(b.name)) :
|
||
this.sortOption === 'sizeDesc' ? data.sort((a, b) => b.metadata.fileSize - a.metadata.fileSize) :
|
||
data.sort((a, b) => b.metadata.TimeStamp - a.metadata.TimeStamp);
|
||
},
|
||
formattedFileDetails(item) {
|
||
const metadata = item.metadata;
|
||
const timestamp = new Date(metadata.TimeStamp).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||
return `
|
||
<div style="text-align: left; padding: 5px;">
|
||
<div><strong>ID:</strong>${item.name}</div>
|
||
<div><strong>文件名:</strong>${metadata.fileName || item.name}</div>
|
||
<div><strong>上传时间:</strong>${timestamp}</div>
|
||
<div><strong>元数据:</strong>${JSON.stringify(metadata)}</div>
|
||
</div>
|
||
`;
|
||
},
|
||
calculatePageSize() { // 设置页面大小
|
||
const config = {
|
||
minSize: 15, // 最小页面卡片数
|
||
cardWidth: 240,
|
||
ratio: 3/4, // 卡片高宽比
|
||
gap: 20, // 卡片间距
|
||
defaultWidth: 800,
|
||
defaultHeaderHeight: 60 // 默认Dashboard高度
|
||
};
|
||
// 获取容器尺寸
|
||
const content = document.querySelector('.content');
|
||
const header = document.querySelector('.header-content');
|
||
const width = content?.clientWidth || config.defaultWidth;
|
||
const height = window.innerHeight - (header?.offsetHeight || config.defaultHeaderHeight);
|
||
// 计算行列数
|
||
const cols = Math.floor(width / (config.cardWidth + config.gap));
|
||
const cardHeight = (width / cols - config.gap) * config.ratio;
|
||
const rows = Math.floor(height / (cardHeight + config.gap));
|
||
// 设置页面大小
|
||
this.pageSize = Math.max(rows * cols, config.minSize);
|
||
},
|
||
updateWindowWidth() { // 动态调整页面大小
|
||
this.windowWidth = window.innerWidth;
|
||
this.calculatePageSize();
|
||
},
|
||
updateStats() {
|
||
this.Number = this.tableData.length;
|
||
if (!Number.isFinite(this.totalCount) || this.totalCount < this.Number) {
|
||
this.totalCount = this.Number;
|
||
}
|
||
let fileCount = {image: 0, video: 0, audio: 0, document: 0};
|
||
this.tableData.forEach(file => {
|
||
const ext = file.name.split('.').pop().toLowerCase();
|
||
const type = Object.keys(this.fileConfig).find(t =>
|
||
this.fileConfig[t].exts.includes(ext)
|
||
) || 'document';
|
||
fileCount[type]++;
|
||
});
|
||
['image', 'video', 'audio', 'document'].forEach((type) => {
|
||
this.fileConfig[type].count = fileCount[type] || 0;
|
||
});
|
||
},
|
||
// 文件操作
|
||
async uploadFiles(event) {
|
||
const files = Array.from(event.target.files || []);
|
||
if (!files.length) return;
|
||
// 文件验证配置
|
||
// telegram-bot可分发的最大文件大小(20MB)
|
||
// 大于20MB虽然能够成功上传,但无法获取文件链接
|
||
const config = this.uploadConfig;
|
||
// 过滤有效文件
|
||
const valid = [], invalid = [];
|
||
files.forEach(file => {
|
||
(file.size <= config.maxSize ? valid : invalid).push(file);
|
||
});
|
||
// 显示错误信息
|
||
if(invalid.length) { this.$message.error(`文件超过${config.maxSize/1024/1024}MB: \n${invalid.map(f => f.name).join('\n')}`); }
|
||
if(!valid.length) { this.$message.info('没有符合条件的文件'); event.target.value = ''; return; }
|
||
|
||
try {
|
||
await this.$confirm(`确定要上传这 ${valid.length} 个文件吗?`, '提示', {
|
||
type: 'warning'
|
||
});
|
||
const loading = this.$message({ message: '上传中...', duration: 0 });
|
||
let [successCount, failed] = [0, []];
|
||
// 并发上传处理函数
|
||
const upload = async file => {
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
const res = await fetch(`${this.baseURL}/upload`, {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'include'
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok || (Array.isArray(data) && data[0]?.error)) {
|
||
const serverError = Array.isArray(data)
|
||
? data[0]?.error
|
||
: data?.error;
|
||
throw new Error(serverError || '上传失败');
|
||
}
|
||
const src = data[0]?.src;
|
||
if (!src) throw new Error('未返回文件路径');
|
||
// 验证并添加到列表
|
||
const preview = await fetch(`${this.baseURL}${src}`, {
|
||
credentials: 'include'
|
||
});
|
||
if (preview.ok) {
|
||
this.tableData.unshift({
|
||
name: src.replace(/^\/file\//, ''),
|
||
selected: false,
|
||
metadata: { TimeStamp: Date.now(), fileSize: file.size, fileName: file.name }
|
||
});
|
||
successCount++;
|
||
}
|
||
} catch (err) {
|
||
failed.push(`${file.name} (${err.message})`);
|
||
console.error('Upload error:', err);
|
||
}
|
||
};
|
||
// 分批上传
|
||
for (let i = 0; i < valid.length; i += config.maxConcurrent) {
|
||
await Promise.all(
|
||
valid.slice(i, i + config.maxConcurrent).map(upload)
|
||
);
|
||
}
|
||
loading.close();
|
||
successCount && this.$message.success(`成功上传 ${successCount} 个文件`);
|
||
failed.length && this.$message.error(`上传失败: ${failed.join(', ')}`);
|
||
this.refreshFileList();
|
||
|
||
} catch {
|
||
this.$message.info('已取消上传');
|
||
}
|
||
event.target.value = '';
|
||
},
|
||
async refreshFileList() { // 不刷新页面,仅更新数据
|
||
try {
|
||
const result = await fetch("./api/manage/list?limit=100&includeStats=1", { method: 'GET', credentials: 'include' })
|
||
.then(response => response.json());
|
||
const mapped = (result.keys || []).map((file) => this.normalizeListItem(file));
|
||
this.tableData = mapped;
|
||
this.nextCursor = result.list_complete ? null : result.cursor;
|
||
this.totalCount = Number.isFinite(result?.stats?.total) ? result.stats.total : mapped.length;
|
||
this.updateStats();
|
||
this.sortData(this.tableData);
|
||
} catch {
|
||
this.$message.error('刷新文件列表失败,请检查网络连接');
|
||
}
|
||
},
|
||
toggleSelect(index, name) {
|
||
const fileIndex = this.tableData.findIndex(file => file.name === name);
|
||
this.tableData[fileIndex].selected = !this.tableData[fileIndex].selected;
|
||
},
|
||
toggleLike(index, name) {
|
||
console.log(`Toggling like for : ${name}`);
|
||
const fileIndex = this.tableData.findIndex(file => file.name === name);
|
||
// 乐观更新收藏状态
|
||
this.tableData[fileIndex].metadata.liked = !(this.tableData[fileIndex].metadata.liked ?? false);
|
||
// 发送请求更新服务器数据
|
||
var requestOptions = { method: 'GET', redirect: 'follow', credentials: 'include' };
|
||
fetch(`./api/manage/toggleLike/${name}`, requestOptions)
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (!result.success) { // 如果服务器更新失败,将状态还原
|
||
this.tableData[fileIndex].metadata.liked = !this.tableData[fileIndex].metadata.liked;
|
||
this.$message({message: '更新收藏状态失败,请稍后重试', type: 'error'});
|
||
} else {
|
||
this.$message.success(this.tableData[fileIndex].metadata.liked ? '收藏成功' : '取消收藏');
|
||
}
|
||
})
|
||
.catch(error => { // 如果服务器响应错误,将状态还原
|
||
this.tableData[fileIndex].metadata.liked = !this.tableData[fileIndex].metadata.liked;
|
||
this.$message({message: '同步服务器失败,请检查网络连接', type: 'error'});
|
||
});
|
||
},
|
||
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('已取消删除'));
|
||
},
|
||
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);
|
||
},
|
||
handleCopy(index, key) {
|
||
const link = `${this.baseURL}/file/${key}`;
|
||
(navigator.clipboard?.writeText(link) || this.copyToClipboardFallback(link))
|
||
.then(() => this.$message.success('复制文件链接成功~'))
|
||
.catch(() => this.$message.error('自动复制失败,请手动复制链接:' + link));
|
||
},
|
||
handleQuickCopy(format, key) { // 快速复制不同格式
|
||
const url = `${this.baseURL}/file/${key}`;
|
||
const file = this.tableData.find(f => f.name === key);
|
||
const name = file?.metadata?.fileName || key;
|
||
let link = url;
|
||
|
||
switch (format) {
|
||
case 'markdown':
|
||
link = ``;
|
||
break;
|
||
case 'html':
|
||
link = `<img src="${url}" alt="${name}">`;
|
||
break;
|
||
case 'bbcode':
|
||
link = `[img]${url}[/img]`;
|
||
break;
|
||
default:
|
||
link = url;
|
||
}
|
||
|
||
(navigator.clipboard?.writeText(link) || this.copyToClipboardFallback(link))
|
||
.then(() => this.$message.success(`${format.toUpperCase()} 格式链接已复制~`))
|
||
.catch(() => this.$message.error('复制失败'));
|
||
},
|
||
// 批处理相关
|
||
selectAllInPage() { // 全选当前页
|
||
const selected = !this.paginatedTableData.every(file => file.selected);
|
||
this.paginatedTableData.forEach(file => file.selected = selected);
|
||
},
|
||
handleBatchDelete() { // 批量删除
|
||
this.$confirm('此操作将永久删除这 ' + this.selectedFiles.length + ' 个文件, 是否继续?', '提示', {
|
||
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?.writeText(links) || this.copyToClipboardFallback(links))
|
||
.then(() => this.$message.success('批量复制链接成功~'));
|
||
},
|
||
handleBatchCopyMarkdown() { // 批量复制Markdown格式
|
||
const links = this.selectedFiles.map(file => {
|
||
const url = `${document.location.origin}/file/${file.name}`;
|
||
const name = file.metadata?.fileName || file.name;
|
||
return ``;
|
||
}).join('\n');
|
||
(navigator.clipboard?.writeText(links) || this.copyToClipboardFallback(links))
|
||
.then(() => this.$message.success('Markdown格式链接已复制~'));
|
||
},
|
||
handleBatchCopyHtml() { // 批量复制HTML格式
|
||
const links = this.selectedFiles.map(file => {
|
||
const url = `${document.location.origin}/file/${file.name}`;
|
||
const name = file.metadata?.fileName || file.name;
|
||
return `<img src="${url}" alt="${name}">`;
|
||
}).join('\n');
|
||
(navigator.clipboard?.writeText(links) || this.copyToClipboardFallback(links))
|
||
.then(() => this.$message.success('HTML格式链接已复制~'));
|
||
},
|
||
handleBatchDownload() { // 批量下载
|
||
this.$message.info(`正在下载 ${this.selectedFiles.length} 个文件`, { duration: 1000 });
|
||
this.selectedFiles.forEach((file, index) => {
|
||
setTimeout(() => {
|
||
const link = document.createElement('a');
|
||
link.href = `/file/${file.name}`;
|
||
link.download = file.metadata.fileName || file.name;
|
||
link.click();
|
||
}, index * 800);
|
||
});
|
||
this.selectedFiles = [];
|
||
},
|
||
handleBatchBlockOrUnblock(type) { // 批量加入黑/白名单
|
||
if (type !== 'Block' && type !== 'White') { this.$message.error('无效的操作类型'); return; }
|
||
const typeToName = { Block: '黑名单', White: '白名单' };
|
||
this.$confirm(`确定要将这 ${this.selectedFiles.length} 个文件加入${typeToName[type]}吗?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
const promises = this.selectedFiles.map(file => fetch(`./api/manage/${type.toLowerCase()}/${file.name}`, { method: 'GET', credentials: 'include' }));
|
||
|
||
Promise.all(promises)
|
||
.then(responses => {
|
||
responses.forEach((response, index) => {
|
||
if (response.ok) {
|
||
const fileIndex = this.tableData.findIndex(item => item.name === this.selectedFiles[index].name);
|
||
if (fileIndex !== -1) {
|
||
this.tableData[fileIndex].metadata.ListType = type;
|
||
}
|
||
}
|
||
});
|
||
this.$message.success(`批量加入${typeToName[type]}成功`);
|
||
this.selectedFiles = [];
|
||
})
|
||
.catch(() => this.$message.error(`操作失败,请检查网络连接`));
|
||
});
|
||
},
|
||
handleBatchOperation(command) {
|
||
if (this.selectedFiles.length === 0) { this.$message.warning('请先选择文件'); return; }
|
||
switch (command) {
|
||
case 'copy': this.handleBatchCopy(); break;
|
||
case 'copyMarkdown': this.handleBatchCopyMarkdown(); break;
|
||
case 'copyHtml': this.handleBatchCopyHtml(); break;
|
||
case 'delete': this.handleBatchDelete(); break;
|
||
case 'download': this.handleBatchDownload(); break;
|
||
case 'block': this.handleBatchBlockOrUnblock('Block'); break;
|
||
case 'unblock': this.handleBatchBlockOrUnblock('White'); break;
|
||
}
|
||
},
|
||
// 工具相关
|
||
handleToolkit(command) {
|
||
switch (command) {
|
||
case 'selectAllInPage': this.selectAllInPage(); break;
|
||
case 'checkBrokenFiles': this.checkBrokenFiles(); break;
|
||
case 'editWebsites': this.editWebsites(); break;
|
||
case 'openUploader': this.openUploader(); break;
|
||
case 'exportLinks': this.exportAllLinks(); break;
|
||
}
|
||
},
|
||
openUploader() { // 打开上传中心
|
||
window.open('./', '_blank');
|
||
},
|
||
exportAllLinks() { // 导出全部链接
|
||
const loading = this.$message({ message: '正在生成链接列表...', duration: 0 });
|
||
const links = this.tableData.map(file => `${document.location.origin}/file/${file.name}`);
|
||
const linksText = links.join('\n');
|
||
|
||
// 创建下载文件
|
||
const blob = new Blob([linksText], { type: 'text/plain' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `image-links-${new Date().toISOString().slice(0,10)}.txt`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
|
||
loading.close();
|
||
this.$message.success(`已导出 ${links.length} 个链接`);
|
||
},
|
||
switchFileType(type) { // 切换文件类型
|
||
this.fileType = type;
|
||
this.currentPage = 1;
|
||
localStorage.setItem('fileType', type);
|
||
this.$message({
|
||
message: `已切换为${this.fileConfig[type].name}模式, 共${this.fileConfig[type].count}个文件`,
|
||
type: 'success',
|
||
duration: 1500
|
||
});
|
||
},
|
||
checkBrokenFiles() { // 检测失效文件
|
||
const loadingMessage = this.$message({ message: '正在检测失效文件...', duration: 0});
|
||
|
||
let brokenCount = 0;
|
||
const promises = this.tableData.map((item, index) => {
|
||
const fileIndex = this.tableData.findIndex(item => item.name === this.selectedFiles[index].name);
|
||
return new Promise((resolve) => {
|
||
fetch(`${this.baseURL}/file/${item.name}`, {
|
||
method: 'HEAD',
|
||
cache: 'no-cache' // 避免缓存影响检测结果
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
brokenCount++;
|
||
this.tableData[fileIndex].selected = true;
|
||
resolve({ index, status: 'error' });
|
||
} else {
|
||
resolve({ index, status: 'success' });
|
||
}
|
||
})
|
||
.catch(() => {
|
||
brokenCount++;
|
||
this.tableData[fileIndex].selected = true;
|
||
resolve({ index, status: 'error' });
|
||
});
|
||
});
|
||
});
|
||
|
||
Promise.all(promises).then(() => {
|
||
loadingMessage.close();
|
||
if (brokenCount > 0) {
|
||
this.$message({
|
||
dangerouslyUseHTMLString: true,
|
||
message: `检测到 ${brokenCount} 个失效文件,已自动选中。<br>您可以使用批量删除功能移除它们。`,
|
||
type: 'warning',
|
||
duration: 5000
|
||
});
|
||
} else {
|
||
this.$message({
|
||
message: '未检测到失效文件',
|
||
type: 'success'
|
||
});
|
||
}
|
||
});
|
||
},
|
||
// 处理网站点击
|
||
handleWebsite(url) { window.open(url, '_blank'); },
|
||
// 编辑快捷方式
|
||
editWebsites() {
|
||
const websiteText = this.quickWebsites
|
||
.map(site => `${site.name}|${site.url}|${site.icon}`)
|
||
.join('\n');
|
||
|
||
this.$prompt('', '编辑快捷方式', {
|
||
inputType: 'textarea',
|
||
inputValue: websiteText,
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
dangerouslyUseHTMLString: true,
|
||
customClass: 'website-edit-dialog',
|
||
message: `
|
||
<div>
|
||
每行一个网站,格式:名称|网址|图标类名<br>
|
||
图标需在 <a href="https://fontawesome.com/v6/search?m=free" target="_blank" style="color: #409EFF; text-decoration: underline">Font Awesome</a> 中选择
|
||
</div>
|
||
`,
|
||
inputValidator: (value) => {
|
||
const lines = value.split('\n');
|
||
if (lines.length > 10) return '不能超过10行';
|
||
for (let line of lines) {
|
||
if (!line.trim()) continue;
|
||
const [name, url, icon = 'fas fa-link'] = line.split('|');
|
||
if (!name || !url) return '名称和网址不能为空:' + line;
|
||
if (!(/^https?:\/\/.+/.test(url) || /^\.\/.*/.test(url))) return '网址格式错误:'+line;
|
||
if (!/^(fas?|fa-brands|fa-regular|fa-solid)\s+fa-[a-z0-9-]+$/.test(icon)) {
|
||
return '图标类名格式错误:' + line;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}).then(({ value }) => {
|
||
const newSites = value.split('\n')
|
||
.filter(line => line.trim())
|
||
.map(line => {
|
||
const [name, url, icon = 'fas fa-link'] = line.split('|');
|
||
return { name, url, icon };
|
||
});
|
||
this.quickWebsites = newSites;
|
||
localStorage.setItem('quickWebsites', JSON.stringify(newSites));
|
||
this.$message.success('保存成功');
|
||
}).catch(() => {});
|
||
},
|
||
getFileIcon(filename) {
|
||
const ext = filename.split('.').pop().toLowerCase();
|
||
const iconMap = {
|
||
'pdf': 'fas fa-file-pdf',
|
||
'doc,docx': 'fas fa-file-word',
|
||
'xls,xlsx,csv': 'fas fa-file-excel',
|
||
'ppt,pptx': 'fas fa-file-powerpoint',
|
||
'txt,md,log': 'fas fa-file-lines',
|
||
'zip,rar,7z,tar,gz': 'fas fa-file-zipper',
|
||
'html,htm,css,js,ts,jsx,tsx,vue,php,py,java,c,cpp,h,hpp,cs,go,rs,rb,pl,sh,sql': 'fas fa-file-code',
|
||
'json,xml,yaml,yml,toml': 'fas fa-file-code',
|
||
'mp4,avi,mov,wmv,flv,mkv,webm': 'fas fa-file-video',
|
||
'mp3,wav,ogg,flac,aac,m4a,wma': 'fas fa-file-audio',
|
||
'jpg,jpeg,png,gif,bmp,webp,svg,ico': 'fas fa-file-image',
|
||
'psd,ai,eps,cdr': 'fas fa-file-image',
|
||
'exe,msi,app,dmg,deb,rpm': 'fas fa-file-arrow-down'
|
||
};
|
||
for(const [exts, icon] of Object.entries(iconMap)) {
|
||
if(exts.split(',').includes(ext)) {
|
||
return icon;
|
||
}
|
||
}
|
||
return 'fas fa-file';
|
||
},
|
||
getFileType(filename) {
|
||
const ext = filename.split('.').pop();
|
||
return `${ext.toUpperCase()}`;
|
||
},
|
||
handleEditName(item) {
|
||
this.$prompt('', '修改文件名', {
|
||
inputValue: item.metadata?.fileName || item.name,
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
inputValidator: (value) => {
|
||
if (!value) return '文件名不能为空';
|
||
if (value.length > 64) return '文件名不能超过64个字符';
|
||
return true;
|
||
}
|
||
}).then(({ value }) => {
|
||
fetch(`./api/manage/editName/${item.name}?newName=${encodeURIComponent(value)}`, {
|
||
method: 'GET',
|
||
credentials: 'include'
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
item.metadata.fileName = value;
|
||
this.$message.success('文件名修改成功');
|
||
} else {
|
||
this.$message.error('文件名修改失败');
|
||
}
|
||
})
|
||
.catch(() => this.$message.error('修改文件名时出错,请检查网络连接'));
|
||
}).catch(() => {});
|
||
}
|
||
},
|
||
async mounted() {
|
||
// 先检查认证状态
|
||
try {
|
||
const authResponse = await fetch('./api/auth/check', { method: 'GET', credentials: 'include' });
|
||
const authData = await authResponse.json();
|
||
|
||
if (authData.authRequired && !authData.authenticated) {
|
||
window.location.href = './login.html?redirect=' + encodeURIComponent(window.location.pathname);
|
||
return;
|
||
}
|
||
this.showLogoutButton = authData.authRequired && authData.authenticated;
|
||
} catch (e) {
|
||
this.$message.error('认证检查失败,请刷新页面');
|
||
return;
|
||
}
|
||
|
||
window.addEventListener('resize', this.calculatePageSize);
|
||
this.updateWindowWidth();
|
||
|
||
// 获取文件列表数据
|
||
try {
|
||
const result = await fetch("./api/manage/list?limit=100&includeStats=1", { method: 'GET', credentials: 'include' })
|
||
.then(response => response.json());
|
||
const mapped = (result.keys || []).map((file) => this.normalizeListItem(file));
|
||
this.tableData = mapped;
|
||
this.nextCursor = result.list_complete ? null : result.cursor;
|
||
this.totalCount = Number.isFinite(result?.stats?.total) ? result.stats.total : mapped.length;
|
||
this.updateStats();
|
||
// 恢复设置
|
||
this.sortOption = localStorage.getItem('sortOption') || this.sortOption;
|
||
this.sortData(this.tableData);
|
||
this.fileType = localStorage.getItem('fileType') || this.fileType;
|
||
this.switchFileType(this.fileType);
|
||
} catch {
|
||
this.$message.error('同步数据时出错,请检查网络连接');
|
||
}
|
||
|
||
if (localStorage.getItem('quickWebsites')) {
|
||
this.quickWebsites = JSON.parse(localStorage.getItem('quickWebsites'));
|
||
}
|
||
},
|
||
beforeDestroy() {
|
||
window.removeEventListener('resize', this.calculatePageSize);
|
||
}
|
||
});
|
||
</script>
|
||
</html>
|