diff --git a/static/app.js b/static/app.js index 6f0a338..9a9332c 100644 --- a/static/app.js +++ b/static/app.js @@ -8,6 +8,269 @@ let selectedFiles = new Set(); let eventSource = null; let uploadDropzone = null; + let backgroundManager = null; + + // 后台上传管理器 + class BackgroundUploadManager { + constructor() { + this.activeUploads = new Map(); + this.maxConcurrent = 5; + this.dropzoneInstance = null; + this.onUpdateCallbacks = []; + this.isPanelVisible = false; + } + + init() { + this.createBackgroundUI(); + this.bindEvents(); + } + + createBackgroundUI() { + // 创建后台按钮 + const backgroundBtn = document.createElement('button'); + backgroundBtn.className = 'btn btn-info background-btn'; + backgroundBtn.id = 'btn-background'; + backgroundBtn.style.display = 'none'; + backgroundBtn.innerHTML = '📋 后台传输 (0)'; + + // 插入到上传按钮后面 + const uploadBtn = document.getElementById('btn-upload'); + if (uploadBtn && uploadBtn.parentNode) { + uploadBtn.parentNode.insertBefore(backgroundBtn, uploadBtn.nextSibling); + } + + // 创建后台面板 + const panel = document.createElement('div'); + panel.className = 'background-panel'; + panel.id = 'background-panel'; + panel.innerHTML = ` +
+

后台传输 (0)

+ +
+
+
+
暂无后台传输任务
+
+
+ `; + document.body.appendChild(panel); + + // 存储DOM引用 + this.btnBackground = backgroundBtn; + this.backgroundPanel = panel; + this.backgroundList = panel.querySelector('#background-list'); + this.activeCount = panel.querySelector('#active-count'); + this.uploadCount = backgroundBtn.querySelector('#upload-count'); + } + + bindEvents() { + this.btnBackground.addEventListener('click', () => { + this.showPanel(); + }); + + this.backgroundPanel.querySelector('#background-close').addEventListener('click', () => { + this.hidePanel(); + }); + + // 点击面板外部关闭 + this.backgroundPanel.addEventListener('click', (e) => { + if (e.target === this.backgroundPanel) { + this.hidePanel(); + } + }); + } + + takeOverUploads(dropzone) { + this.dropzoneInstance = dropzone; + + // 分析当前上传状态 + const uploadingFiles = dropzone.getUploadingFiles(); + const queuedFiles = dropzone.getQueuedFiles(); + + [...uploadingFiles, ...queuedFiles].forEach(file => { + this.addUploadTask(file); + }); + + // 绑定事件监听 + this.bindDropzoneEvents(); + } + + bindDropzoneEvents() { + if (!this.dropzoneInstance) return; + + const dz = this.dropzoneInstance; + + dz.on('uploadprogress', (file, progress, bytesSent, totalBytesSent, totalBytes) => { + let chunkInfo = null; + if (file.totalChunks > 1) { + const currentChunk = (file.upload && file.upload.chunk) || 0; + chunkInfo = `${currentChunk + 1}/${file.totalChunks}`; + } + this.updateProgress(file._dzFileId, progress, chunkInfo); + }); + + dz.on('success', (file, response) => { + this.completeUpload(file._dzFileId); + }); + + dz.on('error', (file, message) => { + this.failUpload(file._dzFileId); + }); + + dz.on('complete', (file) => { + if (dz.getUploadingFiles().length === 0 && dz.getQueuedFiles().length === 0) { + setTimeout(() => { + loadFiles(currentPath); + }, 500); + } + }); + + dz.on('queuecomplete', () => { + setTimeout(() => { + loadFiles(currentPath); + }, 500); + }); + } + + addUploadTask(file) { + const task = { + id: file._dzFileId, + fileName: file.name, + totalSize: file.size, + progress: file._uploadProgress || 0, + status: 'uploading', + chunks: null, + startTime: Date.now() + }; + + if (file.totalChunks > 1) { + const currentChunk = (file.upload && file.upload.chunk) || 0; + task.chunks = `${currentChunk}/${file.totalChunks}`; + } + + this.activeUploads.set(task.id, task); + this.notifyUIUpdate(); + } + + updateProgress(fileId, progress, chunkInfo) { + const task = this.activeUploads.get(fileId); + if (task) { + task.progress = progress; + task.chunks = chunkInfo; + this.notifyUIUpdate(); + } + } + + completeUpload(fileId) { + const task = this.activeUploads.get(fileId); + if (task) { + task.status = 'completed'; + task.progress = 100; + // 延迟删除,让用户看到完成状态 + setTimeout(() => { + this.activeUploads.delete(fileId); + this.notifyUIUpdate(); + }, 2000); + } + } + + failUpload(fileId) { + const task = this.activeUploads.get(fileId); + if (task) { + task.status = 'failed'; + this.notifyUIUpdate(); + } + } + + hasActiveUploads() { + return this.activeUploads.size > 0; + } + + showPanel() { + this.backgroundPanel.style.display = 'block'; + this.isPanelVisible = true; + this.updateUI(); + } + + hidePanel() { + this.backgroundPanel.style.display = 'none'; + this.isPanelVisible = false; + } + + updateUI() { + const activeCount = this.activeUploads.size; + + // 更新后台按钮 + if (activeCount > 0) { + this.btnBackground.style.display = 'inline-block'; + this.uploadCount.textContent = activeCount; + } else { + this.btnBackground.style.display = 'none'; + } + + // 更新面板标题 + this.activeCount.textContent = activeCount; + + // 更新列表 + if (this.isPanelVisible) { + this.renderUploadList(); + } + } + + renderUploadList() { + const list = this.backgroundList; + list.innerHTML = ''; + + if (this.activeUploads.size === 0) { + list.innerHTML = '
暂无后台传输任务
'; + return; + } + + this.activeUploads.forEach(task => { + const item = this.createUploadItem(task); + list.appendChild(item); + }); + } + + createUploadItem(task) { + const item = document.createElement('div'); + item.className = 'background-item'; + item.dataset.id = task.id; + + const statusIcon = task.status === 'completed' ? '✓' : + task.status === 'failed' ? '✗' : '⏳'; + + item.innerHTML = ` +
${getFileIcon(task.fileName)}
+
+
${escapeHtml(task.fileName)}
+
+
+
+
+
+ ${Math.round(task.progress)}% + ${task.chunks ? `(${task.chunks})` : ''} +
+
+
+
+ ${statusIcon} +
+ `; + + return item; + } + + notifyUIUpdate() { + this.updateUI(); + } + + onUpdate(callback) { + this.onUpdateCallbacks.push(callback); + } + } const elements = { fileListBody: document.getElementById('file-list-body'), @@ -59,9 +322,29 @@ loadFiles(currentPath); setupEventListeners(); initDropzone(); + initBackgroundManager(); startWatch(); } + function initBackgroundManager() { + backgroundManager = new BackgroundUploadManager(); + backgroundManager.init(); + } + + function handleUploadModalClose() { + // 检查是否有正在上传的文件 + const uploadingFiles = uploadDropzone ? uploadDropzone.getUploadingFiles() : []; + const queuedFiles = uploadDropzone ? uploadDropzone.getQueuedFiles() : []; + + if (uploadingFiles.length > 0 || queuedFiles.length > 0) { + // 转移到后台管理器 + backgroundManager.takeOverUploads(uploadDropzone); + } + + // 隐藏模态框 + elements.uploadModal.classList.remove('active'); + } + function loadFiles(path) { elements.fileListBody.innerHTML = '加载中...'; @@ -609,11 +892,11 @@ }); elements.uploadClose.addEventListener('click', function() { - elements.uploadModal.classList.remove('active'); + handleUploadModalClose(); }); elements.uploadCloseBtn.addEventListener('click', function() { - elements.uploadModal.classList.remove('active'); + handleUploadModalClose(); }); document.addEventListener('click', function(e) { @@ -621,7 +904,7 @@ 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) elements.uploadModal.classList.remove('active'); + if (e.target === elements.uploadModal) handleUploadModalClose(); }); } diff --git a/static/style.css b/static/style.css index a603372..ab0bca6 100644 --- a/static/style.css +++ b/static/style.css @@ -224,6 +224,178 @@ body { background: #d32f2f; } +.btn-info { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + color: white; +} + +.btn-info:hover:not(:disabled) { + background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%); +} + +.background-btn { + animation: pulse 2s infinite; + font-size: 13px; + padding: 6px 12px; + white-space: nowrap; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.02); } + 100% { transform: scale(1); } +} + +.background-panel { + position: fixed; + bottom: 20px; + right: 20px; + width: 350px; + max-height: 400px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 1000; + display: none; + flex-direction: column; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + background: #f8f9fa; + border-radius: 8px 8px 0 0; +} + +.panel-header h4 { + margin: 0; + font-size: 14px; + color: #333; + font-weight: 600; +} + +.close-btn { + background: none; + border: none; + font-size: 18px; + color: #999; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +.close-btn:hover { + background: #e9ecef; + color: #666; +} + +.background-content { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.background-list { + flex: 1; + overflow-y: auto; + max-height: 350px; +} + +.empty-state { + padding: 40px 20px; + text-align: center; + color: #999; + font-size: 14px; +} + +.background-item { + display: flex; + align-items: center; + padding: 10px; + border-bottom: 1px solid #f0f0f0; + transition: background-color 0.2s; +} + +.background-item:hover { + background: #f8f9fa; +} + +.background-item:last-child { + border-bottom: none; +} + +.item-icon { + font-size: 16px; + width: 24px; + text-align: center; + flex-shrink: 0; +} + +.item-info { + flex: 1; + min-width: 0; + margin-left: 10px; +} + +.item-name { + font-size: 12px; + color: #333; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; + font-weight: 500; +} + +.item-progress { + display: flex; + align-items: center; + gap: 8px; +} + +.progress-bar { + width: 80px; + height: 4px; + background: #e0e0e0; + border-radius: 2px; + overflow: hidden; + flex-shrink: 0; +} + +.progress-fill { + height: 100%; + background: #2196f3; + border-radius: 2px; + transition: width 0.3s ease; +} + +.progress-text { + font-size: 11px; + color: #666; + font-family: monospace; + min-width: 60px; +} + +.item-status { + width: 20px; + text-align: center; + flex-shrink: 0; +} + +.status-icon { + font-size: 14px; +} + .file-list-container { background: #fff; border-radius: 6px;