class FilePanel { constructor() { this.isVisible = false; this.localPath = ''; this.remotePath = ''; this.localFiles = []; this.remoteFiles = []; this.selectedLocal = null; this.selectedRemote = null; this.transfers = []; this.chunkSize = 5 * 1024 * 1024; this.init(); } init() { this.createPanel(); this.bindEvents(); } createPanel() { const panel = document.createElement('div'); panel.id = 'file-panel'; panel.innerHTML = `
文件传输
本地文件
远程文件
`; document.body.appendChild(panel); this.panel = panel; } bindEvents() { document.getElementById('close-panel').addEventListener('click', () => this.hide()); document.getElementById('local-up').addEventListener('click', () => this.navigateLocalUp()); document.getElementById('remote-up').addEventListener('click', () => this.navigateRemoteUp()); document.getElementById('select-local-file').addEventListener('click', () => { document.getElementById('file-input').click(); }); document.getElementById('file-input').addEventListener('change', (e) => { this.handleLocalFileSelect(e.target.files); }); document.getElementById('upload-btn').addEventListener('click', () => this.uploadSelected()); document.getElementById('download-btn').addEventListener('click', () => this.downloadSelected()); document.getElementById('delete-btn').addEventListener('click', () => this.deleteSelected()); document.getElementById('remote-files').addEventListener('dblclick', (e) => { const item = e.target.closest('.file-item'); if (item && item.dataset.isDirectory === 'true') { this.navigateRemote(item.dataset.name); } }); document.getElementById('remote-files').addEventListener('click', (e) => { const item = e.target.closest('.file-item'); if (item) { this.selectRemote(item.dataset.name, item.dataset.isDirectory === 'true'); } }); } show() { this.isVisible = true; this.panel.classList.add('visible'); this.refreshRemote(); } hide() { this.isVisible = false; this.panel.classList.remove('visible'); } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); } } async refreshRemote() { try { const res = await fetch(`/api/files/browse?path=${encodeURIComponent(this.remotePath)}`); const data = await res.json(); if (data.error) { console.error('Browse error:', data.error); return; } this.remoteFiles = data.items || []; this.remotePath = data.currentPath || ''; document.getElementById('remote-path').value = this.remotePath || '/'; this.renderRemoteFiles(); } catch (error) { console.error('Failed to refresh remote:', error); } } renderRemoteFiles() { const container = document.getElementById('remote-files'); if (this.remotePath) { container.innerHTML = `
📁 ..
`; } else { container.innerHTML = ''; } this.remoteFiles.forEach(item => { const div = document.createElement('div'); div.className = `file-item ${item.isDirectory ? 'directory' : 'file'}`; div.dataset.name = item.name; div.dataset.isDirectory = item.isDirectory; div.innerHTML = ` ${item.isDirectory ? '📁' : this.getFileIcon(item.name)} ${item.name} ${item.isDirectory ? '' : this.formatSize(item.size)} `; container.appendChild(div); }); this.selectedRemote = null; this.updateRemoteActions(); } selectRemote(name, isDirectory) { const items = document.querySelectorAll('#remote-files .file-item'); items.forEach(item => item.classList.remove('selected')); const selected = document.querySelector(`#remote-files .file-item[data-name="${name}"]`); if (selected) { selected.classList.add('selected'); this.selectedRemote = { name, isDirectory }; } this.updateRemoteActions(); } updateRemoteActions() { const downloadBtn = document.getElementById('download-btn'); const deleteBtn = document.getElementById('delete-btn'); if (this.selectedRemote && !this.selectedRemote.isDirectory) { downloadBtn.disabled = false; deleteBtn.disabled = false; } else { downloadBtn.disabled = true; deleteBtn.disabled = true; } } navigateRemote(name) { if (name === '..') { this.remotePath = this.remotePath.split('/').slice(0, -1).join('/'); } else { this.remotePath = this.remotePath ? `${this.remotePath}/${name}` : name; } this.refreshRemote(); } navigateRemoteUp() { if (this.remotePath) { this.remotePath = this.remotePath.split('/').slice(0, -1).join('/'); this.refreshRemote(); } } handleLocalFileSelect(files) { if (files.length === 0) return; this.localFiles = Array.from(files).map(file => ({ name: file.name, size: file.size, file: file, isDirectory: false })); this.renderLocalFiles(); } renderLocalFiles() { const container = document.getElementById('local-files'); container.innerHTML = ''; this.localFiles.forEach((item, index) => { const div = document.createElement('div'); div.className = 'file-item file'; div.dataset.index = index; div.innerHTML = ` ${this.getFileIcon(item.name)} ${item.name} ${this.formatSize(item.size)} `; container.appendChild(div); }); container.querySelectorAll('.remove-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const index = parseInt(btn.dataset.index); this.localFiles.splice(index, 1); this.renderLocalFiles(); this.updateLocalActions(); }); }); container.querySelectorAll('.file-item').forEach(item => { item.addEventListener('click', () => { container.querySelectorAll('.file-item').forEach(i => i.classList.remove('selected')); item.classList.add('selected'); this.selectedLocal = parseInt(item.dataset.index); this.updateLocalActions(); }); }); this.updateLocalActions(); } updateLocalActions() { const uploadBtn = document.getElementById('upload-btn'); uploadBtn.disabled = this.localFiles.length === 0; } navigateLocalUp() { this.localFiles = []; this.selectedLocal = null; this.renderLocalFiles(); this.updateLocalActions(); } async uploadSelected() { if (this.localFiles.length === 0) return; for (const item of this.localFiles) { await this.uploadFile(item); } this.localFiles = []; this.selectedLocal = null; this.renderLocalFiles(); this.updateLocalActions(); this.refreshRemote(); } async uploadFile(item) { const transferId = Date.now(); this.addTransfer(transferId, item.name, 'upload', item.size); try { const totalChunks = Math.ceil(item.size / this.chunkSize); const startRes = await fetch('/api/files/upload/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: item.name, totalChunks, fileSize: item.size }) }); const { fileId } = await startRes.json(); for (let i = 0; i < totalChunks; i++) { const start = i * this.chunkSize; const end = Math.min(start + this.chunkSize, item.size); const chunk = item.file.slice(start, end); const formData = new FormData(); formData.append('fileId', fileId); formData.append('chunkIndex', i); formData.append('chunk', chunk); await fetch('/api/files/upload/chunk', { method: 'POST', body: formData }); const progress = Math.round(((i + 1) / totalChunks) * 100); this.updateTransfer(transferId, progress); } const remoteFilePath = this.remotePath ? `${this.remotePath}/${item.name}` : item.name; await fetch('/api/files/upload/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileId, totalChunks, filename: remoteFilePath }) }); this.completeTransfer(transferId); } catch (error) { console.error('Upload failed:', error); this.failTransfer(transferId, error.message); } } async downloadSelected() { if (!this.selectedRemote || this.selectedRemote.isDirectory) return; const filename = this.selectedRemote.name; const remoteFilePath = this.remotePath ? `${this.remotePath}/${filename}` : filename; const transferId = Date.now(); this.addTransfer(transferId, filename, 'download', 0); try { const res = await fetch(`/api/files/${encodeURIComponent(remoteFilePath)}`); if (!res.ok) { throw new Error('Download failed'); } const contentLength = parseInt(res.headers.get('Content-Length') || '0'); this.updateTransferSize(transferId, contentLength); const reader = res.body.getReader(); const chunks = []; let received = 0; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); received += value.length; const progress = contentLength > 0 ? Math.round((received / contentLength) * 100) : 0; this.updateTransfer(transferId, progress); } const blob = new Blob(chunks); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); this.completeTransfer(transferId); } catch (error) { console.error('Download failed:', error); this.failTransfer(transferId, error.message); } } async deleteSelected() { if (!this.selectedRemote || this.selectedRemote.isDirectory) return; if (!confirm(`确定要删除 "${this.selectedRemote.name}" 吗?`)) return; const remoteFilePath = this.remotePath ? `${this.remotePath}/${this.selectedRemote.name}` : this.selectedRemote.name; try { const res = await fetch(`/api/files/${encodeURIComponent(remoteFilePath)}`, { method: 'DELETE' }); if (res.ok) { this.refreshRemote(); } } catch (error) { console.error('Delete failed:', error); } } addTransfer(id, name, type, size) { const transfer = { id, name, type, size, progress: 0, status: 'transferring' }; this.transfers.push(transfer); this.renderTransfers(); } updateTransfer(id, progress) { const transfer = this.transfers.find(t => t.id === id); if (transfer) { transfer.progress = progress; this.renderTransfers(); } } updateTransferSize(id, size) { const transfer = this.transfers.find(t => t.id === id); if (transfer) { transfer.size = size; this.renderTransfers(); } } completeTransfer(id) { const transfer = this.transfers.find(t => t.id === id); if (transfer) { transfer.status = 'completed'; transfer.progress = 100; this.renderTransfers(); setTimeout(() => { this.transfers = this.transfers.filter(t => t.id !== id); this.renderTransfers(); }, 3000); } } failTransfer(id, error) { const transfer = this.transfers.find(t => t.id === id); if (transfer) { transfer.status = 'failed'; transfer.error = error; this.renderTransfers(); } } renderTransfers() { const container = document.getElementById('transfer-list'); if (this.transfers.length === 0) { container.innerHTML = ''; return; } container.innerHTML = this.transfers.map(t => `
${t.type === 'upload' ? '↑' : '↓'} ${t.name}
${t.progress}%
`).join(''); } formatSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } getFileIcon(filename) { const ext = filename.split('.').pop().toLowerCase(); const icons = { 'txt': '📄', 'doc': '📄', 'docx': '📄', 'pdf': '📄', 'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'mp3': '🎵', 'wav': '🎵', 'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬', 'zip': '📦', 'rar': '📦', '7z': '📦', 'exe': '⚙️', 'msi': '⚙️', 'js': '📜', 'ts': '📜', 'py': '📜', 'html': '📜', 'css': '📜' }; return icons[ext] || '📄'; } } window.FilePanel = FilePanel;