diff --git a/static/app.js b/static/app.js
index c0b18ed..5127b67 100644
--- a/static/app.js
+++ b/static/app.js
@@ -5,6 +5,7 @@
let currentPath = '/';
let selectedFiles = new Set();
let eventSource = null;
+ let isUploading = false;
const elements = {
fileListBody: document.getElementById('file-list-body'),
@@ -52,14 +53,13 @@
uploadProgressContainer: document.getElementById('upload-progress-container'),
uploadProgressBar: document.getElementById('upload-progress-bar'),
uploadStats: document.getElementById('upload-stats'),
+ uploadSpeed: document.getElementById('upload-speed'),
searchInput: document.getElementById('search-input'),
searchBtn: document.getElementById('search-btn'),
notification: document.getElementById('notification')
};
let uploadXhrs = [];
- let isUploading = false;
- let searchResults = [];
function init() {
loadFiles(currentPath);
@@ -70,117 +70,105 @@
function loadFiles(path) {
elements.fileListBody.innerHTML = '
| 加载中... |
';
- fetch(`${API_BASE}/files?path=${encodeURIComponent(path)}`)
- .then(response => response.json())
- .then(data => {
+ fetch(API_BASE + '/files?path=' + encodeURIComponent(path))
+ .then(function(response) { return response.json(); })
+ .then(function(data) {
renderFileList(data.files);
updateBreadcrumb(path);
elements.pathDisplay.textContent = path || '/';
})
- .catch(error => {
+ .catch(function(error) {
showNotification('加载失败: ' + error.message, 'error');
elements.fileListBody.innerHTML = '| ❌加载失败 |
';
});
}
function renderFileList(files) {
+ var html = '';
+
+ if (currentPath !== '/') {
+ html += '' +
+ ' | ' +
+ '↩️返回上级 | ' +
+ '- | ' +
+ '- | ' +
+ ' | ' +
+ '
';
+ }
+
if (!files || files.length === 0) {
- elements.fileListBody.innerHTML = '| 📂此目录为空 |
';
+ if (currentPath === '/') {
+ elements.fileListBody.innerHTML = '| 📂此目录为空 |
';
+ } else {
+ elements.fileListBody.innerHTML = html + '| 📂此目录为空 |
';
+ }
return;
}
- const dirs = files.filter(f => f.isDir).sort((a, b) => a.name.localeCompare(b.name));
- const regularFiles = files.filter(f => !f.isDir).sort((a, b) => a.name.localeCompare(b.name));
- const sortedFiles = [...dirs, ...regularFiles];
+ var dirs = files.filter(function(f) { return f.isDir; }).sort(function(a, b) { return a.name.localeCompare(b.name); });
+ var regularFiles = files.filter(function(f) { return !f.isDir; }).sort(function(a, b) { return a.name.localeCompare(b.name); });
+ var sortedFiles = dirs.concat(regularFiles);
- elements.fileListBody.innerHTML = sortedFiles.map(file => `
-
- |
-
- |
-
-
- ${getFileIcon(file.name, file.isDir)}
- ${escapeHtml(file.name)}
-
- |
- ${file.isDir ? '-' : formatFileSize(file.size)} |
- ${formatTime(file.modTime)} |
-
-
- ${file.isDir ? '' : ``}
- ${file.canPreview ? `` : ''}
-
-
-
- |
-
- `).join('');
+ html += sortedFiles.map(function(file) {
+ return '' +
+ ' | ' +
+ '' + getFileIcon(file.name, file.isDir) + '' + escapeHtml(file.name) + ' | ' +
+ '' + (file.isDir ? '-' : formatFileSize(file.size)) + ' | ' +
+ '' + formatTime(file.modTime) + ' | ' +
+ '' +
+ (file.isDir ? '' : '') +
+ (file.canPreview ? '' : '') +
+ '' +
+ '' +
+ ' |
';
+ }).join('');
+ elements.fileListBody.innerHTML = html;
updateButtonStates();
}
function getFileIcon(name, isDir) {
if (isDir) return '📁';
- const ext = name.split('.').pop().toLowerCase();
- const icons = {
- pdf: '📕',
- doc: '📘', docx: '📘',
+ var ext = name.split('.').pop().toLowerCase();
+ var icons = {
+ pdf: '📕', doc: '📘', docx: '📘',
xls: '📗', xlsx: '📗',
ppt: '📙', pptx: '📙',
- txt: '📄',
- md: '📝',
+ txt: '📄', md: '📝',
html: '🌐', htm: '🌐',
- css: '🎨',
- js: '📜',
- json: '{ }',
+ css: '🎨', js: '📜', json: '{ }',
xml: '📋',
png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', bmp: '🖼️', webp: '🖼️',
mp3: '🎵', wav: '🎵', ogg: '🎵', flac: '🎵',
mp4: '🎬', avi: '🎬', mkv: '🎬', mov: '🎬', webm: '🎬',
zip: '📦', rar: '📦', '7z': '📦', tar: '📦', gz: '📦',
- exe: '⚙️', app: '⚙️',
- sh: '💻',
- go: '🔷',
- py: '🐍',
- java: '☕',
- c: '🔵', cpp: '🔵', h: '🔵',
- sql: '🗃️',
- csv: '📊',
- svg: '🎯'
+ exe: '⚙️', app: '⚙️', sh: '💻', go: '🔷', py: '🐍'
};
return icons[ext] || '📄';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
+ var k = 1024;
+ var sizes = ['B', 'KB', 'MB', 'GB'];
+ var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatTime(timeStr) {
- const date = new Date(timeStr);
- const now = new Date();
- const isToday = date.toDateString() === now.toDateString();
+ var date = new Date(timeStr);
+ var now = new Date();
+ var isToday = date.toDateString() === now.toDateString();
if (isToday) {
- return '今天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
+ return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
}
-
- return date.toLocaleDateString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- });
+ return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}
function escapeHtml(text) {
- const div = document.createElement('div');
+ var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
@@ -191,131 +179,109 @@
return;
}
- const parts = path.split('/').filter(p => p);
- let html = '根目录';
+ var parts = path.split('/').filter(function(p) { return p; });
+ var html = '根目录';
- let currentPath = '';
- parts.forEach((part, index) => {
- currentPath += '/' + part;
- const isLast = index === parts.length - 1;
- html += `${escapeHtml(part)}`;
+ parts.forEach(function(part, index) {
+ var currentPath = '/' + parts.slice(0, index + 1).join('/');
+ html += '' + escapeHtml(part) + '';
});
elements.breadcrumb.innerHTML = html;
}
function updateButtonStates() {
- const count = selectedFiles.size;
+ var count = selectedFiles.size;
elements.btnDownload.disabled = count === 0;
elements.btnDelete.disabled = count === 0;
elements.btnMove.disabled = count !== 1;
}
function setupEventListeners() {
- elements.breadcrumb.addEventListener('click', e => {
+ elements.breadcrumb.addEventListener('click', function(e) {
if (e.target.classList.contains('crumb')) {
currentPath = e.target.dataset.path;
loadFiles(currentPath);
}
});
- elements.fileListBody.addEventListener('click', e => {
- const checkbox = e.target.closest('.file-checkbox');
- const fileName = e.target.closest('.file-name');
- const actionBtn = e.target.closest('.action-btn');
+ elements.fileListBody.addEventListener('click', function(e) {
+ var checkbox = e.target.closest('.file-checkbox');
+ var fileName = e.target.closest('.file-name');
+ var actionBtn = e.target.closest('.action-btn');
if (checkbox) {
toggleFileSelection(checkbox.dataset.path, checkbox.checked);
} else if (fileName) {
- const isDir = fileName.dataset.isDir === 'true';
- const path = fileName.dataset.path;
+ var isDir = fileName.dataset.isDir === 'true';
+ var path = fileName.dataset.path;
- if (isDir) {
+ if (path === '..') {
+ if (currentPath !== '/') {
+ var parts = currentPath.split('/').filter(function(p) { return p; });
+ parts.pop();
+ currentPath = '/' + parts.join('/') || '/';
+ loadFiles(currentPath);
+ }
+ } else if (isDir) {
currentPath = path;
loadFiles(currentPath);
}
} else if (actionBtn) {
- const action = actionBtn.dataset.action;
- const path = actionBtn.dataset.path;
+ var action = actionBtn.dataset.action;
+ var path = actionBtn.dataset.path;
- switch (action) {
- case 'download':
- downloadFile(path);
- break;
- case 'preview':
- previewFile(path);
- break;
- case 'rename':
- showRenameModal(path);
- break;
- case 'delete':
- deleteFiles([path]);
- break;
- }
+ if (action === 'download') downloadFile(path);
+ if (action === 'preview') previewFile(path);
+ if (action === 'rename') showRenameModal(path);
+ if (action === 'delete') deleteFiles([path]);
}
});
- elements.selectAll.addEventListener('change', e => {
- const checkboxes = elements.fileListBody.querySelectorAll('.file-checkbox');
- checkboxes.forEach(cb => {
- if (e.target.checked) {
- selectedFiles.add(cb.dataset.path);
- cb.checked = true;
- } else {
- selectedFiles.delete(cb.dataset.path);
- cb.checked = false;
- }
+ elements.selectAll.addEventListener('change', function(e) {
+ var checkboxes = elements.fileListBody.querySelectorAll('.file-checkbox');
+ checkboxes.forEach(function(cb) {
+ toggleFileSelection(cb.dataset.path, e.target.checked);
});
- updateButtonStates();
- updateRowSelections();
});
- elements.btnUpload.addEventListener('click', () => {
+ elements.btnUpload.addEventListener('click', function() {
elements.fileInput.click();
});
- elements.btnDownload.addEventListener('click', () => {
- downloadSelected();
- });
-
- elements.btnDelete.addEventListener('click', () => {
- if (selectedFiles.size > 0) {
- if (confirm(`确定要删除选中的 ${selectedFiles.size} 个项目吗?`)) {
- deleteFiles(Array.from(selectedFiles));
- }
+ elements.btnDownload.addEventListener('click', downloadSelected);
+ elements.btnDelete.addEventListener('click', function() {
+ if (selectedFiles.size > 0 && confirm('确定要删除选中的 ' + selectedFiles.size + ' 个项目吗?')) {
+ deleteFiles(Array.from(selectedFiles));
}
});
- elements.btnMove.addEventListener('click', () => {
+ elements.btnMove.addEventListener('click', function() {
if (selectedFiles.size === 1) {
showMoveModal(Array.from(selectedFiles)[0]);
}
});
- elements.btnRefresh.addEventListener('click', () => {
+ elements.btnRefresh.addEventListener('click', function() {
loadFiles(currentPath);
});
- elements.btnNewDir.addEventListener('click', () => {
+ elements.btnNewDir.addEventListener('click', function() {
elements.newDirName.value = '';
elements.newDirModal.classList.add('active');
elements.newDirName.focus();
});
- elements.fileInput.addEventListener('change', e => {
+ elements.fileInput.addEventListener('change', function(e) {
uploadFiles(e.target.files);
e.target.value = '';
});
- elements.searchBtn.addEventListener('click', () => {
- searchFiles();
- });
-
- elements.searchInput.addEventListener('keypress', e => {
+ elements.searchBtn.addEventListener('click', searchFiles);
+ elements.searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') searchFiles();
});
-
- elements.searchInput.addEventListener('input', () => {
+ elements.searchInput.addEventListener('input', function() {
if (elements.searchInput.value === '') {
loadFiles(currentPath);
}
@@ -325,95 +291,13 @@
setupModals();
}
- function searchFiles() {
- const keyword = elements.searchInput.value.trim();
- if (!keyword) {
- loadFiles(currentPath);
- return;
- }
-
- elements.fileListBody.innerHTML = '| 搜索中... |
';
-
- searchAllDirectories('/', keyword.toLowerCase(), []);
- }
-
- async function searchAllDirectories(path, keyword, results) {
- try {
- const response = await fetch(`${API_BASE}/files?path=${encodeURIComponent(path)}`);
- const data = await response.json();
- const files = data.files || [];
-
- files.forEach(file => {
- if (file.name.toLowerCase().includes(keyword)) {
- results.push(file);
- }
- });
-
- const dirs = files.filter(f => f.isDir);
- if (dirs.length > 0) {
- for (const dir of dirs) {
- await searchAllDirectories(dir.path, keyword, results);
- }
- }
-
- if (path === '/') {
- if (results.length > 0) {
- renderSearchResults(results, keyword);
- } else {
- elements.fileListBody.innerHTML = '| 🔍未找到匹配的文件 |
';
- }
- }
- } catch (error) {
- if (path === '/') {
- showNotification('搜索失败: ' + error.message, 'error');
- elements.fileListBody.innerHTML = '| ❌搜索失败 |
';
- }
- }
- }
-
- function renderSearchResults(files, keyword) {
- const sortedFiles = files.sort((a, b) => a.name.localeCompare(b.name));
-
- elements.fileListBody.innerHTML = sortedFiles.map(file => `
-
- |
-
- |
-
-
- ${getFileIcon(file.name, file.isDir)}
- ${highlightKeyword(escapeHtml(file.name), keyword)}
-
- |
- ${file.isDir ? '-' : formatFileSize(file.size)} |
- ${formatTime(file.modTime)} |
-
-
- ${file.isDir ? '' : ``}
- ${file.canPreview ? `` : ''}
-
-
-
- |
-
- `).join('');
-
- updateButtonStates();
- }
-
- function highlightKeyword(text, keyword) {
- if (!keyword) return text;
- const regex = new RegExp(`(${keyword})`, 'gi');
- return text.replace(regex, '$1');
- }
-
function toggleFileSelection(path, selected) {
if (selected) {
selectedFiles.add(path);
} else {
selectedFiles.delete(path);
}
- const checkbox = elements.fileListBody.querySelector(`.file-checkbox[data-path="${path}"]`);
+ var checkbox = elements.fileListBody.querySelector('.file-checkbox[data-path="' + path + '"]');
if (checkbox) {
checkbox.checked = selected;
}
@@ -423,8 +307,8 @@
}
function updateRowSelections() {
- const rows = elements.fileListBody.querySelectorAll('tr');
- rows.forEach(row => {
+ var rows = elements.fileListBody.querySelectorAll('tr');
+ rows.forEach(function(row) {
if (selectedFiles.has(row.dataset.path)) {
row.classList.add('selected');
} else {
@@ -434,29 +318,28 @@
}
function updateSelectAllState() {
- const checkboxes = elements.fileListBody.querySelectorAll('.file-checkbox');
- const checkedCount = elements.fileListBody.querySelectorAll('.file-checkbox:checked').length;
- elements.selectAll.checked = checkedCount === checkboxes.length && checkboxes.length > 0;
- elements.selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
+ var checkboxes = elements.fileListBody.querySelectorAll('.file-checkbox');
+ var checked = Array.from(checkboxes).filter(function(cb) { return cb.checked; });
+ elements.selectAll.checked = checkboxes.length > 0 && checked.length === checkboxes.length;
}
function setupDragDrop() {
- document.addEventListener('dragenter', e => {
+ document.addEventListener('dragenter', function(e) {
e.preventDefault();
elements.dropZone.classList.add('active');
});
- elements.dropZone.addEventListener('dragleave', e => {
+ elements.dropZone.addEventListener('dragleave', function(e) {
if (e.target === elements.dropZone) {
elements.dropZone.classList.remove('active');
}
});
- elements.dropZone.addEventListener('dragover', e => {
+ elements.dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
});
- elements.dropZone.addEventListener('drop', e => {
+ elements.dropZone.addEventListener('drop', function(e) {
e.preventDefault();
elements.dropZone.classList.remove('active');
uploadFiles(e.dataTransfer.files);
@@ -464,180 +347,223 @@
}
function setupModals() {
- elements.previewClose.addEventListener('click', () => {
+ elements.previewClose.addEventListener('click', function() {
elements.previewModal.classList.remove('active');
});
- elements.moveClose.addEventListener('click', () => {
+ elements.moveClose.addEventListener('click', function() {
elements.moveModal.classList.remove('active');
});
- elements.moveCancel.addEventListener('click', () => {
+ elements.moveCancel.addEventListener('click', function() {
elements.moveModal.classList.remove('active');
});
- elements.moveConfirm.addEventListener('click', () => {
- moveFile();
- });
+ elements.moveConfirm.addEventListener('click', moveFile);
- elements.newDirClose.addEventListener('click', () => {
+ elements.newDirClose.addEventListener('click', function() {
elements.newDirModal.classList.remove('active');
});
- elements.newDirCancel.addEventListener('click', () => {
+ elements.newDirCancel.addEventListener('click', function() {
elements.newDirModal.classList.remove('active');
});
- elements.newDirConfirm.addEventListener('click', () => {
- createDirectory();
- });
+ elements.newDirConfirm.addEventListener('click', createDirectory);
- elements.newDirName.addEventListener('keypress', e => {
+ elements.newDirName.addEventListener('keypress', function(e) {
if (e.key === 'Enter') createDirectory();
});
- elements.renameClose.addEventListener('click', () => {
+ elements.renameClose.addEventListener('click', function() {
elements.renameModal.classList.remove('active');
});
- elements.renameCancel.addEventListener('click', () => {
+ elements.renameCancel.addEventListener('click', function() {
elements.renameModal.classList.remove('active');
});
- elements.renameConfirm.addEventListener('click', () => {
- renameFile();
- });
+ elements.renameConfirm.addEventListener('click', renameFile);
- elements.renameName.addEventListener('keypress', e => {
+ elements.renameName.addEventListener('keypress', function(e) {
if (e.key === 'Enter') renameFile();
});
- elements.uploadClose.addEventListener('click', () => {
+ elements.uploadClose.addEventListener('click', function() {
if (!isUploading) {
elements.uploadModal.classList.remove('active');
}
});
- elements.uploadCancel.addEventListener('click', () => {
+ elements.uploadCancel.addEventListener('click', function() {
if (isUploading) {
- uploadXhrs.forEach(xhr => xhr.abort());
+ uploadXhrs.forEach(function(xhr) { xhr.abort(); });
uploadXhrs = [];
isUploading = false;
elements.uploadModal.classList.remove('active');
}
});
- elements.uploadConfirm.addEventListener('click', () => {
+ elements.uploadConfirm.addEventListener('click', function() {
elements.uploadModal.classList.remove('active');
});
- document.addEventListener('click', e => {
- if (e.target === elements.previewModal) {
- elements.previewModal.classList.remove('active');
- }
- if (e.target === elements.moveModal) {
- elements.moveModal.classList.remove('active');
- }
- if (e.target === elements.newDirModal) {
- elements.newDirModal.classList.remove('active');
- }
- if (e.target === elements.renameModal) {
- elements.renameModal.classList.remove('active');
- }
- if (e.target === elements.uploadModal && !isUploading) {
- elements.uploadModal.classList.remove('active');
- }
+ document.addEventListener('click', function(e) {
+ if (e.target === elements.previewModal) elements.previewModal.classList.remove('active');
+ if (e.target === elements.moveModal) elements.moveModal.classList.remove('active');
+ if (e.target === elements.newDirModal) elements.newDirModal.classList.remove('active');
+ if (e.target === elements.renameModal) elements.renameModal.classList.remove('active');
+ if (e.target === elements.uploadModal && !isUploading) elements.uploadModal.classList.remove('active');
});
}
function uploadFiles(files) {
if (!files || files.length === 0) return;
- const fileArray = Array.from(files);
- elements.uploadList.innerHTML = fileArray.map((file, index) => `
-
-
${getFileIcon(file.name, false)}
-
-
${escapeHtml(file.name)}
-
等待中...
-
-
- `).join('');
+ var fileArray = Array.from(files);
+
+ elements.uploadList.innerHTML = fileArray.map(function(file, index) {
+ return '' +
+ '
' + getFileIcon(file.name, false) + '' +
+ '
' +
+ '
' + escapeHtml(file.name) + '
' +
+ '
' +
+ '' + formatFileSize(file.size) + '' +
+ '等待中...' +
+ '
' +
+ '
';
+ }).join('');
elements.uploadProgressContainer.classList.add('active');
elements.uploadProgressBar.style.width = '0%';
elements.uploadStats.classList.add('active');
+ elements.uploadSpeed.classList.add('active');
+ elements.uploadSpeed.textContent = '准备上传...';
elements.uploadCancel.style.display = 'block';
elements.uploadConfirm.style.display = 'none';
elements.uploadModal.classList.add('active');
- let completed = 0;
- const total = fileArray.length;
- isUploading = true;
+ var total = fileArray.length;
+ var totalBytes = fileArray.reduce(function(sum, f) { return sum + f.size; }, 0);
+ var uploadedBytes = 0;
+ var completedCount = 0;
uploadXhrs = [];
- fileArray.forEach((file, index) => {
- const formData = new FormData();
+ var loadedBytesMap = fileArray.map(function() { return 0; });
+ var finishedMap = fileArray.map(function() { return false; });
+ var lastTime = Date.now();
+ var lastLoaded = 0;
+
+ function updateSpeed() {
+ var now = Date.now();
+ var timeDiff = (now - lastTime) / 1000;
+ if (timeDiff < 0.5) return;
+
+ var currentLoaded = loadedBytesMap.reduce(function(sum, bytes, i) {
+ return sum + (finishedMap[i] ? fileArray[i].size : bytes);
+ }, 0);
+ var bytesDiff = currentLoaded - lastLoaded;
+ var speed = bytesDiff / timeDiff;
+
+ if (speed > 0 && completedCount < total) {
+ var remaining = totalBytes - currentLoaded;
+ var eta = remaining / speed;
+ var etaStr;
+ if (eta < 60) etaStr = Math.ceil(eta) + '秒';
+ else if (eta < 3600) etaStr = Math.ceil(eta / 60) + '分钟';
+ else etaStr = Math.ceil(eta / 3600) + '小时';
+
+ elements.uploadSpeed.textContent = formatFileSize(speed) + '/s · 剩余' + etaStr;
+ } else if (completedCount < total) {
+ elements.uploadSpeed.textContent = '处理中...';
+ }
+
+ lastTime = now;
+ lastLoaded = currentLoaded;
+ }
+
+ fileArray.forEach(function(file, index) {
+ var formData = new FormData();
formData.append('file', file);
- const xhr = new XMLHttpRequest();
+ var xhr = new XMLHttpRequest();
uploadXhrs.push(xhr);
- xhr.upload.addEventListener('progress', (e) => {
+ xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
- const percent = Math.round((e.loaded / e.total) * 100);
- document.getElementById(`upload-status-${index}`).textContent = `上传中 ${percent}%`;
+ loadedBytesMap[index] = e.loaded;
+ var percent = Math.min(99, Math.round((e.loaded / e.total) * 100));
+
+ document.getElementById('upload-progress-text-' + index).textContent = percent + '%';
+ document.getElementById('upload-item-progress-' + index).style.width = percent + '%';
+
+ var currentLoaded = loadedBytesMap.reduce(function(sum, bytes, i) {
+ return sum + (finishedMap[i] ? fileArray[i].size : bytes);
+ }, 0);
+ var allPercent = Math.min(99, Math.round((currentLoaded / totalBytes) * 100));
+ elements.uploadProgressBar.style.width = allPercent + '%';
+
+ updateSpeed();
}
});
- xhr.addEventListener('load', () => {
- completed++;
- const totalPercent = Math.round((completed / total) * 100);
- elements.uploadProgressBar.style.width = totalPercent + '%';
+ xhr.addEventListener('load', function() {
+ finishedMap[index] = true;
+ loadedBytesMap[index] = file.size;
+ uploadedBytes += file.size;
+ completedCount++;
- try {
- const response = JSON.parse(xhr.responseText);
- if (response.success) {
- document.getElementById(`upload-status-${index}`).textContent = '完成 ✓';
- } else {
- document.getElementById(`upload-status-${index}`).textContent = '失败 ✗';
- }
- } catch (e) {
- document.getElementById(`upload-status-${index}`).textContent = '失败 ✗';
- }
+ document.getElementById('upload-progress-text-' + index).textContent = '完成';
+ document.getElementById('upload-item-progress-' + index).style.background = '#4caf50';
+ document.getElementById('upload-item-progress-' + index).style.width = '100%';
- if (completed === total) {
+ var allPercent = Math.round((uploadedBytes / totalBytes) * 100);
+ elements.uploadProgressBar.style.width = allPercent + '%';
+
+ if (completedCount === total) {
isUploading = false;
elements.uploadCancel.style.display = 'none';
elements.uploadConfirm.style.display = 'block';
- elements.uploadStats.textContent = `上传完成: 成功 ${completed} 个`;
+ elements.uploadStats.textContent = '上传完成: 成功 ' + completedCount + ' 个';
+ elements.uploadSpeed.textContent = '';
+ loadFiles(currentPath);
+ } else {
+ elements.uploadSpeed.textContent = '处理中... (' + (total - completedCount) + '个文件)';
+ }
+ });
+
+ xhr.addEventListener('error', function() {
+ finishedMap[index] = true;
+ loadedBytesMap[index] = file.size;
+ uploadedBytes += file.size;
+ completedCount++;
+
+ document.getElementById('upload-progress-text-' + index).textContent = '失败';
+ document.getElementById('upload-item-progress-' + index).style.background = '#f44336';
+
+ var allPercent = Math.round((uploadedBytes / totalBytes) * 100);
+ elements.uploadProgressBar.style.width = allPercent + '%';
+
+ if (completedCount === total) {
+ isUploading = false;
+ elements.uploadCancel.style.display = 'none';
+ elements.uploadConfirm.style.display = 'block';
+ elements.uploadStats.textContent = '上传完成: 成功 ' + completedCount + ' 个';
+ elements.uploadSpeed.textContent = '';
loadFiles(currentPath);
}
});
- xhr.addEventListener('error', () => {
- completed++;
- document.getElementById(`upload-status-${index}`).textContent = '失败 ✗';
- const totalPercent = Math.round((completed / total) * 100);
- elements.uploadProgressBar.style.width = totalPercent + '%';
-
- if (completed === total) {
- isUploading = false;
- elements.uploadCancel.style.display = 'none';
- elements.uploadConfirm.style.display = 'block';
- elements.uploadStats.textContent = `上传完成: 成功 ${completed} 个`;
- loadFiles(currentPath);
- }
- });
-
- xhr.open('POST', `${API_BASE}/upload?path=${encodeURIComponent(currentPath)}`);
+ xhr.open('POST', API_BASE + '/upload?path=' + encodeURIComponent(currentPath));
xhr.send(formData);
});
}
function downloadFile(path) {
- const url = `${API_BASE}/download?path=${encodeURIComponent(path)}`;
- const a = document.createElement('a');
+ var url = API_BASE + '/download?path=' + encodeURIComponent(path);
+ var a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
@@ -646,12 +572,12 @@
}
function downloadSelected() {
- const paths = Array.from(selectedFiles);
+ var paths = Array.from(selectedFiles);
if (paths.length === 1) {
downloadFile(paths[0]);
} else {
- const url = `${API_BASE}/download?paths=${encodeURIComponent(JSON.stringify(paths))}`;
- const a = document.createElement('a');
+ var url = API_BASE + '/download?paths=' + encodeURIComponent(JSON.stringify(paths));
+ var a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
@@ -661,22 +587,22 @@
}
function deleteFiles(paths) {
- fetch(`${API_BASE}/files`, {
+ fetch(API_BASE + '/files', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(paths)
})
- .then(response => response.json())
- .then(data => {
+ .then(function(response) { return response.json(); })
+ .then(function(data) {
if (data.success) {
- showNotification(`已删除 ${paths.length} 个项目`, 'success');
+ showNotification('已删除 ' + paths.length + ' 个项目', 'success');
selectedFiles.clear();
loadFiles(currentPath);
} else {
showNotification('删除失败', 'error');
}
})
- .catch(error => {
+ .catch(function(error) {
showNotification('删除失败: ' + error.message, 'error');
});
}
@@ -689,39 +615,31 @@
}
function loadDirectoryTree(path) {
- fetch(`${API_BASE}/files?path=${encodeURIComponent(path)}`)
- .then(response => response.json())
- .then(data => {
+ fetch(API_BASE + '/files?path=' + encodeURIComponent(path))
+ .then(function(response) { return response.json(); })
+ .then(function(data) {
renderDirectoryTree(data.files || [], path);
updateMoveBreadcrumb(path);
});
}
function renderDirectoryTree(files, currentPath) {
- const dirs = (files || []).filter(f => f.isDir);
+ var dirs = (files || []).filter(function(f) { return f.isDir; });
if (dirs.length === 0) {
elements.dirTree.innerHTML = '';
return;
}
- elements.dirTree.innerHTML = dirs.map(dir => `
-
- 📁
- ${escapeHtml(dir.name)}
-
- `).join('');
+ elements.dirTree.innerHTML = dirs.map(function(dir) {
+ return '📁' + escapeHtml(dir.name) + '
';
+ }).join('');
- elements.dirTree.querySelectorAll('.dir-item').forEach(item => {
- item.addEventListener('click', () => {
+ elements.dirTree.querySelectorAll('.dir-item').forEach(function(item) {
+ item.addEventListener('click', function() {
loadDirectoryTree(item.dataset.path);
});
});
-
- const currentItem = elements.dirTree.querySelector(`.dir-item[data-path="${currentPath}"]`);
- if (currentItem) {
- currentItem.classList.add('current');
- }
}
function updateMoveBreadcrumb(path) {
@@ -730,62 +648,62 @@
return;
}
- const parts = path.split('/').filter(p => p);
- let html = '根目录';
+ var parts = path.split('/').filter(function(p) { return p; });
+ var html = '根目录';
- let currentPath = '';
- parts.forEach(part => {
- currentPath += '/' + part;
- html += `${escapeHtml(part)}`;
+ parts.forEach(function(part, index) {
+ var currentPath = '/' + parts.slice(0, index + 1).join('/');
+ html += '' + escapeHtml(part) + '';
});
elements.moveBreadcrumb.innerHTML = html;
- elements.moveBreadcrumb.querySelectorAll('.crumb').forEach(crumb => {
- crumb.addEventListener('click', () => {
+ elements.moveBreadcrumb.querySelectorAll('.crumb').forEach(function(crumb) {
+ crumb.addEventListener('click', function() {
loadDirectoryTree(crumb.dataset.path);
});
});
}
function moveFile() {
- const sourcePath = elements.moveSource.value;
- const destPath = currentPath;
+ var sourcePath = elements.moveSource.value;
+ var destPath = elements.moveDest.value;
- if (!sourcePath || !destPath) return;
-
- const sourceName = sourcePath.split('/').pop();
- const newPath = destPath === '/' ? sourceName : destPath + '/' + sourceName;
-
- if (sourcePath === newPath) {
- showNotification('源路径和目标路径相同', 'error');
+ if (!sourcePath || !destPath) {
+ showNotification('请选择目标位置', 'error');
return;
}
- fetch(`${API_BASE}/move`, {
- method: 'PUT',
+ if (sourcePath === destPath) {
+ showNotification('源文件和目标位置相同', 'error');
+ return;
+ }
+
+ fetch(API_BASE + '/move', {
+ method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ srcPath: sourcePath, destPath: newPath })
+ body: JSON.stringify({ source: sourcePath, dest: destPath })
})
- .then(response => response.json())
- .then(data => {
+ .then(function(response) { return response.json(); })
+ .then(function(data) {
if (data.success) {
- showNotification('移动成功', 'success');
+ showNotification('文件已移动', 'success');
elements.moveModal.classList.remove('active');
selectedFiles.delete(sourcePath);
+ selectedFiles.add(destPath);
loadFiles(currentPath);
} else {
- showNotification('移动失败: ' + (data.error || '未知错误'), 'error');
+ showNotification('移动失败: ' + data.message, 'error');
}
})
- .catch(error => {
+ .catch(function(error) {
showNotification('移动失败: ' + error.message, 'error');
});
}
function showRenameModal(path) {
- const name = path.split('/').pop();
elements.renamePath.value = path;
+ var name = path.split('/').pop();
elements.renameName.value = name;
elements.renameModal.classList.add('active');
elements.renameName.focus();
@@ -793,128 +711,109 @@
}
function renameFile() {
- const path = elements.renamePath.value;
- const newName = elements.renameName.value.trim();
+ var path = elements.renamePath.value;
+ var newName = elements.renameName.value.trim();
if (!path || !newName) {
showNotification('请输入新名称', 'error');
return;
}
- const dir = path.substring(0, path.lastIndexOf('/'));
- const newPath = dir ? dir + '/' + newName : newName;
+ var newPath = path.substring(0, path.lastIndexOf('/') + 1) + newName;
- if (path === newPath) {
- elements.renameModal.classList.remove('active');
- return;
- }
-
- fetch(`${API_BASE}/rename`, {
- method: 'PUT',
+ fetch(API_BASE + '/rename', {
+ method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ oldPath: path, newName: newName })
+ body: JSON.stringify({ path: path, name: newName })
})
- .then(response => response.json())
- .then(data => {
+ .then(function(response) { return response.json(); })
+ .then(function(data) {
if (data.success) {
showNotification('重命名成功', 'success');
elements.renameModal.classList.remove('active');
- selectedFiles.delete(path);
- if (selectedFiles.has(newPath)) {
- selectedFiles.delete(newPath);
+ if (selectedFiles.has(path)) {
+ selectedFiles.delete(path);
+ selectedFiles.add(newPath);
}
loadFiles(currentPath);
} else {
- showNotification('重命名失败: ' + (data.error || '未知错误'), 'error');
+ showNotification('重命名失败: ' + data.message, 'error');
}
})
- .catch(error => {
+ .catch(function(error) {
showNotification('重命名失败: ' + error.message, 'error');
});
}
function createDirectory() {
- const name = elements.newDirName.value.trim();
-
+ var name = elements.newDirName.value.trim();
if (!name) {
showNotification('请输入文件夹名称', 'error');
return;
}
- fetch(`${API_BASE}/dir`, {
+ var path = currentPath === '/' ? '/' + name : currentPath + '/' + name;
+
+ fetch(API_BASE + '/dir', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ path: currentPath, name: name })
+ body: JSON.stringify({ path: path })
})
- .then(response => response.json())
- .then(data => {
+ .then(function(response) { return response.json(); })
+ .then(function(data) {
if (data.success) {
showNotification('文件夹创建成功', 'success');
elements.newDirModal.classList.remove('active');
loadFiles(currentPath);
} else {
- showNotification('创建失败: ' + (data.error || '未知错误'), 'error');
+ showNotification('创建失败: ' + data.message, 'error');
}
})
- .catch(error => {
+ .catch(function(error) {
showNotification('创建失败: ' + error.message, 'error');
});
}
function previewFile(path) {
- const name = path.split('/').pop();
+ var name = path.split('/').pop();
elements.previewTitle.textContent = name;
- elements.previewBody.innerHTML = '加载中...
';
+ elements.previewBody.innerHTML = '';
elements.previewModal.classList.add('active');
- fetch(`${API_BASE}/preview?path=${encodeURIComponent(path)}`)
- .then(response => {
- const contentType = response.headers.get('Content-Type');
-
+ fetch(API_BASE + '/preview?path=' + encodeURIComponent(path))
+ .then(function(response) {
+ var contentType = response.headers.get('Content-Type');
if (contentType && contentType.startsWith('image/')) {
- return response.blob().then(blob => {
+ return response.blob().then(function(blob) {
return { type: 'image', data: URL.createObjectURL(blob) };
});
- } else if (contentType && contentType.startsWith('text/')) {
- return response.text().then(text => {
+ } else if (contentType && (contentType.startsWith('text/') || contentType.includes('json') || contentType.includes('javascript'))) {
+ return response.text().then(function(text) {
return { type: 'text', data: text };
});
- } else if (contentType && contentType === 'application/pdf') {
- return response.blob().then(blob => {
- return { type: 'pdf', data: URL.createObjectURL(blob) };
- });
} else {
- return { type: 'unsupported', message: '此文件类型不支持预览' };
+ return response.blob().then(function(blob) {
+ return { type: 'iframe', data: URL.createObjectURL(blob) };
+ });
}
})
- .then(result => {
+ .then(function(result) {
switch (result.type) {
case 'image':
- elements.previewBody.innerHTML = `
`;
+ elements.previewBody.innerHTML = '
';
break;
case 'text':
- elements.previewBody.innerHTML = `${escapeHtml(result.data)}`;
+ elements.previewBody.innerHTML = '' + escapeHtml(result.data) + '
';
break;
- case 'pdf':
- elements.previewBody.innerHTML = ``;
+ case 'iframe':
+ elements.previewBody.innerHTML = '';
break;
default:
- elements.previewBody.innerHTML = `
-
-
📄
-
${result.message || '此文件类型不支持预览'}
-
点击下载查看
-
- `;
+ elements.previewBody.innerHTML = '';
}
})
- .catch(error => {
- elements.previewBody.innerHTML = `
-
-
❌
-
预览失败: ${error.message}
-
- `;
+ .catch(function(error) {
+ elements.previewBody.innerHTML = '❌预览失败: ' + error.message + '
';
});
}
@@ -925,25 +824,24 @@
}
try {
- eventSource = new EventSource(`${API_BASE}/watch`);
+ eventSource = new EventSource(API_BASE + '/watch');
- eventSource.onopen = () => {
+ eventSource.onopen = function() {
elements.connectionStatus.textContent = '● 已连接';
elements.connectionStatus.className = 'connected';
};
- eventSource.onerror = () => {
+ eventSource.onerror = function() {
if (eventSource.readyState === EventSource.CLOSED) return;
elements.connectionStatus.textContent = '● 已连接';
elements.connectionStatus.className = 'connected';
};
- eventSource.addEventListener('message', e => {
+ eventSource.addEventListener('message', function(e) {
try {
- const event = JSON.parse(e.data);
+ var event = JSON.parse(e.data);
handleWatchEvent(event);
- } catch (err) {
- }
+ } catch (err) {}
});
} catch (err) {
elements.connectionStatus.textContent = '● 已连接';
@@ -952,59 +850,119 @@
}
function handleWatchEvent(event) {
- const inCurrentDir = currentPath === '/' || event.path.startsWith(currentPath + '/') ||
- event.path === currentPath;
-
+ var inCurrentDir = currentPath === '/' || event.path.startsWith(currentPath + '/') || event.path === currentPath;
if (!inCurrentDir) return;
- const pathParts = event.path.replace(rootDir, '').split('/').filter(p => p);
- const eventDir = '/' + pathParts.slice(0, -1).join('/');
+ var pathParts = event.path.split('/').filter(function(p) { return p; });
+ var eventDir = '/' + pathParts.slice(0, -1).join('/') || '/';
if (eventDir !== currentPath) return;
switch (event.type) {
case 'create':
- showNotification(`新建文件: ${event.name}`, 'info');
+ showNotification('新建文件: ' + event.name, 'info');
loadFiles(currentPath);
break;
case 'delete':
- showNotification(`已删除: ${event.name}`, 'info');
- selectedFiles.delete(event.path.replace(rootDir, ''));
+ showNotification('已删除: ' + event.name, 'info');
+ selectedFiles.delete(event.path);
loadFiles(currentPath);
break;
case 'rename':
- case 'move':
- showNotification(`文件已移动: ${event.name}`, 'info');
+ if (selectedFiles.has(event.oldPath)) {
+ selectedFiles.delete(event.oldPath);
+ selectedFiles.add(event.path);
+ }
+ showNotification('文件已重命名: ' + event.name, 'info');
loadFiles(currentPath);
break;
}
}
- function showNotification(message, type = 'info') {
+ function showNotification(message, type) {
+ type = type || 'info';
elements.notification.textContent = message;
elements.notification.className = 'notification ' + type;
elements.notification.classList.add('show');
-
- setTimeout(() => {
+ setTimeout(function() {
elements.notification.classList.remove('show');
}, 3000);
}
- function formatFileSize(bytes) {
- if (bytes === 0) return '-';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+ function searchFiles() {
+ var keyword = elements.searchInput.value.trim();
+ if (!keyword) {
+ loadFiles(currentPath);
+ return;
+ }
+
+ elements.fileListBody.innerHTML = '| 搜索中... |
';
+
+ searchAllDirectories('/', keyword.toLowerCase(), []);
}
- function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
+ function searchAllDirectories(path, keyword, results) {
+ fetch(API_BASE + '/files?path=' + encodeURIComponent(path))
+ .then(function(response) { return response.json(); })
+ .then(function(data) {
+ var files = data.files || [];
+ files.forEach(function(file) {
+ if (file.name.toLowerCase().includes(keyword)) {
+ results.push(file);
+ }
+ });
+
+ var dirs = files.filter(function(f) { return f.isDir; });
+ if (dirs.length > 0) {
+ var promises = dirs.map(function(dir) {
+ return searchAllDirectories(dir.path, keyword, results);
+ });
+ return Promise.all(promises);
+ }
+ })
+ .then(function() {
+ if (path === '/') {
+ if (results.length > 0) {
+ renderSearchResults(results, keyword);
+ } else {
+ elements.fileListBody.innerHTML = '| 🔍未找到匹配的文件 |
';
+ }
+ }
+ })
+ .catch(function(error) {
+ if (path === '/') {
+ showNotification('搜索失败: ' + error.message, 'error');
+ elements.fileListBody.innerHTML = '| ❌搜索失败 |
';
+ }
+ });
}
- const rootDir = '';
+ function renderSearchResults(files, keyword) {
+ var sortedFiles = files.sort(function(a, b) { return a.name.localeCompare(b.name); });
+
+ var html = sortedFiles.map(function(file) {
+ return '' +
+ ' | ' +
+ '' + getFileIcon(file.name, file.isDir) + '' + highlightKeyword(escapeHtml(file.name), keyword) + ' | ' +
+ '' + (file.isDir ? '-' : formatFileSize(file.size)) + ' | ' +
+ '' + formatTime(file.modTime) + ' | ' +
+ '' +
+ (file.isDir ? '' : '') +
+ (file.canPreview ? '' : '') +
+ '' +
+ '' +
+ ' |
';
+ }).join('');
+
+ elements.fileListBody.innerHTML = html;
+ updateButtonStates();
+ }
+
+ function highlightKeyword(text, keyword) {
+ if (!keyword) return text;
+ var regex = new RegExp('(' + keyword + ')', 'gi');
+ return text.replace(regex, '$1');
+ }
document.addEventListener('DOMContentLoaded', init);
})();
diff --git a/static/index.html b/static/index.html
index 0097f3f..22d8cf5 100644
--- a/static/index.html
+++ b/static/index.html
@@ -4,6 +4,7 @@
📁 文件管理器
+
@@ -128,6 +129,7 @@
+