Spaces:
Sleeping
Sleeping
<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> |