Files
auto-index-go/static/app.js
admin 5120ed8af0 Fix background upload stopping issue
- Remove duplicate uploadprogress event listeners that were conflicting
- Simplify progress calculation logic to avoid jumping
- Ensure Dropzone continues processing queue after modal close
- Fix BackgroundUploadManager event binding to prevent conflicts
- Add proper progress synchronization between modal and background views
2026-01-23 15:55:27 +08:00

1261 lines
52 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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 = '📋 后台传输 (<span id="upload-count">0</span>)';
// 插入到上传按钮后面
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 = `
<div class="panel-header">
<h4>后台传输 (<span id="active-count">0</span>)</h4>
<button class="close-btn" id="background-close">×</button>
</div>
<div class="background-content">
<div class="background-list" id="background-list">
<div class="empty-state">暂无后台传输任务</div>
</div>
</div>
`;
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: 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 = '<div class="empty-state">暂无后台传输任务</div>';
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 = `
<div class="item-icon">${getFileIcon(task.fileName)}</div>
<div class="item-info">
<div class="item-name">${escapeHtml(task.fileName)}</div>
<div class="item-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${task.progress}%"></div>
</div>
<div class="progress-text">
<span class="progress-percent">${Math.round(task.progress)}%</span>
${task.chunks ? `<span class="progress-chunks">(${task.chunks})</span>` : ''}
</div>
</div>
</div>
<div class="item-status">
<span class="status-icon">${statusIcon}</span>
</div>
`;
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 = '<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) + '%';
}
}
// 同步到后台管理器
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 = '<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);
})();