CloudVault / templates /index.html
lexlepty's picture
Upload index.html
776c698 verified
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>云存储</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js"></script>
<style>
/* 基础样式变量 */
:root {
--primary-glow: #ff9580;
--secondary-glow: #ffd700;
--background: #ffffff;
--text: #333333;
--sidebar-bg: #f8f9fa;
--card-bg: #ffffff;
--border-color: #e0e0e0;
--shadow-color: rgba(0, 0, 0, 0.1);
--sidebar-width: 240px;
--header-height: 70px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--background);
color: var(--text);
min-height: 100vh;
}
/* 布局样式 */
.container {
display: flex;
min-height: 100vh;
}
/* 侧边栏样式 */
.sidebar {
width: var(--sidebar-width);
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
padding: 20px;
position: fixed;
height: 100vh;
overflow-y: auto;
transition: all 0.3s ease;
}
.logo {
padding: 20px 15px;
margin-bottom: 30px;
font-size: 24px;
font-weight: bold;
color: var(--primary-glow);
}
.nav-item {
display: flex;
align-items: center;
padding: 15px;
margin: 8px 0;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
background: var(--card-bg);
border: 1px solid transparent;
}
.nav-item:hover {
border-color: var(--primary-glow);
box-shadow: 0 0 15px rgba(255, 149, 128, 0.2);
transform: translateX(5px);
}
.nav-item.active {
background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
color: white;
}
.nav-item i {
margin-right: 12px;
font-size: 20px;
}
/* 主内容区样式 */
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
padding: calc(var(--header-height) + 20px) 30px 30px;
background: var(--background);
}
/* 头部搜索栏样式 */
.header {
position: fixed;
top: 0;
left: var(--sidebar-width);
right: 0;
height: var(--header-height);
background: var(--card-bg);
padding: 15px 30px;
display: flex;
align-items: center;
box-shadow: 0 2px 10px var(--shadow-color);
z-index: 100;
}
.search-container {
flex: 1;
max-width: 600px;
margin: 0 20px;
position: relative;
}
.search-box {
width: 100%;
padding: 12px 20px;
border-radius: 25px;
border: 2px solid var(--border-color);
background: var(--background);
font-size: 16px;
transition: all 0.3s ease;
}
.search-box:focus {
outline: none;
border-color: var(--primary-glow);
box-shadow: 0 0 10px rgba(255, 149, 128, 0.3);
}
/* 视图切换按钮样式 */
.view-toggle {
position: absolute;
right: 30px;
top: calc(var(--header-height) + 20px);
display: flex;
gap: 10px;
z-index: 10;
}
.view-btn {
padding: 8px 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--card-bg);
cursor: pointer;
transition: all 0.3s ease;
}
.view-btn.active {
background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
color: white;
border-color: transparent;
}
/* 文件显示样式 */
.file-container {
margin-top: 60px;
}
/* 网格视图样式 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
padding: 20px 0;
}
.file-item.grid {
background: var(--card-bg);
border-radius: 15px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.file-item.grid:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px var(--shadow-color);
border-color: var(--primary-glow);
}
.file-item.grid::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary-glow), var(--secondary-glow));
opacity: 0;
transition: opacity 0.3s ease;
}
.file-item.grid:hover::before {
opacity: 1;
}
/* 列表视图样式 */
.file-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.file-item.list {
display: flex;
align-items: center;
padding: 15px;
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--border-color);
transition: all 0.3s ease;
}
.file-item.list:hover {
transform: translateX(5px);
border-color: var(--primary-glow);
box-shadow: 0 5px 15px var(--shadow-color);
}
.file-item.list .file-icon {
font-size: 24px;
margin-right: 15px;
}
.file-item.list .file-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-item.list .file-name {
font-weight: 500;
}
.file-item.list .file-meta {
display: flex;
gap: 20px;
color: #666;
}
/* 文件图标和信息样式 */
.file-icon {
font-size: 48px;
margin-bottom: 15px;
color: var(--primary-glow);
}
.file-name {
font-size: 14px;
margin-bottom: 8px;
word-break: break-word;
}
.file-size {
font-size: 12px;
color: #666;
}
/* 文件操作菜单 */
.file-menu {
position: absolute;
background: var(--card-bg);
border-radius: 8px;
box-shadow: 0 5px 20px var(--shadow-color);
padding: 8px 0;
z-index: 1000;
}
.file-menu-item {
padding: 8px 20px;
cursor: pointer;
transition: background 0.3s ease;
white-space: nowrap;
}
.file-menu-item:hover {
background: var(--sidebar-bg);
}
/* 上传按钮和进度条 */
.upload-btn {
position: fixed;
right: 30px;
bottom: 30px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 15px rgba(255, 149, 128, 0.4);
transition: all 0.3s ease;
z-index: 1000;
}
.upload-btn:hover {
transform: scale(1.1);
}
.upload-progress {
position: fixed;
bottom: 30px;
right: 100px;
background: var(--card-bg);
padding: 15px;
border-radius: 12px;
box-shadow: 0 5px 20px var(--shadow-color);
display: none;
}
.progress-bar {
width: 200px;
height: 6px;
background: var(--border-color);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-glow), var(--secondary-glow));
width: 0%;
transition: width 0.3s ease;
}
/* 移动端适配 */
@media (max-width: 768px) {
.sidebar {
width: 100%;
height: 60px;
padding: 0 10px;
bottom: 0;
display: flex;
align-items: center;
justify-content: space-around;
z-index: 1000;
background: var(--sidebar-bg);
box-shadow: 0 -2px 10px var(--shadow-color);
}
.logo {
display: none;
}
.nav-item {
flex: 1;
margin: 0 5px;
padding: 8px 15px;
flex-direction: row;
align-items: center;
font-size: 12px;
height: 40px;
border-radius: 8px;
}
.nav-item i {
margin: 0 8px 0 0;
font-size: 16px;
}
.nav-item-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.main-content {
margin-left: 0;
margin-bottom: 70px;
padding-top: 90px;
padding-bottom: 70px;
min-height: calc(100vh - 70px);
}
.header {
left: 0;
z-index: 999;
}
.search-container {
margin: 0;
}
.upload-btn {
right: 20px;
bottom: 80px;
z-index: 1001;
}
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.view-toggle {
right: 20px;
gap: 8px;
}
.upload-progress {
bottom: 70px;
right: 20px;
max-width: calc(100vw - 40px);
z-index: 1001;
}
}
.action-buttons {
position: absolute;
right: 30px;
top: calc(var(--header-height) + 20px);
display: flex;
gap: 16px;
align-items: center;
z-index: 10;
}
.action-btn {
padding: 8px 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--card-bg);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
color: var(--text);
}
.action-btn:hover {
border-color: var(--primary-glow);
box-shadow: 0 2px 8px var(--shadow-color);
}
.action-btn i {
font-size: 16px;
color: var(--primary-glow);
}
@media (max-width: 768px) {
.action-buttons {
right: 20px;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
font-size: 12px;
}
.action-btn i {
font-size: 14px;
}
}
/* 拖拽上传区域样式 */
.drag-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 149, 128, 0.1);
border: 3px dashed var(--primary-glow);
z-index: 2000;
display: none;
align-items: center;
justify-content: center;
font-size: 24px;
color: var(--primary-glow);
}
/* 面包屑导航 */
.breadcrumb {
margin: 20px 0;
padding: 12px 16px;
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 14px;
background: var(--card-bg);
border-radius: 8px;
box-shadow: 0 2px 8px var(--shadow-color);
width: auto;
min-width: min-content;
}
.breadcrumb-item {
cursor: pointer;
color: var(--text);
transition: all 0.3s ease;
padding: 4px 8px;
border-radius: 4px;
display: inline-flex; /* Changed from flex to inline-flex */
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.breadcrumb-item:hover {
color: var(--primary-glow);
background: rgba(255, 149, 128, 0.1);
}
.breadcrumb-separator {
color: var(--border-color);
margin: 0 4px;
user-select: none;
}
@media (max-width: 768px) {
.breadcrumb {
padding: 8px 12px;
margin: 12px 0;
font-size: 12px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
}
.breadcrumb::-webkit-scrollbar {
display: none;
}
.breadcrumb-item {
padding: 4px 6px;
max-width: 150px;
}
}
/* 加载指示器样式 */
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
background: var(--card-bg);
border-radius: 15px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-color);
border-top-color: var(--primary-glow);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
100% { transform: rotate(360deg); }
}
.loading-text {
color: var(--text);
font-size: 1rem;
margin-top: 1rem;
}
/* 上传进度样式 */
.upload-progress {
width: 400px;
max-width: 90vw;
}
.progress-item {
background: var(--card-bg);
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.5rem;
box-shadow: 0 2px 8px var(--shadow-color);
}
.file-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.filename {
font-weight: 500;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cancel-upload {
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
padding: 0.25rem 0.75rem;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.3s ease;
}
.cancel-upload:hover {
background: #ff6666;
transform: translateY(-1px);
}
.upload-stats {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
color: #666;
margin-top: 0.5rem;
}
.progress-bar {
width: 100%;
height: 6px;
background: var(--border-color);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-glow), var(--secondary-glow));
width: 0%;
transition: width 0.3s ease;
}
/* 确认对话框样式 */
.confirm-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 3000;
}
.confirm-content {
background: var(--card-bg);
border-radius: 12px;
padding: 24px;
max-width: 400px;
width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.confirm-content h3 {
margin-bottom: 16px;
color: var(--text);
}
.confirm-content p {
margin-bottom: 24px;
color: #666;
line-height: 1.5;
}
.confirm-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.confirm-buttons button {
padding: 8px 20px;
border-radius: 6px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.confirm-cancel {
background: #f0f0f0;
color: #666;
}
.confirm-ok {
background: #ff4444;
color: white;
}
.confirm-buttons button:hover {
transform: translateY(-1px);
}
/* 提示消息样式 */
.toast-message {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 6px;
font-size: 14px;
opacity: 0;
transition: all 0.3s ease;
}
.toast-message.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.preview-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: none;
z-index: 2000;
}
.preview-content {
max-width: 90%;
max-height: 90%;
position: relative;
background: #fff;
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.preview-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-header {
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-body {
flex: 1;
overflow: auto;
padding: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.preview-image-container {
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
}
.text-preview,
.markdown-preview {
background: white;
padding: 20px;
overflow: auto;
font-size: 14px;
line-height: 1.6;
}
.markdown-preview {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.preview-action-btn {
padding: 8px;
margin-left: 8px;
border: none;
background: none;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
}
.preview-action-btn:hover {
color: #000;
background: #e9ecef;
border-radius: 4px;
}
.preview-close {
position: absolute;
top: 20px;
right: 20px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.cancel-download {
padding: 4px 8px;
border: none;
background: #ff4444;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
}
.cancel-download:hover {
background: #ff6666;
transform: translateY(-1px);
}
.stats-row {
display: flex;
justify-content: space-between;
margin-top: 4px;
}
.download-stats {
font-size: 12px;
color: #666;
margin-top: 8px;
}
.file-item.selectable {
position: relative;
cursor: pointer;
}
.file-item.selectable::before {
content: '';
position: absolute;
top: 10px;
left: 10px;
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-radius: 4px;
background: white;
z-index: 1;
}
.file-item.selected::before {
background: var(--primary-glow);
border-color: var(--primary-glow);
}
.file-item.selected::after {
content: '\f00c';
font-family: 'Font Awesome 6 Free';
font-weight: 900;
position: absolute;
top: 10px;
left: 10px;
width: 20px;
height: 20px;
color: white;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.multi-select-btn.active {
background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
color: white;
border-color: transparent;
}
.batch-operations {
display: flex;
gap: 8px;
margin-left: 16px;
}
.folder-name-input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
margin: 16px 0;
font-size: 14px;
}
.folder-name-input:focus {
outline: none;
border-color: var(--primary-glow);
box-shadow: 0 0 0 2px rgba(255, 149, 128, 0.2);
}
.multi-select-btn.active {
background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
color: white;
border-color: transparent;
}
.logout-btn {
padding: 8px 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--card-bg);
color: var(--text);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
margin-right: 10px;
margin-left: 10px;
}
.logout-btn:hover {
border-color: var(--primary-glow);
color: var(--primary-glow);
transform: translateY(-2px);
}
.logout-btn i {
font-size: 16px;
}
</style>
</head>
<body>
<div class="container">
<!-- 侧边栏 -->
<nav class="sidebar">
<div class="logo">
<i class="fas fa-cloud"></i> Cloud Vault
</div>
<div class="nav-item active" data-type="all">
<i class="fas fa-folder"></i>
<span class="nav-item-text">全部文件</span>
</div>
<div class="nav-item" data-type="image">
<i class="fas fa-image"></i>
<span class="nav-item-text">图片</span>
</div>
<div class="nav-item" data-type="video">
<i class="fas fa-video"></i>
<span class="nav-item-text">视频</span>
</div>
<div class="nav-item" data-type="document">
<i class="fas fa-file-alt"></i>
<span class="nav-item-text">文档</span>
</div>
<div class="nav-item" data-type="audio">
<i class="fas fa-music"></i>
<span class="nav-item-text">音频</span>
</div>
<div class="nav-item" data-type="archive">
<i class="fas fa-file-archive"></i>
<span class="nav-item-text">压缩包</span>
</div>
</nav>
<!-- 顶部搜索栏 -->
<header class="header">
<div class="search-container">
<input type="text" class="search-box" placeholder="搜索文件...">
</div>
<!-- 添加退出登录按钮 -->
<button class="logout-btn" onclick="handleLogout()">
<i class="fas fa-sign-out-alt"></i>
退出登录
</button>
</header>
<!-- 主内容区 -->
<main class="main-content">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<span class="breadcrumb-item" data-path="/">根目录</span>
</div>
<button class="action-btn create-folder-btn">
<i class="fas fa-folder-plus"></i>
<span>新建文件夹</span>
</button>
<!-- 视图切换按钮 -->
<div class="view-toggle">
<button class="view-btn active" data-view="grid">
<i class="fas fa-th"></i>
</button>
<button class="view-btn" data-view="list">
<i class="fas fa-list"></i>
</button>
</div>
<!-- 文件容器 -->
<div class="file-container">
<!-- 文件内容将通过 JavaScript 动态生成 -->
</div>
</main>
<!-- 上传按钮 -->
<div class="upload-btn" id="uploadBtn">
<i class="fas fa-plus"></i>
<input type="file" id="fileInput" style="display: none;" multiple>
</div>
<!-- 上传进度条 -->
<div class="upload-progress">
</div>
</div>
<!-- 拖拽上传遮罩 -->
<div class="drag-overlay">
<div>释放鼠标上传文件</div>
</div>
<!-- 文件操作菜单 -->
<div class="file-menu" style="display: none;">
<div class="file-menu-item" data-action="preview">
<i class="fas fa-eye"></i> 预览
</div>
<div class="file-menu-item" data-action="download">
<i class="fas fa-download"></i> 下载
</div>
<div class="file-menu-item" data-action="delete">
<i class="fas fa-trash"></i> 删除
</div>
</div>
<!-- 预览模态框 -->
<div class="preview-modal">
<div class="preview-container">
<div class="preview-content">
<!-- 预览内容将通过 JavaScript 动态生成 -->
</div>
<button class="preview-close">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<script>
// 文件管理类
class FileManager {
constructor() {
this.selectedFiles = new Set();
this.isMultiSelectMode = false;
this.currentPath = '/';
this.currentView = 'grid';
this.currentFileType = 'all';
this.files = [];
this.initEventListeners();
this.loadFiles();
}
// 初始化事件监听
initEventListeners() {
// 视图切换
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', () => this.switchView(btn.dataset.view));
});
// 文件类型筛选
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => this.filterByType(item.dataset.type));
});
// 搜索
const searchBox = document.querySelector('.search-box');
searchBox.addEventListener('input', this.debounce((e) => this.handleSearch(e.target.value), 300));
// 文件上传
const uploadBtn = document.getElementById('uploadBtn');
const fileInput = document.getElementById('fileInput');
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => this.handleFileUpload(e.target.files));
// 拖拽上传
this.initDragAndDrop();
// 新建文件夹按钮监听
const createFolderBtn = document.querySelector('.create-folder-btn');
createFolderBtn.addEventListener('click', () => this.showCreateFolderDialog());
// 添加多选按钮
const multiSelectBtn = document.createElement('button');
multiSelectBtn.className = 'action-btn multi-select-btn';
multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>多选</span>';
multiSelectBtn.addEventListener('click', () => this.toggleMultiSelectMode());
document.querySelector('.view-toggle').prepend(multiSelectBtn);
}
// 加载文件列表
async loadFiles() {
try {
const path = this.currentPath === '/' ? '' : this.currentPath;
const response = await fetch(`/api/files/list/${path}`);
if (!response.ok) throw new Error('Failed to load files');
this.files = await response.json();
this.renderFiles();
this.updateBreadcrumb();
} catch (error) {
console.error('Error loading files:', error);
this.showError('加载文件失败');
}
}
// 渲染文件列表
renderFiles() {
const container = document.querySelector('.file-container');
container.innerHTML = '';
const viewClass = this.currentView === 'grid' ? 'file-grid' : 'file-list';
container.className = `file-container ${viewClass}`;
let filteredFiles = this.files;
if (this.currentFileType !== 'all') {
filteredFiles = this.files.filter(file => file.file_type === this.currentFileType);
}
filteredFiles.forEach(file => {
const fileElement = this.createFileElement(file);
container.appendChild(fileElement);
});
}
// 创建文件元素
createFileElement(file) {
const element = document.createElement('div');
element.className = `file-item ${this.currentView}`;
// 添加多选模式相关的类
if (this.isMultiSelectMode) {
element.classList.add('selectable');
if (this.selectedFiles.has(file)) {
element.classList.add('selected');
}
}
const icon = this.getFileIcon(file.type, file.file_type);
const size = this.formatFileSize(file.size);
if (this.currentView === 'grid') {
element.innerHTML = `
<i class="${icon} file-icon"></i>
<div class="file-name">${file.path.split('/').pop()}</div>
<div class="file-size">${size}</div>
`;
} else {
element.innerHTML = `
<i class="${icon} file-icon"></i>
<div class="file-info">
<div class="file-name">${file.path.split('/').pop()}</div>
<div class="file-meta">
<span>${size}</span>
<span>${file.file_type || '未知类型'}</span>
</div>
</div>
`;
}
// 事件处理逻辑
if (this.isMultiSelectMode) {
// 多选模式下的点击处理
element.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (file.type === 'directory') {
// 文件夹仍然保持导航功能
this.currentPath = file.path;
this.loadFiles();
} else {
// 文件切换选中状态
if (this.selectedFiles.has(file)) {
this.selectedFiles.delete(file);
element.classList.remove('selected');
} else {
this.selectedFiles.add(file);
element.classList.add('selected');
}
}
});
} else {
// 普通模式下的点击处理
element.addEventListener('click', () => this.handleFileClick(file));
}
// 右键菜单处理
element.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showFileMenu(e, file);
});
return element;
}
// 处理文件点击
handleFileClick(file) {
if (file.type === 'directory') {
this.currentPath = file.path;
this.loadFiles();
} else {
this.previewFile(file);
}
}
// 显示文件操作菜单
showFileMenu(e, file) {
e.preventDefault();
const menu = document.querySelector('.file-menu');
menu.style.display = 'block';
menu.style.left = `${e.pageX}px`;
menu.style.top = `${e.pageY}px`;
// 清除旧的事件监听
const menuItems = menu.querySelectorAll('.file-menu-item');
menuItems.forEach(item => {
const clone = item.cloneNode(true);
item.parentNode.replaceChild(clone, item);
});
// 添加新的事件监听
menu.querySelector('[data-action="preview"]').addEventListener('click', () => this.previewFile(file));
menu.querySelector('[data-action="download"]').addEventListener('click', () => this.downloadFile(file));
menu.querySelector('[data-action="delete"]').addEventListener('click', () => this.deleteFile(file));
// 点击其他地方关闭菜单
const closeMenu = () => {
menu.style.display = 'none';
document.removeEventListener('click', closeMenu);
};
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
}
// 文件预览
async previewFile(file) {
try {
const modal = document.querySelector('.preview-modal');
const content = modal.querySelector('.preview-content');
// 计算合适的预览尺寸
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const maxWidth = Math.min(windowWidth * 0.9, 1200); // 最大宽度不超过1200px
const maxHeight = windowHeight * 0.85;
modal.style.display = 'flex';
content.innerHTML = `
<div class="loading-indicator">
<div class="spinner"></div>
<div class="loading-text">正在加载预览...</div>
</div>
`;
const response = await fetch(`/api/files/preview/${file.path}`);
if (!response.ok) throw new Error('Failed to preview file');
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const mimeType = response.headers.get('content-type') || '';
const fileName = file.path.split('/').pop();
// 获取预览内容
const previewContent = `
<div class="preview-header">
<div class="preview-info">
<i class="${this.getFileIcon(file.type, file.file_type)}"></i>
<span>${fileName}</span>
</div>
<div class="preview-actions">
<button class="preview-action-btn zoom-in">
<i class="fas fa-search-plus"></i>
</button>
<button class="preview-action-btn zoom-out">
<i class="fas fa-search-minus"></i>
</button>
<button class="preview-action-btn download">
<i class="fas fa-download"></i>
</button>
</div>
</div>
<div class="preview-body" style="max-width: ${maxWidth}px; max-height: ${maxHeight}px;">
${await this.getPreviewContent(file, url, mimeType, maxWidth, maxHeight)}
</div>
`;
content.innerHTML = previewContent;
// 绑定事件处理
this.bindPreviewEvents(modal, content, file, url);
} catch (error) {
console.error('Error previewing file:', error);
this.showError('预览文件失败');
}
}
async getPreviewContent(file, url, mimeType, maxWidth, maxHeight) {
const extension = file.path.split('.').pop().toLowerCase();
if (file.file_type === 'image' || mimeType.startsWith('image/')) {
return `
<div class="preview-image-container" style="max-width: ${maxWidth}px; max-height: ${maxHeight}px;">
<img src="${url}" alt="${file.path}" class="preview-image">
</div>
`;
}
if (file.file_type === 'video' || mimeType.startsWith('video/')) {
return `
<div class="video-container" style="max-width: ${maxWidth * 0.8}px;">
<video class="plyr-media" controls crossorigin playsinline>
<source src="${url}" type="${mimeType}">
</video>
</div>
`;
}
if (file.file_type === 'audio' || mimeType.startsWith('audio/')) {
return `
<div class="audio-container" style="width: ${maxWidth * 0.6}px;">
<audio class="plyr-media" controls>
<source src="${url}" type="${mimeType}">
</audio>
</div>
`;
}
if (mimeType.includes('pdf')) {
return `
<iframe src="${url}#view=FitH" type="application/pdf"
style="width: ${maxWidth}px; height: ${maxHeight}px; border: none;">
</iframe>
`;
}
// 支持 Markdown 预览
if (extension === 'md') {
const text = await (await fetch(url)).text();
const marked = window.marked; // 确保已引入 marked 库
const htmlContent = marked ? marked(text) : text;
return `
<div class="markdown-preview" style="width: ${maxWidth * 0.8}px; height: ${maxHeight * 0.8}px;">
${htmlContent}
</div>
`;
}
// 支持 HTML 预览
if (extension === 'html' || mimeType.includes('html')) {
return `
<iframe src="${url}" sandbox="allow-same-origin allow-scripts"
style="width: ${maxWidth}px; height: ${maxHeight}px; border: none;">
</iframe>
`;
}
if (mimeType.includes('text/') || mimeType.includes('application/json')) {
const text = await (await fetch(url)).text();
return `
<div class="text-preview" style="width: ${maxWidth * 0.8}px; height: ${maxHeight * 0.8}px;">
<pre><code>${this.escapeHtml(text)}</code></pre>
</div>
`;
}
return `
<div class="unsupported-preview">
<i class="fas fa-exclamation-circle"></i>
<p>此文件类型暂不支持预览</p>
<button class="download-btn">
<i class="fas fa-download"></i> 下载文件
</button>
</div>
`;
}
bindPreviewEvents(modal, content, file, url) {
// 缩放功能
let currentScale = 1;
const zoomStep = 0.1;
const maxScale = 3;
const minScale = 0.5;
const zoomIn = content.querySelector('.zoom-in');
const zoomOut = content.querySelector('.zoom-out');
const previewImage = content.querySelector('.preview-image');
const downloadBtn = content.querySelector('.preview-action-btn.download');
if (zoomIn && zoomOut && previewImage) {
zoomIn.onclick = () => {
if (currentScale < maxScale) {
currentScale += zoomStep;
previewImage.style.transform = `scale(${currentScale})`;
}
};
zoomOut.onclick = () => {
if (currentScale > minScale) {
currentScale -= zoomStep;
previewImage.style.transform = `scale(${currentScale})`;
}
};
}
// 下载功能
if (downloadBtn) {
downloadBtn.onclick = () => this.downloadFile(file);
}
// 初始化视频播放器
if (file.file_type === 'video' || file.file_type === 'audio') {
const playerElement = content.querySelector('.plyr-media');
if (playerElement && window.Plyr) {
new Plyr(playerElement);
}
}
// 关闭预览
const closeBtn = modal.querySelector('.preview-close');
const closePreview = () => {
URL.revokeObjectURL(url);
modal.style.display = 'none';
const players = document.querySelectorAll('.plyr');
players.forEach(player => {
if (player.plyr) {
player.plyr.destroy();
}
});
};
closeBtn.onclick = closePreview;
}
// 文件下载
async downloadFile(file) {
try {
const uploadProgress = document.querySelector('.upload-progress');
const progressItem = document.createElement('div');
progressItem.className = 'progress-item';
progressItem.innerHTML = ''; // 清除之前的进度条
progressItem.innerHTML = `
<div class="file-info">
<span class="filename">${file.path.split('/').pop()}</span>
<button class="cancel-download">
<i class="fas fa-times"></i> 取消
</button>
</div>
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<div class="download-stats">
<div class="stats-row">
<span class="progress-text">0%</span>
<span class="downloaded-size">0 MB / 0 MB</span>
</div>
<div class="stats-row">
<span class="speed">等待开始...</span>
</div>
</div>
`;
uploadProgress.style.display = 'block';
uploadProgress.appendChild(progressItem);
const progressFill = progressItem.querySelector('.progress-fill');
const progressText = progressItem.querySelector('.progress-text');
const speedElement = progressItem.querySelector('.speed');
const sizeElement = progressItem.querySelector('.downloaded-size');
const cancelButton = progressItem.querySelector('.cancel-download');
const controller = new AbortController();
let isDownloadCancelled = false;
cancelButton.onclick = () => {
controller.abort();
isDownloadCancelled = true;
progressItem.remove();
if (!uploadProgress.hasChildNodes()) {
uploadProgress.style.display = 'none';
}
};
const response = await fetch(`/api/files/download/${file.path}`, {
signal: controller.signal
});
if (!response.ok) throw new Error('Download failed');
const contentLength = response.headers.get('content-length');
const total = parseInt(contentLength, 10);
const reader = response.body.getReader();
let receivedLength = 0;
let lastTime = Date.now();
let lastReceived = 0;
let currentSpeed = 0;
let lastSpeedUpdate = Date.now();
const chunks = [];
while (true) {
const {done, value} = await reader.read();
if (done || isDownloadCancelled) break;
chunks.push(value);
receivedLength += value.length;
const percent = (receivedLength / total) * 100;
progressFill.style.width = `${percent.toFixed(1)}%`;
progressText.textContent = `${percent.toFixed(1)}%`;
const now = Date.now();
if (now - lastSpeedUpdate >= 1000) {
const timeElapsed = (now - lastTime) / 1000;
const receivedSinceLastTime = receivedLength - lastReceived;
if (timeElapsed > 0) {
currentSpeed = receivedSinceLastTime / timeElapsed;
if (currentSpeed > 0) {
speedElement.textContent = `${this.formatFileSize(currentSpeed)}/s`;
}
}
lastTime = now;
lastReceived = receivedLength;
lastSpeedUpdate = now;
}
sizeElement.textContent = `${this.formatFileSize(receivedLength)} / ${this.formatFileSize(total)}`;
}
if (!isDownloadCancelled) {
const blob = new Blob(chunks);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.path.split('/').pop();
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
progressItem.remove();
if (!uploadProgress.hasChildNodes()) {
uploadProgress.style.display = 'none';
}
}
} catch (error) {
const uploadProgress = document.querySelector('.upload-progress');
if (error.name === 'AbortError') {
this.showMessage('下载已取消');
} else {
console.error('Error downloading file:', error);
this.showError('下载文件失败');
}
if (!uploadProgress.hasChildNodes()) {
uploadProgress.style.display = 'none';
}
}
}
// 文件上传处理
async handleFileUpload(files) {
const uploadProgress = document.querySelector('.upload-progress');
uploadProgress.style.display = 'block';
uploadProgress.innerHTML = ''; // 清除之前的进度条
let hasSuccessfulUpload = false; // 跟踪是否有文件上传成功
for (const file of files) {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('path', this.currentPath);
const xhr = new XMLHttpRequest();
const startTime = Date.now();
let lastLoaded = 0;
let lastTime = startTime;
// 创建进度条元素
const progressItem = document.createElement('div');
progressItem.className = 'progress-item';
progressItem.innerHTML = `
<div class="file-info">
<span class="filename">${file.name}</span>
<button class="cancel-upload">取消</button>
</div>
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<div class="upload-stats">
<span class="speed">0 KB/s</span>
<span class="time-remaining">计算中...</span>
</div>
`;
uploadProgress.appendChild(progressItem);
const progressFill = progressItem.querySelector('.progress-fill');
const speedElement = progressItem.querySelector('.speed');
const timeElement = progressItem.querySelector('.time-remaining');
const cancelButton = progressItem.querySelector('.cancel-upload');
// 处理取消上传
cancelButton.addEventListener('click', () => {
xhr.abort();
progressItem.remove();
if (uploadProgress.children.length === 0) {
uploadProgress.style.display = 'none';
}
});
// 处理上传进度
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressFill.style.width = `${percent}%`;
// 计算上传速度
const currentTime = Date.now();
const timeElapsed = (currentTime - lastTime) / 1000; // 秒
const loaded = e.loaded - lastLoaded;
const speed = loaded / timeElapsed; // 字节每秒
// 计算剩余时间
const remaining = (e.total - e.loaded) / speed;
const minutes = Math.floor(remaining / 60);
const seconds = Math.floor(remaining % 60);
// 更新UI
speedElement.textContent = `${this.formatFileSize(speed)}/s`;
timeElement.textContent = `预计剩余 ${minutes}${seconds}秒`;
lastLoaded = e.loaded;
lastTime = currentTime;
}
});
// 执行上传请求
await new Promise((resolve, reject) => {
xhr.onload = async () => {
try {
const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
if (xhr.status === 200 && response.success) {
this.showMessage(`文件 ${file.name} 上传成功`);
hasSuccessfulUpload = true; // 标记上传成功
resolve();
} else {
const errorMessage = response.error || '上传失败';
reject(new Error(errorMessage));
}
} catch (e) {
reject(new Error('服务器响应格式错误'));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.onabort = () => reject(new Error('Upload cancelled'));
xhr.open('POST', '/api/files/upload');
xhr.send(formData);
});
// 上传完成后移除进度条
progressItem.remove();
if (uploadProgress.children.length === 0) {
uploadProgress.style.display = 'none';
}
} catch (error) {
if (error.message !== 'Upload cancelled') {
this.showError(`上传文件 ${file.name} 失败`);
}
}
}
// 所有上传完成后,如果有文件上传成功则刷新文件列表
if (hasSuccessfulUpload) {
await this.loadFiles();
}
}
// 拖拽上传初始化
initDragAndDrop() {
const dragOverlay = document.querySelector('.drag-overlay');
const container = document.querySelector('.container');
container.addEventListener('dragover', (e) => {
e.preventDefault();
dragOverlay.style.display = 'flex';
});
container.addEventListener('dragleave', (e) => {
if (e.relatedTarget === null) {
dragOverlay.style.display = 'none';
}
});
container.addEventListener('drop', (e) => {
e.preventDefault();
dragOverlay.style.display = 'none';
if (e.dataTransfer.files.length > 0) {
this.handleFileUpload(e.dataTransfer.files);
}
});
}
// 面包屑导航更新
updateBreadcrumb() {
const breadcrumb = document.querySelector('.breadcrumb');
const paths = this.currentPath.split('/').filter(Boolean);
breadcrumb.innerHTML = '<span class="breadcrumb-item" data-path="/">根目录</span>';
let currentPath = '';
paths.forEach(path => {
currentPath += `/${path}`;
breadcrumb.innerHTML += `
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item" data-path="${currentPath}">${decodeURIComponent(path)}</span>
`;
});
breadcrumb.querySelectorAll('.breadcrumb-item').forEach(item => {
item.addEventListener('click', () => {
this.currentPath = item.dataset.path;
this.loadFiles();
});
});
}
// 视图切换
switchView(view) {
const buttons = document.querySelectorAll('.view-btn');
buttons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.view === view);
});
this.currentView = view;
this.renderFiles();
}
// 文件类型筛选
filterByType(type) {
const items = document.querySelectorAll('.nav-item');
items.forEach(item => {
item.classList.toggle('active', item.dataset.type === type);
});
this.currentFileType = type;
this.renderFiles();
}
// 搜索处理
async handleSearch(keyword) {
if (!keyword) {
await this.loadFiles();
return;
}
try {
const response = await fetch(`/api/files/search?keyword=${encodeURIComponent(keyword)}`);
if (!response.ok) throw new Error('Search failed');
const searchResults = await response.json();
// Transform the MySQL search results to match the file list format
this.files = searchResults.map(file => ({
type: 'file',
path: file.path,
size: parseInt(file.size), // Convert size string to number
file_type: file.type,
size_formatted: file.size,
preview_url: `/api/files/preview/${file.path}`,
download_url: `/api/files/download/${file.path}`,
created_at: file.created_at
}));
// Update the breadcrumb to show we're in search mode
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb.innerHTML = `
<span class="breadcrumb-item" data-path="/">根目录</span>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item">搜索结果: "${keyword}"</span>
`;
this.renderFiles();
// Show result count
this.showMessage(`找到 ${this.files.length} 个匹配的文件`);
} catch (error) {
console.error('Error searching files:', error);
this.showError('搜索失败');
}
}
// 辅助方法
getFileIcon(type, fileType) {
const icons = {
directory: 'fas fa-folder',
image: 'fas fa-file-image',
video: 'fas fa-file-video',
document: 'fas fa-file-alt',
audio: 'fas fa-file-audio',
archive: 'fas fa-file-archive',
code: 'fas fa-file-code',
other: 'fas fa-file'
};
if (type === 'directory') return icons.directory;
return icons[fileType] || icons.other;
}
formatFileSize(bytes) {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
showError(message) {
// 可以根据需要实现错误提示UI
alert(message);
}
async deleteFile(file) {
try {
const confirmed = await this.showConfirmDialog(
'确认删除',
`确定要删除文件 "${file.path.split('/').pop()}" 吗?此操作不可恢复。`
);
if (!confirmed) return;
const response = await fetch(`/api/files/delete/${encodeURIComponent(file.path)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '删除失败');
}
// Only proceed with refresh and success message if deletion was successful
await this.loadFiles();
this.showMessage(`文件 "${file.path.split('/').pop()}" 已成功删除`);
} catch (error) {
console.error('Error deleting file:', error);
this.showError('删除文件失败');
}
}
// 添加确认对话框的实现
showConfirmDialog(title, message) {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'confirm-modal';
modal.innerHTML = `
<div class="confirm-content">
<h3>${title}</h3>
<p>${message}</p>
<div class="confirm-buttons">
<button class="confirm-cancel">取消</button>
<button class="confirm-ok">确定</button>
</div>
</div>
`;
document.body.appendChild(modal);
const handleConfirm = (confirmed) => {
modal.remove();
resolve(confirmed);
};
modal.querySelector('.confirm-cancel').addEventListener('click', () => handleConfirm(false));
modal.querySelector('.confirm-ok').addEventListener('click', () => handleConfirm(true));
});
}
// 添加提示消息的实现
showMessage(message) {
const toast = document.createElement('div');
toast.className = 'toast-message';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 2000);
}, 100);
}
// 添加多选模式切换按钮
addMultiSelectButton() {
const multiSelectBtn = document.createElement('button');
multiSelectBtn.className = 'multi-select-btn';
multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i> 多选';
multiSelectBtn.onclick = () => this.toggleMultiSelectMode();
document.querySelector('.view-toggle').appendChild(multiSelectBtn);
}
// 切换多选模式
toggleMultiSelectMode() {
this.isMultiSelectMode = !this.isMultiSelectMode;
this.selectedFiles.clear();
// 更新按钮状态
const multiSelectBtn = document.querySelector('.multi-select-btn');
multiSelectBtn.classList.toggle('active');
// 更新按钮文本
if (this.isMultiSelectMode) {
// 显示批量操作按钮
this.showBatchOperations();
multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>退出多选</span>';
} else {
// 隐藏批量操作按钮
this.hideBatchOperations();
multiSelectBtn.innerHTML = '<i class="fas fa-check-square"></i><span>多选</span>';
}
this.renderFiles();
}
showBatchOperations() {
const batchOpsContainer = document.createElement('div');
batchOpsContainer.className = 'batch-operations';
batchOpsContainer.innerHTML = `
<button class="action-btn batch-download-btn">
<i class="fas fa-download"></i><span>批量下载</span>
</button>
<button class="action-btn batch-delete-btn">
<i class="fas fa-trash"></i><span>批量删除</span>
</button>
`;
document.querySelector('.view-toggle').appendChild(batchOpsContainer);
// 绑定事件
batchOpsContainer.querySelector('.batch-download-btn').onclick = () => this.batchDownload();
batchOpsContainer.querySelector('.batch-delete-btn').onclick = () => this.batchDelete();
}
hideBatchOperations() {
const batchOps = document.querySelector('.batch-operations');
if (batchOps) {
batchOps.remove();
}
}
// 批量下载
async batchDownload() {
for (const file of this.selectedFiles) {
await this.downloadFile(file);
}
}
// 批量删除
async batchDelete() {
const confirmed = await this.showConfirmDialog(
'批量删除',
`确定要删除选中的 ${this.selectedFiles.size} 个文件吗?此操作不可恢复。`
);
if (confirmed) {
for (const file of this.selectedFiles) {
await this.deleteFile(file);
}
}
}
async createFolder(folderName) {
try {
const response = await fetch('/api/files/create_folder', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: this.currentPath,
name: folderName
})
});
if (!response.ok) {
throw new Error('Failed to create folder');
}
await this.loadFiles();
this.showMessage('文件夹创建成功');
} catch (error) {
console.error('Error creating folder:', error);
this.showError('创建文件夹失败');
}
}
// 显示创建文件夹对话框
async showCreateFolderDialog() {
const modal = document.createElement('div');
modal.className = 'confirm-modal';
modal.innerHTML = `
<div class="confirm-content">
<h3>新建文件夹</h3>
<div class="input-container">
<input type="text"
class="folder-name-input"
placeholder="请输入文件夹名称"
maxlength="255">
</div>
<div class="confirm-buttons">
<button class="confirm-cancel">取消</button>
<button class="confirm-ok">创建</button>
</div>
</div>
`;
document.body.appendChild(modal);
const input = modal.querySelector('.folder-name-input');
input.focus();
try {
const folderName = await new Promise((resolve) => {
const handleCreateFolder = () => {
const name = input.value.trim();
if (name) {
resolve(name);
}
modal.remove();
};
const handleCancel = () => {
resolve(null);
modal.remove();
};
modal.querySelector('.confirm-ok').onclick = handleCreateFolder;
modal.querySelector('.confirm-cancel').onclick = handleCancel;
input.onkeyup = (e) => {
if (e.key === 'Enter') handleCreateFolder();
if (e.key === 'Escape') handleCancel();
};
});
if (folderName) {
await this.createFolder(folderName);
}
} catch (error) {
console.error('Error creating folder:', error);
this.showError('创建文件夹失败');
}
}
}
async function handleLogout() {
try {
const response = await fetch('/logout');
if (response.ok) {
window.location.href = '/login';
} else {
throw new Error('Logout failed');
}
} catch (error) {
console.error('Error during logout:', error);
alert('退出登录失败,请重试');
}
}
// 初始化文件管理器
new FileManager();
</script>
</body>
</html>