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 = `
+
+
+ `;
+ 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;