990 lines
34 KiB
HTML
990 lines
34 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>文件管理器</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background-color: #f5f5f5;
|
||
color: #333;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.header {
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.toolbar {
|
||
background: white;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.file-list {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 20px;
|
||
border-bottom: 1px solid #eee;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.file-item:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.file-item.selected {
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.file-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
margin-right: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.file-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.file-name {
|
||
font-weight: 500;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.file-meta {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.file-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background-color: #1976d2;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background-color: #1565c0;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background-color: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background-color: #5a6268;
|
||
}
|
||
|
||
.btn-danger {
|
||
background-color: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background-color: #c82333;
|
||
}
|
||
|
||
.btn-success {
|
||
background-color: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background-color: #218838;
|
||
}
|
||
|
||
.btn-info {
|
||
background-color: #17a2b8;
|
||
color: white;
|
||
}
|
||
|
||
.btn-info:hover {
|
||
background-color: #138496;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
margin: 15% auto;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-group input {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 15px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.breadcrumb-item {
|
||
cursor: pointer;
|
||
color: #1976d2;
|
||
}
|
||
|
||
.breadcrumb-item:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 2px dashed #ddd;
|
||
border-radius: 8px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
border-color: #1976d2;
|
||
}
|
||
|
||
.upload-area.dragover {
|
||
border-color: #1976d2;
|
||
background-color: #f0f8ff;
|
||
}
|
||
|
||
.search-box {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
}
|
||
|
||
.search-box input {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
padding: 10px;
|
||
}
|
||
|
||
.toolbar {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.search-box {
|
||
min-width: auto;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>📁 项目文件管理器</h1>
|
||
<p>管理您的项目文件,支持上传、下载、删除等操作</p>
|
||
</div>
|
||
|
||
<div class="toolbar">
|
||
<div class="search-box">
|
||
<input type="text" id="searchInput" placeholder="搜索文件..." />
|
||
</div>
|
||
<button class="btn btn-primary" onclick="showUploadModal()">📤 上传文件</button>
|
||
<button class="btn btn-success" onclick="showCreateFolderModal()">📁 新建文件夹</button>
|
||
<button class="btn btn-secondary" onclick="refreshFileList()">🔄 刷新</button>
|
||
<button class="btn btn-info" onclick="toggleBatchMode()" id="batchModeBtn">📦 批量操作</button>
|
||
<button class="btn btn-primary" onclick="selectAll()" id="selectAllBtn" style="display: none;">☑️ 全选</button>
|
||
<button class="btn btn-primary" onclick="downloadSelected()" id="downloadSelectedBtn" style="display: none;">📥 下载选中</button>
|
||
</div>
|
||
|
||
<div class="breadcrumb" id="breadcrumb">
|
||
<span class="breadcrumb-item" onclick="navigateToPath('')">🏠 根目录</span>
|
||
</div>
|
||
|
||
<div class="file-list" id="fileList">
|
||
<!-- 文件列表将在这里动态生成 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上传文件模态框 -->
|
||
<div id="uploadModal" class="modal">
|
||
<div class="modal-content">
|
||
<h3>上传文件</h3>
|
||
<div class="upload-area" id="uploadArea">
|
||
<p>📁 拖拽文件到这里或点击选择文件</p>
|
||
<input type="file" id="fileInput" multiple style="display: none;" />
|
||
</div>
|
||
<div style="margin-top: 15px; text-align: right;">
|
||
<button class="btn btn-secondary" onclick="closeModal('uploadModal')">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新建文件夹模态框 -->
|
||
<div id="createFolderModal" class="modal">
|
||
<div class="modal-content">
|
||
<h3>新建文件夹</h3>
|
||
<div class="form-group">
|
||
<label>文件夹名称:</label>
|
||
<input type="text" id="folderNameInput" placeholder="输入文件夹名称" />
|
||
</div>
|
||
<div style="text-align: right;">
|
||
<button class="btn btn-secondary" onclick="closeModal('createFolderModal')">取消</button>
|
||
<button class="btn btn-primary" onclick="createFolder()">创建</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 重命名模态框 -->
|
||
<div id="renameModal" class="modal">
|
||
<div class="modal-content">
|
||
<h3>重命名</h3>
|
||
<div class="form-group">
|
||
<label>新名称:</label>
|
||
<input type="text" id="renameInput" placeholder="输入新名称" />
|
||
</div>
|
||
<div style="text-align: right;">
|
||
<button class="btn btn-secondary" onclick="closeModal('renameModal')">取消</button>
|
||
<button class="btn btn-primary" onclick="renameItem()">重命名</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API_BASE = `${window.location.protocol}//${window.location.host}/api/v1/files`;
|
||
let currentPath = '';
|
||
let selectedItems = new Set();
|
||
let currentItemToRename = '';
|
||
let batchMode = false;
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadFileList();
|
||
setupEventListeners();
|
||
});
|
||
|
||
function setupEventListeners() {
|
||
// 搜索功能
|
||
document.getElementById('searchInput').addEventListener('input', function(e) {
|
||
if (e.target.value.trim()) {
|
||
searchFiles(e.target.value.trim());
|
||
} else {
|
||
loadFileList();
|
||
}
|
||
});
|
||
|
||
// 文件上传
|
||
const uploadArea = document.getElementById('uploadArea');
|
||
const fileInput = document.getElementById('fileInput');
|
||
|
||
uploadArea.addEventListener('click', () => fileInput.click());
|
||
fileInput.addEventListener('change', handleFileSelect);
|
||
|
||
// 拖拽上传
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
handleFiles(e.dataTransfer.files);
|
||
});
|
||
|
||
// 模态框点击外部关闭
|
||
document.querySelectorAll('.modal').forEach(modal => {
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
async function loadFileList(path = '') {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/list?path=${encodeURIComponent(path)}&recursive=false`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
currentPath = path;
|
||
renderFileList(data.items);
|
||
updateBreadcrumb(path);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载文件列表失败:', error);
|
||
showMessage('加载文件列表失败', 'error');
|
||
}
|
||
}
|
||
|
||
function renderFileList(items) {
|
||
const fileList = document.getElementById('fileList');
|
||
fileList.innerHTML = '';
|
||
|
||
// 返回上级目录
|
||
if (currentPath) {
|
||
const parentItem = createFileItem({
|
||
name: '..',
|
||
type: 'directory',
|
||
path: getParentPath(currentPath),
|
||
modified: Date.now() / 1000
|
||
});
|
||
fileList.appendChild(parentItem);
|
||
}
|
||
|
||
// 渲染文件和文件夹
|
||
items.sort((a, b) => {
|
||
if (a.type !== b.type) {
|
||
return a.type === 'directory' ? -1 : 1;
|
||
}
|
||
return a.name.localeCompare(b.name);
|
||
}).forEach(item => {
|
||
const fileItem = createFileItem(item);
|
||
fileList.appendChild(fileItem);
|
||
});
|
||
}
|
||
|
||
function createFileItem(item) {
|
||
const div = document.createElement('div');
|
||
div.className = 'file-item';
|
||
div.dataset.path = item.path;
|
||
|
||
const icon = item.type === 'directory' ? '📁' : getFileIcon(item.name);
|
||
const size = item.type === 'file' ? formatFileSize(item.size) : '-';
|
||
const date = new Date(item.modified * 1000).toLocaleDateString();
|
||
const isSelected = selectedItems.has(item.path);
|
||
|
||
// 构建复选框HTML
|
||
const checkboxHtml = batchMode ?
|
||
`<input type="checkbox" ${isSelected ? 'checked' : ''} onchange="toggleItemSelection('${item.path}')" style="margin-right: 12px;">` : '';
|
||
|
||
div.innerHTML = `
|
||
${checkboxHtml}
|
||
<div class="file-icon">${icon}</div>
|
||
<div class="file-info">
|
||
<div class="file-name">${item.name}</div>
|
||
<div class="file-meta">${size} • ${date}</div>
|
||
</div>
|
||
<div class="file-actions">
|
||
${item.type === 'file' ?
|
||
`<button class="btn btn-primary" onclick="downloadFile('${item.path}')">下载</button>` :
|
||
`<button class="btn btn-primary" onclick="downloadFolderAsZip('${item.path}')">下载ZIP</button>`
|
||
}
|
||
<button class="btn btn-secondary" onclick="showRenameModal('${item.path}')">重命名</button>
|
||
<button class="btn btn-danger" onclick="deleteItem('${item.path}')">删除</button>
|
||
</div>
|
||
`;
|
||
|
||
// 添加批量模式下的点击事件
|
||
div.addEventListener('click', (e) => {
|
||
if (e.target.closest('.file-actions') || e.target.type === 'checkbox') return;
|
||
|
||
if (batchMode) {
|
||
toggleItemSelection(item.path);
|
||
// 更新复选框状态
|
||
const checkbox = div.querySelector('input[type="checkbox"]');
|
||
if (checkbox) {
|
||
checkbox.checked = selectedItems.has(item.path);
|
||
}
|
||
} else {
|
||
if (item.type === 'directory') {
|
||
loadFileList(item.path);
|
||
} else {
|
||
window.open(`${API_BASE}/download/${item.path}`, '_blank');
|
||
}
|
||
}
|
||
});
|
||
|
||
// 添加选中状态的样式
|
||
if (isSelected) {
|
||
div.classList.add('selected');
|
||
}
|
||
|
||
return div;
|
||
}
|
||
|
||
function getFileIcon(fileName) {
|
||
const ext = fileName.split('.').pop().toLowerCase();
|
||
const iconMap = {
|
||
'md': '📄',
|
||
'txt': '📝',
|
||
'pdf': '📕',
|
||
'doc': '📘',
|
||
'docx': '📘',
|
||
'xls': '📗',
|
||
'xlsx': '📗',
|
||
'ppt': '📙',
|
||
'pptx': '📙',
|
||
'jpg': '🖼️',
|
||
'jpeg': '🖼️',
|
||
'png': '🖼️',
|
||
'gif': '🖼️',
|
||
'zip': '📦',
|
||
'rar': '📦',
|
||
'mp4': '🎬',
|
||
'mp3': '🎵',
|
||
'json': '📋',
|
||
'xml': '📋',
|
||
'html': '🌐',
|
||
'css': '🎨',
|
||
'js': '⚡',
|
||
'py': '🐍'
|
||
};
|
||
|
||
return iconMap[ext] || '📄';
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function updateBreadcrumb(path) {
|
||
const breadcrumb = document.getElementById('breadcrumb');
|
||
breadcrumb.innerHTML = '<span class="breadcrumb-item" onclick="navigateToPath(\'\')">🏠 根目录</span>';
|
||
|
||
if (path) {
|
||
const parts = path.split('/');
|
||
let currentPath = '';
|
||
|
||
parts.forEach((part, index) => {
|
||
currentPath += (currentPath ? '/' : '') + part;
|
||
const isLast = index === parts.length - 1;
|
||
|
||
if (!isLast) {
|
||
breadcrumb.innerHTML += ` <span>›</span> <span class="breadcrumb-item" onclick="navigateToPath('${currentPath}')">${part}</span>`;
|
||
} else {
|
||
breadcrumb.innerHTML += ` <span>›</span> <span>${part}</span>`;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function navigateToPath(path) {
|
||
loadFileList(path);
|
||
}
|
||
|
||
function getParentPath(path) {
|
||
const parts = path.split('/');
|
||
return parts.slice(0, -1).join('/');
|
||
}
|
||
|
||
async function searchFiles(query) {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/search?query=${encodeURIComponent(query)}&path=${encodeURIComponent(currentPath)}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
renderFileList(data.results.map(item => ({
|
||
...item,
|
||
modified: Date.now() / 1000,
|
||
size: 0
|
||
})));
|
||
}
|
||
} catch (error) {
|
||
console.error('搜索失败:', error);
|
||
showMessage('搜索失败', 'error');
|
||
}
|
||
}
|
||
|
||
function handleFileSelect(e) {
|
||
handleFiles(e.target.files);
|
||
}
|
||
|
||
async function handleFiles(files) {
|
||
for (let file of files) {
|
||
await uploadFile(file);
|
||
}
|
||
closeModal('uploadModal');
|
||
loadFileList();
|
||
}
|
||
|
||
async function uploadFile(file) {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('path', currentPath);
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/upload`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showMessage(`文件 ${file.name} 上传成功`, 'success');
|
||
} else {
|
||
showMessage(`文件 ${file.name} 上传失败`, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('上传失败:', error);
|
||
showMessage(`文件 ${file.name} 上传失败`, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteItem(path) {
|
||
if (!confirm(`确定要删除 ${path.split('/').pop()} 吗?`)) return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/delete?path=${encodeURIComponent(path)}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showMessage('删除成功', 'success');
|
||
loadFileList();
|
||
} else {
|
||
showMessage('删除失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除失败:', error);
|
||
showMessage('删除失败', 'error');
|
||
}
|
||
}
|
||
|
||
async function createFolder() {
|
||
const folderName = document.getElementById('folderNameInput').value.trim();
|
||
if (!folderName) {
|
||
showMessage('请输入文件夹名称', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/create-folder`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
path: currentPath,
|
||
name: folderName
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showMessage('文件夹创建成功', 'success');
|
||
closeModal('createFolderModal');
|
||
loadFileList();
|
||
} else {
|
||
showMessage('文件夹创建失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('创建文件夹失败:', error);
|
||
showMessage('创建文件夹失败', 'error');
|
||
}
|
||
}
|
||
|
||
async function renameItem() {
|
||
const newName = document.getElementById('renameInput').value.trim();
|
||
if (!newName) {
|
||
showMessage('请输入新名称', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/rename`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
old_path: currentItemToRename,
|
||
new_name: newName
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showMessage('重命名成功', 'success');
|
||
closeModal('renameModal');
|
||
loadFileList();
|
||
} else {
|
||
showMessage('重命名失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('重命名失败:', error);
|
||
showMessage('重命名失败', 'error');
|
||
}
|
||
}
|
||
|
||
function downloadFile(path) {
|
||
window.open(`${API_BASE}/download/${path}`, '_blank');
|
||
}
|
||
|
||
async function downloadFolderAsZip(folderPath) {
|
||
const folderName = folderPath.split('/').pop();
|
||
const downloadButton = event.target;
|
||
|
||
// 禁用按钮,显示加载状态
|
||
const originalText = downloadButton.textContent;
|
||
downloadButton.disabled = true;
|
||
downloadButton.textContent = '压缩中...';
|
||
|
||
try {
|
||
// 显示压缩开始提示
|
||
showMessage(`正在压缩文件夹 "${folderName}"...`, 'info');
|
||
|
||
// 使用新的下载文件夹ZIP接口
|
||
const response = await fetch(`${API_BASE}/download-folder-zip`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ path: folderPath })
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 获取文件名
|
||
const contentDisposition = response.headers.get('content-disposition');
|
||
let fileName = `${folderName}.zip`;
|
||
if (contentDisposition) {
|
||
const fileNameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
|
||
if (fileNameMatch) {
|
||
fileName = fileNameMatch[1];
|
||
}
|
||
}
|
||
|
||
// 检查文件大小
|
||
const contentLength = response.headers.get('content-length');
|
||
const fileSize = contentLength ? parseInt(contentLength) : 0;
|
||
|
||
if (fileSize > 100 * 1024 * 1024) { // 大于100MB显示警告
|
||
showMessage(`文件较大 (${formatFileSize(fileSize)}),下载可能需要一些时间...`, 'warning');
|
||
}
|
||
|
||
downloadButton.textContent = '下载中...';
|
||
|
||
// 创建下载链接
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = fileName;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
showMessage(`文件夹 "${folderName}" 压缩下载成功`, 'success');
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.detail || `服务器错误: ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('下载文件夹失败:', error);
|
||
showMessage(`下载文件夹 "${folderName}" 失败: ${error.message}`, 'error');
|
||
} finally {
|
||
// 恢复按钮状态
|
||
downloadButton.disabled = false;
|
||
downloadButton.textContent = originalText;
|
||
}
|
||
}
|
||
|
||
function refreshFileList() {
|
||
loadFileList(currentPath);
|
||
}
|
||
|
||
function toggleBatchMode() {
|
||
batchMode = !batchMode;
|
||
const batchBtn = document.getElementById('batchModeBtn');
|
||
const downloadBtn = document.getElementById('downloadSelectedBtn');
|
||
const selectAllBtn = document.getElementById('selectAllBtn');
|
||
|
||
if (batchMode) {
|
||
batchBtn.textContent = '❌ 退出批量';
|
||
batchBtn.classList.remove('btn-info');
|
||
batchBtn.classList.add('btn-secondary');
|
||
downloadBtn.style.display = 'inline-block';
|
||
selectAllBtn.style.display = 'inline-block';
|
||
selectedItems.clear();
|
||
} else {
|
||
batchBtn.textContent = '📦 批量操作';
|
||
batchBtn.classList.remove('btn-secondary');
|
||
batchBtn.classList.add('btn-info');
|
||
downloadBtn.style.display = 'none';
|
||
selectAllBtn.style.display = 'none';
|
||
selectedItems.clear();
|
||
}
|
||
|
||
refreshFileList(); // 重新渲染以显示/隐藏复选框
|
||
}
|
||
|
||
function selectAll() {
|
||
const allItems = document.querySelectorAll('.file-item[data-path]');
|
||
const allPaths = Array.from(allItems).map(item => item.dataset.path);
|
||
|
||
if (selectedItems.size === allPaths.length) {
|
||
// 如果已全选,则取消全选
|
||
selectedItems.clear();
|
||
document.getElementById('selectAllBtn').textContent = '☑️ 全选';
|
||
} else {
|
||
// 全选
|
||
selectedItems.clear();
|
||
allPaths.forEach(path => selectedItems.add(path));
|
||
document.getElementById('selectAllBtn').textContent = '❌ 取消全选';
|
||
}
|
||
|
||
// 更新所有复选框和选中状态
|
||
allItems.forEach(item => {
|
||
const path = item.dataset.path;
|
||
const checkbox = item.querySelector('input[type="checkbox"]');
|
||
if (checkbox) {
|
||
checkbox.checked = selectedItems.has(path);
|
||
}
|
||
if (selectedItems.has(path)) {
|
||
item.classList.add('selected');
|
||
} else {
|
||
item.classList.remove('selected');
|
||
}
|
||
});
|
||
|
||
updateBatchUI();
|
||
}
|
||
|
||
function toggleItemSelection(path) {
|
||
if (selectedItems.has(path)) {
|
||
selectedItems.delete(path);
|
||
} else {
|
||
selectedItems.add(path);
|
||
}
|
||
updateBatchUI();
|
||
}
|
||
|
||
function updateBatchUI() {
|
||
const downloadBtn = document.getElementById('downloadSelectedBtn');
|
||
const count = selectedItems.size;
|
||
|
||
if (count > 0) {
|
||
downloadBtn.textContent = `📥 下载选中 (${count})`;
|
||
downloadBtn.classList.remove('btn-primary');
|
||
downloadBtn.classList.add('btn-success');
|
||
} else {
|
||
downloadBtn.textContent = '📥 下载选中';
|
||
downloadBtn.classList.remove('btn-success');
|
||
downloadBtn.classList.add('btn-primary');
|
||
}
|
||
}
|
||
|
||
async function downloadSelected() {
|
||
if (selectedItems.size === 0) {
|
||
showMessage('请先选择要下载的文件', 'warning');
|
||
return;
|
||
}
|
||
|
||
const items = Array.from(selectedItems);
|
||
const hasFolders = items.some(path => {
|
||
const fullPath = PROJECTS_DIR / path;
|
||
return fullPath.isDirectory();
|
||
});
|
||
|
||
if (hasFolders) {
|
||
showMessage('检测到文件夹,正在创建压缩包...', 'info');
|
||
await downloadItemsAsZip(items);
|
||
} else {
|
||
// 下载单个文件
|
||
for (const item of items) {
|
||
downloadFile(item);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function downloadItemsAsZip(items) {
|
||
try {
|
||
showMessage(`正在压缩 ${items.length} 个项目...`, 'info');
|
||
|
||
const response = await fetch(`${API_BASE}/download-multiple-zip`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
paths: items,
|
||
filename: `batch_download_${new Date().getTime()}.zip`
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const contentDisposition = response.headers.get('content-disposition');
|
||
let fileName = `batch_download.zip`;
|
||
if (contentDisposition) {
|
||
const fileNameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
|
||
if (fileNameMatch) {
|
||
fileName = fileNameMatch[1];
|
||
}
|
||
}
|
||
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = fileName;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
showMessage('批量下载成功', 'success');
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.detail || `服务器错误: ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('批量下载失败:', error);
|
||
showMessage(`批量下载失败: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function showUploadModal() {
|
||
document.getElementById('uploadModal').style.display = 'block';
|
||
document.getElementById('fileInput').value = '';
|
||
}
|
||
|
||
function showCreateFolderModal() {
|
||
document.getElementById('createFolderModal').style.display = 'block';
|
||
document.getElementById('folderNameInput').value = '';
|
||
}
|
||
|
||
function showRenameModal(path) {
|
||
currentItemToRename = path;
|
||
const fileName = path.split('/').pop();
|
||
document.getElementById('renameInput').value = fileName;
|
||
document.getElementById('renameModal').style.display = 'block';
|
||
}
|
||
|
||
function closeModal(modalId) {
|
||
document.getElementById(modalId).style.display = 'none';
|
||
}
|
||
|
||
function showMessage(message, type) {
|
||
// 消息提示,支持多种类型
|
||
const toast = document.createElement('div');
|
||
let bgColor = '#6c757d'; // 默认灰色
|
||
|
||
switch(type) {
|
||
case 'success':
|
||
bgColor = '#28a745';
|
||
break;
|
||
case 'error':
|
||
bgColor = '#dc3545';
|
||
break;
|
||
case 'info':
|
||
bgColor = '#17a2b8';
|
||
break;
|
||
case 'warning':
|
||
bgColor = '#ffc107';
|
||
break;
|
||
}
|
||
|
||
toast.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 12px 20px;
|
||
border-radius: 4px;
|
||
color: ${type === 'warning' ? '#212529' : 'white'};
|
||
z-index: 10000;
|
||
background-color: ${bgColor};
|
||
max-width: 300px;
|
||
word-wrap: break-word;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||
`;
|
||
toast.textContent = message;
|
||
document.body.appendChild(toast);
|
||
|
||
// 自动移除
|
||
setTimeout(() => {
|
||
if (document.body.contains(toast)) {
|
||
document.body.removeChild(toast);
|
||
}
|
||
}, 4000);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |