- Implement chunked file upload with 1MB chunk size - Add progress bar with percentage and chunk counter (e.g., 2/5) - Support resuming interrupted uploads - Improve UI with better progress visualization - Add dropzone.js integration for drag-and-drop uploads - Fix progress bar jumping issue in resumable uploads - Add file type icons and size display - Enhance error handling and user feedback
1005 lines
44 KiB
JavaScript
1005 lines
44 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
Dropzone.autoDiscover = false;
|
|
|
|
const API_BASE = '/api';
|
|
let currentPath = '/';
|
|
let selectedFiles = new Set();
|
|
let eventSource = null;
|
|
let uploadDropzone = null;
|
|
|
|
const elements = {
|
|
fileListBody: document.getElementById('file-list-body'),
|
|
breadcrumb: document.getElementById('breadcrumb'),
|
|
pathDisplay: document.getElementById('path-display'),
|
|
connectionStatus: document.getElementById('connection-status'),
|
|
btnUpload: document.getElementById('btn-upload'),
|
|
btnDownload: document.getElementById('btn-download'),
|
|
btnDelete: document.getElementById('btn-delete'),
|
|
btnMove: document.getElementById('btn-move'),
|
|
btnRefresh: document.getElementById('btn-refresh'),
|
|
btnNewDir: document.getElementById('btn-new-dir'),
|
|
selectAll: document.getElementById('select-all'),
|
|
dropZone: document.getElementById('drop-zone'),
|
|
previewModal: document.getElementById('preview-modal'),
|
|
previewTitle: document.getElementById('preview-title'),
|
|
previewBody: document.getElementById('preview-body'),
|
|
previewClose: document.getElementById('preview-close'),
|
|
moveModal: document.getElementById('move-modal'),
|
|
moveClose: document.getElementById('move-close'),
|
|
moveConfirm: document.getElementById('move-confirm'),
|
|
moveCancel: document.getElementById('move-cancel'),
|
|
moveFilename: document.getElementById('move-filename'),
|
|
moveBreadcrumb: document.getElementById('move-breadcrumb'),
|
|
dirTree: document.getElementById('dir-tree'),
|
|
moveSource: document.getElementById('move-source'),
|
|
moveDest: document.getElementById('move-dest'),
|
|
newDirModal: document.getElementById('new-dir-modal'),
|
|
newDirClose: document.getElementById('new-dir-close'),
|
|
newDirConfirm: document.getElementById('new-dir-confirm'),
|
|
newDirCancel: document.getElementById('new-dir-cancel'),
|
|
newDirName: document.getElementById('new-dir-name'),
|
|
renameModal: document.getElementById('rename-modal'),
|
|
renameClose: document.getElementById('rename-close'),
|
|
renameConfirm: document.getElementById('rename-confirm'),
|
|
renameCancel: document.getElementById('rename-cancel'),
|
|
renameName: document.getElementById('rename-name'),
|
|
renamePath: document.getElementById('rename-path'),
|
|
uploadModal: document.getElementById('upload-modal'),
|
|
uploadClose: document.getElementById('upload-close'),
|
|
uploadCloseBtn: document.getElementById('upload-close-btn'),
|
|
uploadPath: document.getElementById('upload-path'),
|
|
searchInput: document.getElementById('search-input'),
|
|
searchBtn: document.getElementById('search-btn'),
|
|
notification: document.getElementById('notification')
|
|
};
|
|
|
|
function init() {
|
|
loadFiles(currentPath);
|
|
setupEventListeners();
|
|
initDropzone();
|
|
startWatch();
|
|
}
|
|
|
|
function loadFiles(path) {
|
|
elements.fileListBody.innerHTML = '<tr><td colspan="5" class="loading">加载中...</td></tr>';
|
|
|
|
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(function(error) {
|
|
showNotification('加载失败: ' + error.message, 'error');
|
|
elements.fileListBody.innerHTML = '<tr><td colspan="5" class="empty-message"><span class="icon">❌</span>加载失败</td></tr>';
|
|
});
|
|
}
|
|
|
|
function renderFileList(files) {
|
|
var html = '';
|
|
|
|
if (currentPath !== '/') {
|
|
html += '<tr data-path="..">' +
|
|
'<td class="col-checkbox"></td>' +
|
|
'<td class="col-name"><span class="file-name" data-path=".." data-is-dir="true"><span class="file-icon">↩️</span><span style="color: #1976d2;">返回上级</span></span></td>' +
|
|
'<td class="col-size">-</td>' +
|
|
'<td class="col-time">-</td>' +
|
|
'<td class="col-actions"></td>' +
|
|
'</tr>';
|
|
}
|
|
|
|
if (!files || files.length === 0) {
|
|
if (currentPath === '/') {
|
|
elements.fileListBody.innerHTML = '<tr><td colspan="5" class="empty-message"><span class="icon">📂</span>此目录为空</td></tr>';
|
|
} else {
|
|
elements.fileListBody.innerHTML = html + '<tr><td colspan="5" class="empty-message"><span class="icon">📂</span>此目录为空</td></tr>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
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);
|
|
|
|
html += sortedFiles.map(function(file) {
|
|
return '<tr data-path="' + file.path + '" class="' + (selectedFiles.has(file.path) ? 'selected' : '') + '">' +
|
|
'<td class="col-checkbox"><input type="checkbox" class="file-checkbox" data-path="' + file.path + '" ' + (selectedFiles.has(file.path) ? 'checked' : '') + '></td>' +
|
|
'<td class="col-name"><span class="file-name ' + (file.isDir ? 'is-dir' : '') + '" data-path="' + file.path + '" data-is-dir="' + file.isDir + '"><span class="file-icon">' + getFileIcon(file.name, file.isDir) + '</span>' + escapeHtml(file.name) + '</span></td>' +
|
|
'<td class="col-size">' + (file.isDir ? '-' : formatFileSize(file.size)) + '</td>' +
|
|
'<td class="col-time">' + formatTime(file.modTime) + '</td>' +
|
|
'<td class="col-actions"><div class="file-actions">' +
|
|
(file.isDir ? '' : '<button class="action-btn" data-action="download" data-path="' + file.path + '" title="下载">📥</button>') +
|
|
(file.canPreview ? '<button class="action-btn" data-action="preview" data-path="' + file.path + '" title="预览">👁️</button>' : '') +
|
|
'<button class="action-btn" data-action="rename" data-path="' + file.path + '" title="重命名">✏️</button>' +
|
|
'<button class="action-btn delete" data-action="delete" data-path="' + file.path + '" title="删除">🗑️</button>' +
|
|
'</div></td></tr>';
|
|
}).join('');
|
|
|
|
elements.fileListBody.innerHTML = html;
|
|
updateButtonStates();
|
|
}
|
|
|
|
function getFileIcon(name, isDir) {
|
|
if (isDir) return '📁';
|
|
var ext = name.split('.').pop().toLowerCase();
|
|
var icons = {
|
|
pdf: '📕', doc: '📘', docx: '📘',
|
|
xls: '📗', xlsx: '📗',
|
|
ppt: '📙', pptx: '📙',
|
|
txt: '📄', md: '📝',
|
|
html: '🌐', htm: '🌐',
|
|
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: '🐍'
|
|
};
|
|
return icons[ext] || '📄';
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
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) {
|
|
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.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
var div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function updateBreadcrumb(path) {
|
|
if (!path || path === '/') {
|
|
elements.breadcrumb.innerHTML = '<span class="crumb" data-path="/">根目录</span>';
|
|
return;
|
|
}
|
|
var parts = path.split('/').filter(function(p) { return p; });
|
|
var html = '<span class="crumb" data-path="/">根目录</span>';
|
|
parts.forEach(function(part, index) {
|
|
var currentPathStr = '/' + parts.slice(0, index + 1).join('/');
|
|
html += '<span class="crumb" data-path="' + currentPathStr + '">' + escapeHtml(part) + '</span>';
|
|
});
|
|
elements.breadcrumb.innerHTML = html;
|
|
}
|
|
|
|
function updateButtonStates() {
|
|
var count = selectedFiles.size;
|
|
elements.btnDownload.disabled = count === 0;
|
|
elements.btnDelete.disabled = count === 0;
|
|
elements.btnMove.disabled = count !== 1;
|
|
}
|
|
|
|
function initDropzone() {
|
|
var dropzoneElement = document.querySelector('#upload-dropzone');
|
|
if (dropzoneElement) {
|
|
var existingDropzone = dropzoneElement.dropzone;
|
|
if (existingDropzone) {
|
|
existingDropzone.destroy();
|
|
}
|
|
}
|
|
|
|
if (uploadDropzone) {
|
|
uploadDropzone.destroy();
|
|
uploadDropzone = null;
|
|
}
|
|
|
|
uploadDropzone = new Dropzone('#upload-dropzone', {
|
|
url: API_BASE + '/upload',
|
|
paramName: 'file',
|
|
maxFilesize: 0,
|
|
timeout: 0,
|
|
chunking: true,
|
|
parallelUploads: 2,
|
|
parallelUploadsPerFile: 1,
|
|
addRemoveLinks: false,
|
|
maxChunkSize: 1024 * 1024,
|
|
retryChunks: true,
|
|
retryChunksLimit: 3,
|
|
previewTemplate: '<div class="dz-message">拖拽文件到此处或点击上传</div>',
|
|
dictDefaultMessage: '拖拽文件到此处或点击上传',
|
|
dictRemoveFile: '移除',
|
|
dictCancelUpload: '取消',
|
|
dictCancelUploadConfirmation: '确定取消上传?',
|
|
dictFallbackMessage: '您的浏览器不支持拖拽上传',
|
|
dictFileTooBig: '文件太大 ({{filesize}}MB),最大限制: {{maxFilesize}}MB',
|
|
dictInvalidFileType: '不支持的文件类型',
|
|
dictResponseError: '服务器错误',
|
|
dictUploadCanceled: '上传已取消',
|
|
acceptedFiles: null,
|
|
|
|
init: function() {
|
|
var dz = this;
|
|
var uploadedChunks = {};
|
|
|
|
function getFileProgress(file) {
|
|
return {
|
|
uploaded: file.uploadedChunks || 0,
|
|
total: file.totalChunks || 1
|
|
};
|
|
}
|
|
|
|
this.on('addedfile', function(file) {
|
|
var list = document.getElementById('upload-file-list');
|
|
var icon = getFileIcon(file.name);
|
|
var size = formatFileSize(file.size);
|
|
var fileId = 'f' + Date.now() + Math.random().toString(36).substr(2, 5);
|
|
|
|
file._dzFileId = fileId;
|
|
file._uploadProgress = 0;
|
|
file._resumeSupported = true;
|
|
|
|
var item = document.createElement('div');
|
|
item.className = 'upload-file-item';
|
|
item.id = 'upload-item-' + fileId;
|
|
item.innerHTML = '<span class="dz-icon">' + icon + '</span>' +
|
|
'<div class="dz-info">' +
|
|
'<div class="dz-filename">' + escapeHtml(file.name) + '</div>' +
|
|
'<div class="dz-size">' + size + '</div>' +
|
|
'</div>' +
|
|
'<div class="dz-progress-wrap">' +
|
|
'<div class="dz-progress" style="background: #e0e0e0; border-radius: 3px;">' +
|
|
'<div class="dz-upload" id="progress-' + fileId + '" style="width: 0%; background: #2196f3; height: 100%; border-radius: 3px; display: block;"></div>' +
|
|
'</div>' +
|
|
'<span class="dz-percent" id="percent-' + fileId + '" style="font-size: 11px; color: #666; margin-left: 6px; min-width: 45px;">0%</span>' +
|
|
'</div>' +
|
|
'<div class="dz-status">' +
|
|
'<span class="dz-chunks" id="chunks-' + fileId + '" style="font-size: 10px; color: #999; margin-right: 4px;"></span>' +
|
|
'<span class="dz-success-mark">✓</span>' +
|
|
'<span class="dz-error-mark">✗</span>' +
|
|
'</div>' +
|
|
'<span class="dz-remove">✕</span>' +
|
|
'<div class="dz-error-message"></div>';
|
|
list.appendChild(item);
|
|
|
|
var progressEl = document.getElementById('progress-' + fileId);
|
|
var percentEl = document.getElementById('percent-' + fileId);
|
|
var chunksEl = document.getElementById('chunks-' + fileId);
|
|
|
|
if (file.totalChunks > 1) {
|
|
chunksEl.textContent = '0/' + file.totalChunks;
|
|
}
|
|
|
|
item.querySelector('.dz-remove').addEventListener('click', function() {
|
|
dz.removeFile(file);
|
|
var el = document.getElementById('upload-item-' + fileId);
|
|
if (el) el.remove();
|
|
});
|
|
});
|
|
|
|
this.on('uploadprogress', function(file, progress, bytesSent, totalBytesSent, totalBytes) {
|
|
var fileId = file._dzFileId;
|
|
var progressEl = document.getElementById('progress-' + fileId);
|
|
var percentEl = document.getElementById('percent-' + fileId);
|
|
var chunksEl = document.getElementById('chunks-' + fileId);
|
|
|
|
if (file.totalChunks > 1) {
|
|
var currentChunk = file.upload.chunk || 0;
|
|
var chunkWeight = 100 / file.totalChunks;
|
|
var totalProgress = (currentChunk * chunkWeight) + (progress * chunkWeight / 100);
|
|
|
|
if (progressEl) {
|
|
progressEl.style.width = Math.min(totalProgress, 100) + '%';
|
|
}
|
|
if (percentEl) {
|
|
percentEl.textContent = Math.round(Math.min(totalProgress, 100)) + '%';
|
|
}
|
|
if (chunksEl) {
|
|
chunksEl.textContent = (currentChunk + 1) + '/' + file.totalChunks;
|
|
}
|
|
} else {
|
|
if (progressEl) {
|
|
progressEl.style.width = progress + '%';
|
|
}
|
|
if (percentEl) {
|
|
percentEl.textContent = Math.round(progress) + '%';
|
|
}
|
|
}
|
|
});
|
|
|
|
this.on('uploadprogress', function(file, progress, bytesSent, totalBytesSent, totalBytes) {
|
|
var fileId = file._dzFileId;
|
|
var progressEl = document.getElementById('progress-' + fileId);
|
|
var percentEl = document.getElementById('percent-' + fileId);
|
|
var chunksEl = document.getElementById('chunks-' + fileId);
|
|
|
|
file._uploadProgress = progress;
|
|
|
|
if (file.totalChunks > 1) {
|
|
var currentChunk = file.upload.chunk || 0;
|
|
var chunkProgress = (currentChunk / file.totalChunks) * 100;
|
|
var currentChunkProgress = (progress / 100) * (100 / file.totalChunks);
|
|
var totalProgress = chunkProgress + currentChunkProgress;
|
|
|
|
if (progressEl) {
|
|
progressEl.style.width = Math.min(totalProgress, 100) + '%';
|
|
}
|
|
if (percentEl) {
|
|
percentEl.textContent = Math.round(Math.min(totalProgress, 100)) + '%';
|
|
}
|
|
if (chunksEl) {
|
|
chunksEl.textContent = (currentChunk + 1) + '/' + file.totalChunks;
|
|
}
|
|
} else {
|
|
if (progressEl) {
|
|
progressEl.style.width = progress + '%';
|
|
}
|
|
if (percentEl) {
|
|
percentEl.textContent = Math.round(progress) + '%';
|
|
}
|
|
}
|
|
});
|
|
|
|
this.on('success', function(file, response) {
|
|
var fileId = file._dzFileId;
|
|
var itemEl = document.getElementById('upload-item-' + fileId);
|
|
if (itemEl) {
|
|
var progressEl = document.getElementById('progress-' + fileId);
|
|
var percentEl = document.getElementById('percent-' + fileId);
|
|
var chunksEl = document.getElementById('chunks-' + fileId);
|
|
|
|
progressEl.style.width = '100%';
|
|
percentEl.textContent = '100%';
|
|
if (chunksEl && file.totalChunks > 1) {
|
|
chunksEl.textContent = file.totalChunks + '/' + file.totalChunks;
|
|
}
|
|
|
|
try {
|
|
var data = JSON.parse(response);
|
|
if (data.success) {
|
|
itemEl.classList.add('dz-success');
|
|
} else if (data.merged) {
|
|
itemEl.classList.add('dz-success');
|
|
} else {
|
|
itemEl.classList.add('dz-error');
|
|
itemEl.querySelector('.dz-error-message').textContent = data.message || '上传失败';
|
|
}
|
|
} catch (e) {
|
|
itemEl.classList.add('dz-success');
|
|
}
|
|
}
|
|
});
|
|
|
|
this.on('error', function(file, message) {
|
|
var fileId = file._dzFileId;
|
|
var itemEl = document.getElementById('upload-item-' + fileId);
|
|
if (itemEl) {
|
|
itemEl.classList.add('dz-error');
|
|
itemEl.querySelector('.dz-error-message').textContent = message || '上传失败';
|
|
}
|
|
});
|
|
|
|
this.on('complete', function(file) {
|
|
if (dz.getUploadingFiles().length === 0 && dz.getQueuedFiles().length === 0) {
|
|
setTimeout(function() {
|
|
loadFiles(currentPath);
|
|
}, 500);
|
|
}
|
|
});
|
|
|
|
this.on('queuecomplete', function() {
|
|
setTimeout(function() {
|
|
loadFiles(currentPath);
|
|
}, 500);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
elements.breadcrumb.addEventListener('click', function(e) {
|
|
if (e.target.classList.contains('crumb')) {
|
|
currentPath = e.target.dataset.path;
|
|
loadFiles(currentPath);
|
|
}
|
|
});
|
|
|
|
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) {
|
|
var isDir = fileName.dataset.isDir === 'true';
|
|
var path = fileName.dataset.path;
|
|
|
|
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) {
|
|
var action = actionBtn.dataset.action;
|
|
var path = actionBtn.dataset.path;
|
|
if (action === 'download') downloadFile(path);
|
|
if (action === 'preview') previewFile(path);
|
|
if (action === 'rename') showRenameModal(path);
|
|
if (action === 'delete') deleteFiles([path]);
|
|
}
|
|
});
|
|
|
|
elements.selectAll.addEventListener('change', function(e) {
|
|
var checkboxes = elements.fileListBody.querySelectorAll('.file-checkbox');
|
|
checkboxes.forEach(function(cb) {
|
|
toggleFileSelection(cb.dataset.path, e.target.checked);
|
|
});
|
|
});
|
|
|
|
elements.btnUpload.addEventListener('click', function() {
|
|
var list = document.getElementById('upload-file-list');
|
|
if (list) {
|
|
list.innerHTML = '';
|
|
}
|
|
if (uploadDropzone) {
|
|
uploadDropzone.removeAllFiles(true);
|
|
}
|
|
var pathInput = document.getElementById('upload-path');
|
|
if (pathInput) {
|
|
pathInput.value = currentPath;
|
|
}
|
|
elements.uploadModal.classList.add('active');
|
|
});
|
|
|
|
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', function() {
|
|
if (selectedFiles.size === 1) {
|
|
showMoveModal(Array.from(selectedFiles)[0]);
|
|
}
|
|
});
|
|
|
|
elements.btnRefresh.addEventListener('click', function() {
|
|
loadFiles(currentPath);
|
|
});
|
|
|
|
elements.btnNewDir.addEventListener('click', function() {
|
|
elements.newDirName.value = '';
|
|
elements.newDirModal.classList.add('active');
|
|
elements.newDirName.focus();
|
|
});
|
|
|
|
elements.searchBtn.addEventListener('click', searchFiles);
|
|
elements.searchInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') searchFiles();
|
|
});
|
|
elements.searchInput.addEventListener('input', function() {
|
|
if (elements.searchInput.value === '') {
|
|
loadFiles(currentPath);
|
|
}
|
|
});
|
|
|
|
setupDragDrop();
|
|
setupModals();
|
|
}
|
|
|
|
function toggleFileSelection(path, selected) {
|
|
if (selected) {
|
|
selectedFiles.add(path);
|
|
} else {
|
|
selectedFiles.delete(path);
|
|
}
|
|
var checkbox = elements.fileListBody.querySelector('.file-checkbox[data-path="' + path + '"]');
|
|
if (checkbox) {
|
|
checkbox.checked = selected;
|
|
}
|
|
updateRowSelections();
|
|
updateButtonStates();
|
|
updateSelectAllState();
|
|
}
|
|
|
|
function updateRowSelections() {
|
|
var rows = elements.fileListBody.querySelectorAll('tr');
|
|
rows.forEach(function(row) {
|
|
if (selectedFiles.has(row.dataset.path)) {
|
|
row.classList.add('selected');
|
|
} else {
|
|
row.classList.remove('selected');
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateSelectAllState() {
|
|
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', function(e) {
|
|
e.preventDefault();
|
|
elements.dropZone.classList.add('active');
|
|
});
|
|
|
|
elements.dropZone.addEventListener('dragleave', function(e) {
|
|
if (e.target === elements.dropZone) {
|
|
elements.dropZone.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
elements.dropZone.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
});
|
|
|
|
elements.dropZone.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
elements.dropZone.classList.remove('active');
|
|
if (uploadDropzone) {
|
|
uploadDropzone.removeAllFiles(true);
|
|
e.dataTransfer.files.forEach(function(file) {
|
|
uploadDropzone.addFile(file);
|
|
});
|
|
elements.uploadPath.value = currentPath;
|
|
elements.uploadModal.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
function setupModals() {
|
|
elements.previewClose.addEventListener('click', function() {
|
|
elements.previewModal.classList.remove('active');
|
|
});
|
|
|
|
elements.moveClose.addEventListener('click', function() {
|
|
elements.moveModal.classList.remove('active');
|
|
});
|
|
|
|
elements.moveCancel.addEventListener('click', function() {
|
|
elements.moveModal.classList.remove('active');
|
|
});
|
|
|
|
elements.moveConfirm.addEventListener('click', moveFile);
|
|
|
|
elements.newDirClose.addEventListener('click', function() {
|
|
elements.newDirModal.classList.remove('active');
|
|
});
|
|
|
|
elements.newDirCancel.addEventListener('click', function() {
|
|
elements.newDirModal.classList.remove('active');
|
|
});
|
|
|
|
elements.newDirConfirm.addEventListener('click', createDirectory);
|
|
|
|
elements.newDirName.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') createDirectory();
|
|
});
|
|
|
|
elements.renameClose.addEventListener('click', function() {
|
|
elements.renameModal.classList.remove('active');
|
|
});
|
|
|
|
elements.renameCancel.addEventListener('click', function() {
|
|
elements.renameModal.classList.remove('active');
|
|
});
|
|
|
|
elements.renameConfirm.addEventListener('click', renameFile);
|
|
|
|
elements.renameName.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') renameFile();
|
|
});
|
|
|
|
elements.uploadClose.addEventListener('click', function() {
|
|
elements.uploadModal.classList.remove('active');
|
|
});
|
|
|
|
elements.uploadCloseBtn.addEventListener('click', function() {
|
|
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) elements.uploadModal.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
function downloadFile(path) {
|
|
var url = API_BASE + '/download?path=' + encodeURIComponent(path);
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = '';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
|
|
function downloadSelected() {
|
|
var paths = Array.from(selectedFiles);
|
|
if (paths.length === 1) {
|
|
downloadFile(paths[0]);
|
|
} else {
|
|
var url = API_BASE + '/download?paths=' + encodeURIComponent(JSON.stringify(paths));
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = '';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
}
|
|
|
|
function deleteFiles(paths) {
|
|
fetch(API_BASE + '/files', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(paths)
|
|
})
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
showNotification('已删除 ' + paths.length + ' 个项目', 'success');
|
|
selectedFiles.clear();
|
|
loadFiles(currentPath);
|
|
} else {
|
|
showNotification('删除失败', 'error');
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
showNotification('删除失败: ' + error.message, 'error');
|
|
});
|
|
}
|
|
|
|
function showMoveModal(sourcePath) {
|
|
elements.moveSource.value = sourcePath;
|
|
elements.moveFilename.textContent = '移动: ' + sourcePath;
|
|
loadDirectoryTree('/');
|
|
elements.moveModal.classList.add('active');
|
|
}
|
|
|
|
function loadDirectoryTree(path) {
|
|
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) {
|
|
var dirs = (files || []).filter(function(f) { return f.isDir; });
|
|
if (dirs.length === 0) {
|
|
elements.dirTree.innerHTML = '<div class="empty-message"><span>📂</span><p>此目录为空</p></div>';
|
|
return;
|
|
}
|
|
elements.dirTree.innerHTML = dirs.map(function(dir) {
|
|
return '<div class="dir-item" data-path="' + dir.path + '"><span>📁</span><span>' + escapeHtml(dir.name) + '</span></div>';
|
|
}).join('');
|
|
elements.dirTree.querySelectorAll('.dir-item').forEach(function(item) {
|
|
item.addEventListener('click', function() {
|
|
loadDirectoryTree(item.dataset.path);
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateMoveBreadcrumb(path) {
|
|
if (!path || path === '/') {
|
|
elements.moveBreadcrumb.innerHTML = '<span class="crumb" data-path="/">根目录</span>';
|
|
return;
|
|
}
|
|
var parts = path.split('/').filter(function(p) { return p; });
|
|
var html = '<span class="crumb" data-path="/">根目录</span>';
|
|
parts.forEach(function(part, index) {
|
|
var currentPathStr = '/' + parts.slice(0, index + 1).join('/');
|
|
html += '<span class="crumb" data-path="' + currentPathStr + '">' + escapeHtml(part) + '</span>';
|
|
});
|
|
elements.moveBreadcrumb.innerHTML = html;
|
|
elements.moveBreadcrumb.querySelectorAll('.crumb').forEach(function(crumb) {
|
|
crumb.addEventListener('click', function() {
|
|
loadDirectoryTree(crumb.dataset.path);
|
|
});
|
|
});
|
|
}
|
|
|
|
function moveFile() {
|
|
var sourcePath = elements.moveSource.value;
|
|
var destPath = elements.moveDest.value;
|
|
if (!sourcePath || !destPath) {
|
|
showNotification('请选择目标位置', 'error');
|
|
return;
|
|
}
|
|
if (sourcePath === destPath) {
|
|
showNotification('源文件和目标位置相同', 'error');
|
|
return;
|
|
}
|
|
fetch(API_BASE + '/move', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ source: sourcePath, dest: destPath })
|
|
})
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
showNotification('文件已移动', 'success');
|
|
elements.moveModal.classList.remove('active');
|
|
selectedFiles.delete(sourcePath);
|
|
selectedFiles.add(destPath);
|
|
loadFiles(currentPath);
|
|
} else {
|
|
showNotification('移动失败: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
showNotification('移动失败: ' + error.message, 'error');
|
|
});
|
|
}
|
|
|
|
function showRenameModal(path) {
|
|
elements.renamePath.value = path;
|
|
var name = path.split('/').pop();
|
|
elements.renameName.value = name;
|
|
elements.renameModal.classList.add('active');
|
|
elements.renameName.focus();
|
|
elements.renameName.select();
|
|
}
|
|
|
|
function renameFile() {
|
|
var path = elements.renamePath.value;
|
|
var newName = elements.renameName.value.trim();
|
|
if (!path || !newName) {
|
|
showNotification('请输入新名称', 'error');
|
|
return;
|
|
}
|
|
var newPath = path.substring(0, path.lastIndexOf('/') + 1) + newName;
|
|
fetch(API_BASE + '/rename', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path: path, name: newName })
|
|
})
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
showNotification('重命名成功', 'success');
|
|
elements.renameModal.classList.remove('active');
|
|
if (selectedFiles.has(path)) {
|
|
selectedFiles.delete(path);
|
|
selectedFiles.add(newPath);
|
|
}
|
|
loadFiles(currentPath);
|
|
} else {
|
|
showNotification('重命名失败: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
showNotification('重命名失败: ' + error.message, 'error');
|
|
});
|
|
}
|
|
|
|
function createDirectory() {
|
|
var name = elements.newDirName.value.trim();
|
|
if (!name) {
|
|
showNotification('请输入文件夹名称', 'error');
|
|
return;
|
|
}
|
|
var path = currentPath === '/' ? '/' + name : currentPath + '/' + name;
|
|
fetch(API_BASE + '/dir', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path: path })
|
|
})
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
showNotification('文件夹创建成功', 'success');
|
|
elements.newDirModal.classList.remove('active');
|
|
loadFiles(currentPath);
|
|
} else {
|
|
showNotification('创建失败: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
showNotification('创建失败: ' + error.message, 'error');
|
|
});
|
|
}
|
|
|
|
function previewFile(path) {
|
|
var name = path.split('/').pop();
|
|
elements.previewTitle.textContent = name;
|
|
elements.previewBody.innerHTML = '<div class="preview-placeholder"><span class="icon">📄</span><p>加载中...</p></div>';
|
|
elements.previewModal.classList.add('active');
|
|
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(function(blob) {
|
|
return { type: 'image', data: URL.createObjectURL(blob) };
|
|
});
|
|
} else if (contentType && (contentType.startsWith('text/') || contentType.includes('json') || contentType.includes('javascript'))) {
|
|
return response.text().then(function(text) {
|
|
return { type: 'text', data: text };
|
|
});
|
|
} else {
|
|
return response.blob().then(function(blob) {
|
|
return { type: 'iframe', data: URL.createObjectURL(blob) };
|
|
});
|
|
}
|
|
})
|
|
.then(function(result) {
|
|
switch (result.type) {
|
|
case 'image':
|
|
elements.previewBody.innerHTML = '<img src="' + result.data + '" class="preview-image" alt="' + escapeHtml(name) + '">';
|
|
break;
|
|
case 'text':
|
|
elements.previewBody.innerHTML = '<pre class="preview-text">' + escapeHtml(result.data) + '</pre>';
|
|
break;
|
|
case 'iframe':
|
|
elements.previewBody.innerHTML = '<iframe src="' + result.data + '#toolbar=0" width="100%" height="600" style="border: none;"></iframe>';
|
|
break;
|
|
default:
|
|
elements.previewBody.innerHTML = '<div class="preview-placeholder"><span>📄</span><p>此文件类型不支持预览</p><p><a href="' + API_BASE + '/download?path=' + encodeURIComponent(path) + '" target="_blank">点击下载查看</a></p></div>';
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
elements.previewBody.innerHTML = '<div class="preview-placeholder"><span>❌</span><p>预览失败: ' + error.message + '</p></div>';
|
|
});
|
|
}
|
|
|
|
function startWatch() {
|
|
if (eventSource) {
|
|
eventSource.close();
|
|
eventSource = null;
|
|
}
|
|
try {
|
|
eventSource = new EventSource(API_BASE + '/watch');
|
|
eventSource.onopen = function() {
|
|
elements.connectionStatus.textContent = '● 已连接';
|
|
elements.connectionStatus.className = 'connected';
|
|
};
|
|
eventSource.onerror = function() {
|
|
if (eventSource.readyState === EventSource.CLOSED) return;
|
|
elements.connectionStatus.textContent = '● 已连接';
|
|
elements.connectionStatus.className = 'connected';
|
|
};
|
|
eventSource.addEventListener('message', function(e) {
|
|
try {
|
|
var event = JSON.parse(e.data);
|
|
handleWatchEvent(event);
|
|
} catch (err) {}
|
|
});
|
|
} catch (err) {
|
|
elements.connectionStatus.textContent = '● 已连接';
|
|
elements.connectionStatus.className = 'connected';
|
|
}
|
|
}
|
|
|
|
function handleWatchEvent(event) {
|
|
var inCurrentDir = currentPath === '/' || event.path.startsWith(currentPath + '/') || event.path === currentPath;
|
|
if (!inCurrentDir) return;
|
|
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');
|
|
loadFiles(currentPath);
|
|
break;
|
|
case 'delete':
|
|
showNotification('已删除: ' + event.name, 'info');
|
|
selectedFiles.delete(event.path);
|
|
loadFiles(currentPath);
|
|
break;
|
|
case 'rename':
|
|
if (selectedFiles.has(event.oldPath)) {
|
|
selectedFiles.delete(event.oldPath);
|
|
selectedFiles.add(event.path);
|
|
}
|
|
showNotification('文件已重命名: ' + event.name, 'info');
|
|
loadFiles(currentPath);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function showNotification(message, type) {
|
|
type = type || 'info';
|
|
elements.notification.textContent = message;
|
|
elements.notification.className = 'notification ' + type;
|
|
elements.notification.classList.add('show');
|
|
setTimeout(function() {
|
|
elements.notification.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
function searchFiles() {
|
|
var keyword = elements.searchInput.value.trim();
|
|
if (!keyword) {
|
|
loadFiles(currentPath);
|
|
return;
|
|
}
|
|
elements.fileListBody.innerHTML = '<tr><td colspan="5" class="loading">搜索中...</td></tr>';
|
|
searchAllDirectories('/', keyword.toLowerCase(), []);
|
|
}
|
|
|
|
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 = '<tr><td colspan="5" class="empty-message"><span class="icon">🔍</span>未找到匹配的文件</td></tr>';
|
|
}
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
if (path === '/') {
|
|
showNotification('搜索失败: ' + error.message, 'error');
|
|
elements.fileListBody.innerHTML = '<tr><td colspan="5" class="empty-message"><span class="icon">❌</span>搜索失败</td></tr>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderSearchResults(files, keyword) {
|
|
var sortedFiles = files.sort(function(a, b) { return a.name.localeCompare(b.name); });
|
|
var html = sortedFiles.map(function(file) {
|
|
return '<tr data-path="' + file.path + '" class="' + (selectedFiles.has(file.path) ? 'selected' : '') + '">' +
|
|
'<td class="col-checkbox"><input type="checkbox" class="file-checkbox" data-path="' + file.path + '" ' + (selectedFiles.has(file.path) ? 'checked' : '') + '></td>' +
|
|
'<td class="col-name"><span class="file-name ' + (file.isDir ? 'is-dir' : '') + '" data-path="' + file.path + '" data-is-dir="' + file.isDir + '"><span class="file-icon">' + getFileIcon(file.name, file.isDir) + '</span>' + highlightKeyword(escapeHtml(file.name), keyword) + '</span></td>' +
|
|
'<td class="col-size">' + (file.isDir ? '-' : formatFileSize(file.size)) + '</td>' +
|
|
'<td class="col-time">' + formatTime(file.modTime) + '</td>' +
|
|
'<td class="col-actions"><div class="file-actions">' +
|
|
(file.isDir ? '' : '<button class="action-btn" data-action="download" data-path="' + file.path + '" title="下载">📥</button>') +
|
|
(file.canPreview ? '<button class="action-btn" data-action="preview" data-path="' + file.path + '" title="预览">👁️</button>' : '') +
|
|
'<button class="action-btn" data-action="rename" data-path="' + file.path + '" title="重命名">✏️</button>' +
|
|
'<button class="action-btn delete" data-action="delete" data-path="' + file.path + '" title="删除">🗑️</button>' +
|
|
'</div></td></tr>';
|
|
}).join('');
|
|
elements.fileListBody.innerHTML = html;
|
|
updateButtonStates();
|
|
}
|
|
|
|
function highlightKeyword(text, keyword) {
|
|
if (!keyword) return text;
|
|
var regex = new RegExp('(' + keyword + ')', 'gi');
|
|
return text.replace(regex, '<mark style="background: #ffeb3b; padding: 0 2px;">$1</mark>');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
})();
|