From 88db4903c92071ecbcc201f299b2aa81dd20af82 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 23 Jan 2026 09:43:04 +0800 Subject: [PATCH] Fix upload progress and JavaScript syntax errors --- static/app.js | 874 ++++++++++++++++++++++------------------------ static/index.html | 2 + static/style.css | 187 ++++++---- 3 files changed, 530 insertions(+), 533 deletions(-) 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 = `${escapeHtml(name)}`; + elements.previewBody.innerHTML = '' + escapeHtml(name) + ''; 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 @@
+