<!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>