(function() {
'use strict';
Dropzone.autoDiscover = false;
const API_BASE = '/api';
let currentPath = '/';
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();
// 确保Dropzone继续处理上传队列
setTimeout(() => {
if (dropzone.getQueuedFiles().length > 0) {
dropzone.processQueue();
}
}, 100);
}
bindDropzoneEvents() {
if (!this.dropzoneInstance) return;
const dz = this.dropzoneInstance;
// 监听成功和错误事件
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: isNaN(file._uploadProgress) ? 0 : Math.max(0, Math.min(100, 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) {
// 确保progress是有效的数值
task.progress = isNaN(progress) ? 0 : Math.max(0, Math.min(100, 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' ? '✗' : '⏳';
// 确保progress是有效的数值
const safeProgress = isNaN(task.progress) ? 0 : Math.max(0, Math.min(100, task.progress));
item.innerHTML = `
${getFileIcon(task.fileName)}
${escapeHtml(task.fileName)}
${Math.round(safeProgress)}%
${task.chunks ? `(${task.chunks}) ` : ''}
${statusIcon}
`;
return item;
}
notifyUIUpdate() {
this.updateUI();
}
onUpdate(callback) {
this.onUpdateCallbacks.push(callback);
}
}
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();
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 = '加载中... ';
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 = '❌ 加载失败 ';
});
}
function renderFileList(files) {
var html = '';
if (currentPath !== '/') {
html += '' +
' ' +
'↩️ 返回上级 ' +
'- ' +
'- ' +
' ' +
' ';
}
if (!files || files.length === 0) {
if (currentPath === '/') {
elements.fileListBody.innerHTML = '📂 此目录为空 ';
} else {
elements.fileListBody.innerHTML = html + '📂 此目录为空 ';
}
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 '' +
' ' +
'' + 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 '📁';
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 = '根目录 ';
return;
}
var parts = path.split('/').filter(function(p) { return p; });
var html = '根目录 ';
parts.forEach(function(part, index) {
var currentPathStr = '/' + parts.slice(0, index + 1).join('/');
html += '' + escapeHtml(part) + ' ';
});
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: '拖拽文件到此处或点击上传
',
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 = '' + icon + ' ' +
'' +
'
' + escapeHtml(file.name) + '
' +
'
' + size + '
' +
'
' +
'' +
'' +
' ' +
'✓ ' +
'✗ ' +
'
' +
'✕ ' +
'
';
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 totalProgress = progress; // 默认值
// 更新悬浮窗中的进度
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;
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 {
totalProgress = progress;
if (progressEl) {
progressEl.style.width = progress + '%';
}
if (percentEl) {
percentEl.textContent = Math.round(progress) + '%';
}
}
// 同步到后台管理器
if (backgroundManager) {
backgroundManager.updateProgress(fileId, totalProgress, file.totalChunks > 1 ? `${(file.upload.chunk || 0) + 1}/${file.totalChunks}` : null);
}
});
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() {
handleUploadModalClose();
});
elements.uploadCloseBtn.addEventListener('click', function() {
handleUploadModalClose();
});
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) handleUploadModalClose();
});
}
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 = '';
return;
}
elements.dirTree.innerHTML = dirs.map(function(dir) {
return '📁 ' + escapeHtml(dir.name) + '
';
}).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 = '根目录 ';
return;
}
var parts = path.split('/').filter(function(p) { return p; });
var html = '根目录 ';
parts.forEach(function(part, index) {
var currentPathStr = '/' + parts.slice(0, index + 1).join('/');
html += '' + escapeHtml(part) + ' ';
});
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 = '';
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 = ' ';
break;
case 'text':
elements.previewBody.innerHTML = '' + escapeHtml(result.data) + ' ';
break;
case 'iframe':
elements.previewBody.innerHTML = '';
break;
default:
elements.previewBody.innerHTML = '';
}
})
.catch(function(error) {
elements.previewBody.innerHTML = '❌ 预览失败: ' + error.message + '
';
});
}
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 = '搜索中... ';
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 = '🔍 未找到匹配的文件 ';
}
}
})
.catch(function(error) {
if (path === '/') {
showNotification('搜索失败: ' + error.message, 'error');
elements.fileListBody.innerHTML = '❌ 搜索失败 ';
}
});
}
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);
})();