Add resumable upload feature with progress display
- 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
This commit is contained in:
129
main.go
129
main.go
@@ -391,6 +391,22 @@ func uploadFile(c *gin.Context) {
|
|||||||
path = "."
|
path = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dzuuid := c.PostForm("dzuuid")
|
||||||
|
chunkIndex := c.PostForm("dzchunkindex")
|
||||||
|
totalChunkCount := c.PostForm("dztotalchunkcount")
|
||||||
|
fileName := c.PostForm("dzanonymousfilename")
|
||||||
|
|
||||||
|
isChunked := dzuuid != "" && chunkIndex != ""
|
||||||
|
|
||||||
|
tmpDir := filepath.Join(rootDir, ".tmp", "chunks")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
file, header, err := c.Request.FormFile("file")
|
file, header, err := c.Request.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, UploadResponse{
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
||||||
@@ -401,7 +417,12 @@ func uploadFile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
fullPath := filepath.Join(rootDir, path, header.Filename)
|
targetFileName := header.Filename
|
||||||
|
if fileName != "" {
|
||||||
|
targetFileName = fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(rootDir, path, targetFileName)
|
||||||
if !strings.HasPrefix(fullPath, rootDir) {
|
if !strings.HasPrefix(fullPath, rootDir) {
|
||||||
c.JSON(http.StatusBadRequest, UploadResponse{
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -410,24 +431,98 @@ func uploadFile(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
outFile, err := os.Create(fullPath)
|
if isChunked {
|
||||||
if err != nil {
|
idx := 0
|
||||||
c.JSON(http.StatusBadRequest, UploadResponse{
|
fmt.Sscanf(chunkIndex, "%d", &idx)
|
||||||
Success: false,
|
total := 0
|
||||||
Message: err.Error(),
|
if totalChunkCount != "" {
|
||||||
|
fmt.Sscanf(totalChunkCount, "%d", &total)
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkDir := filepath.Join(tmpDir, dzuuid)
|
||||||
|
if err := os.MkdirAll(chunkDir, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkPath := filepath.Join(chunkDir, fmt.Sprintf("chunk_%d", idx))
|
||||||
|
outFile, err := os.Create(chunkPath)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
io.Copy(outFile, file)
|
||||||
|
|
||||||
|
if idx == total-1 && total > 0 {
|
||||||
|
finalFile, err := os.Create(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(chunkDir)
|
||||||
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer finalFile.Close()
|
||||||
|
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
chunkPath := filepath.Join(chunkDir, fmt.Sprintf("chunk_%d", i))
|
||||||
|
chunkFile, err := os.Open(chunkPath)
|
||||||
|
if err != nil {
|
||||||
|
finalFile.Close()
|
||||||
|
os.RemoveAll(chunkDir)
|
||||||
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
io.Copy(finalFile, chunkFile)
|
||||||
|
chunkFile.Close()
|
||||||
|
os.Remove(chunkPath)
|
||||||
|
}
|
||||||
|
os.RemoveAll(chunkDir)
|
||||||
|
|
||||||
|
watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: targetFileName}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, UploadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "上传成功",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, UploadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "分块上传成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outFile, err := os.Create(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
io.Copy(outFile, file)
|
||||||
|
|
||||||
|
watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: targetFileName}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, UploadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "上传成功",
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer outFile.Close()
|
|
||||||
|
|
||||||
io.Copy(outFile, file)
|
|
||||||
|
|
||||||
watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: header.Filename}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, UploadResponse{
|
|
||||||
Success: true,
|
|
||||||
Message: "上传成功",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteFiles(c *gin.Context) {
|
func deleteFiles(c *gin.Context) {
|
||||||
|
|||||||
464
static/app.js
464
static/app.js
@@ -1,11 +1,13 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
Dropzone.autoDiscover = false;
|
||||||
|
|
||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
let currentPath = '/';
|
let currentPath = '/';
|
||||||
let selectedFiles = new Set();
|
let selectedFiles = new Set();
|
||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
let isUploading = false;
|
let uploadDropzone = null;
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
fileListBody: document.getElementById('file-list-body'),
|
fileListBody: document.getElementById('file-list-body'),
|
||||||
@@ -19,7 +21,6 @@
|
|||||||
btnRefresh: document.getElementById('btn-refresh'),
|
btnRefresh: document.getElementById('btn-refresh'),
|
||||||
btnNewDir: document.getElementById('btn-new-dir'),
|
btnNewDir: document.getElementById('btn-new-dir'),
|
||||||
selectAll: document.getElementById('select-all'),
|
selectAll: document.getElementById('select-all'),
|
||||||
fileInput: document.getElementById('file-input'),
|
|
||||||
dropZone: document.getElementById('drop-zone'),
|
dropZone: document.getElementById('drop-zone'),
|
||||||
previewModal: document.getElementById('preview-modal'),
|
previewModal: document.getElementById('preview-modal'),
|
||||||
previewTitle: document.getElementById('preview-title'),
|
previewTitle: document.getElementById('preview-title'),
|
||||||
@@ -47,23 +48,17 @@
|
|||||||
renamePath: document.getElementById('rename-path'),
|
renamePath: document.getElementById('rename-path'),
|
||||||
uploadModal: document.getElementById('upload-modal'),
|
uploadModal: document.getElementById('upload-modal'),
|
||||||
uploadClose: document.getElementById('upload-close'),
|
uploadClose: document.getElementById('upload-close'),
|
||||||
uploadCancel: document.getElementById('upload-cancel'),
|
uploadCloseBtn: document.getElementById('upload-close-btn'),
|
||||||
uploadConfirm: document.getElementById('upload-confirm'),
|
uploadPath: document.getElementById('upload-path'),
|
||||||
uploadList: document.getElementById('upload-list'),
|
|
||||||
uploadProgressContainer: document.getElementById('upload-progress-container'),
|
|
||||||
uploadProgressBar: document.getElementById('upload-progress-bar'),
|
|
||||||
uploadStats: document.getElementById('upload-stats'),
|
|
||||||
uploadSpeed: document.getElementById('upload-speed'),
|
|
||||||
searchInput: document.getElementById('search-input'),
|
searchInput: document.getElementById('search-input'),
|
||||||
searchBtn: document.getElementById('search-btn'),
|
searchBtn: document.getElementById('search-btn'),
|
||||||
notification: document.getElementById('notification')
|
notification: document.getElementById('notification')
|
||||||
};
|
};
|
||||||
|
|
||||||
let uploadXhrs = [];
|
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
loadFiles(currentPath);
|
loadFiles(currentPath);
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
|
initDropzone();
|
||||||
startWatch();
|
startWatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +124,6 @@
|
|||||||
|
|
||||||
function getFileIcon(name, isDir) {
|
function getFileIcon(name, isDir) {
|
||||||
if (isDir) return '📁';
|
if (isDir) return '📁';
|
||||||
|
|
||||||
var ext = name.split('.').pop().toLowerCase();
|
var ext = name.split('.').pop().toLowerCase();
|
||||||
var icons = {
|
var icons = {
|
||||||
pdf: '📕', doc: '📘', docx: '📘',
|
pdf: '📕', doc: '📘', docx: '📘',
|
||||||
@@ -160,7 +154,6 @@
|
|||||||
var date = new Date(timeStr);
|
var date = new Date(timeStr);
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
var isToday = date.toDateString() === now.toDateString();
|
var isToday = date.toDateString() === now.toDateString();
|
||||||
|
|
||||||
if (isToday) {
|
if (isToday) {
|
||||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||||
}
|
}
|
||||||
@@ -178,15 +171,12 @@
|
|||||||
elements.breadcrumb.innerHTML = '<span class="crumb" data-path="/">根目录</span>';
|
elements.breadcrumb.innerHTML = '<span class="crumb" data-path="/">根目录</span>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = path.split('/').filter(function(p) { return p; });
|
var parts = path.split('/').filter(function(p) { return p; });
|
||||||
var html = '<span class="crumb" data-path="/">根目录</span>';
|
var html = '<span class="crumb" data-path="/">根目录</span>';
|
||||||
|
|
||||||
parts.forEach(function(part, index) {
|
parts.forEach(function(part, index) {
|
||||||
var currentPath = '/' + parts.slice(0, index + 1).join('/');
|
var currentPathStr = '/' + parts.slice(0, index + 1).join('/');
|
||||||
html += '<span class="crumb" data-path="' + currentPath + '">' + escapeHtml(part) + '</span>';
|
html += '<span class="crumb" data-path="' + currentPathStr + '">' + escapeHtml(part) + '</span>';
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.breadcrumb.innerHTML = html;
|
elements.breadcrumb.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +187,222 @@
|
|||||||
elements.btnMove.disabled = count !== 1;
|
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() {
|
function setupEventListeners() {
|
||||||
elements.breadcrumb.addEventListener('click', function(e) {
|
elements.breadcrumb.addEventListener('click', function(e) {
|
||||||
if (e.target.classList.contains('crumb')) {
|
if (e.target.classList.contains('crumb')) {
|
||||||
@@ -230,7 +436,6 @@
|
|||||||
} else if (actionBtn) {
|
} else if (actionBtn) {
|
||||||
var action = actionBtn.dataset.action;
|
var action = actionBtn.dataset.action;
|
||||||
var path = actionBtn.dataset.path;
|
var path = actionBtn.dataset.path;
|
||||||
|
|
||||||
if (action === 'download') downloadFile(path);
|
if (action === 'download') downloadFile(path);
|
||||||
if (action === 'preview') previewFile(path);
|
if (action === 'preview') previewFile(path);
|
||||||
if (action === 'rename') showRenameModal(path);
|
if (action === 'rename') showRenameModal(path);
|
||||||
@@ -246,10 +451,22 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
elements.btnUpload.addEventListener('click', function() {
|
elements.btnUpload.addEventListener('click', function() {
|
||||||
elements.fileInput.click();
|
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.btnDownload.addEventListener('click', downloadSelected);
|
||||||
|
|
||||||
elements.btnDelete.addEventListener('click', function() {
|
elements.btnDelete.addEventListener('click', function() {
|
||||||
if (selectedFiles.size > 0 && confirm('确定要删除选中的 ' + selectedFiles.size + ' 个项目吗?')) {
|
if (selectedFiles.size > 0 && confirm('确定要删除选中的 ' + selectedFiles.size + ' 个项目吗?')) {
|
||||||
deleteFiles(Array.from(selectedFiles));
|
deleteFiles(Array.from(selectedFiles));
|
||||||
@@ -272,11 +489,6 @@
|
|||||||
elements.newDirName.focus();
|
elements.newDirName.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.fileInput.addEventListener('change', function(e) {
|
|
||||||
uploadFiles(e.target.files);
|
|
||||||
e.target.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
elements.searchBtn.addEventListener('click', searchFiles);
|
elements.searchBtn.addEventListener('click', searchFiles);
|
||||||
elements.searchInput.addEventListener('keypress', function(e) {
|
elements.searchInput.addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') searchFiles();
|
if (e.key === 'Enter') searchFiles();
|
||||||
@@ -342,7 +554,14 @@
|
|||||||
elements.dropZone.addEventListener('drop', function(e) {
|
elements.dropZone.addEventListener('drop', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
elements.dropZone.classList.remove('active');
|
elements.dropZone.classList.remove('active');
|
||||||
uploadFiles(e.dataTransfer.files);
|
if (uploadDropzone) {
|
||||||
|
uploadDropzone.removeAllFiles(true);
|
||||||
|
e.dataTransfer.files.forEach(function(file) {
|
||||||
|
uploadDropzone.addFile(file);
|
||||||
|
});
|
||||||
|
elements.uploadPath.value = currentPath;
|
||||||
|
elements.uploadModal.classList.add('active');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,21 +609,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
elements.uploadClose.addEventListener('click', function() {
|
elements.uploadClose.addEventListener('click', function() {
|
||||||
if (!isUploading) {
|
elements.uploadModal.classList.remove('active');
|
||||||
elements.uploadModal.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.uploadCancel.addEventListener('click', function() {
|
elements.uploadCloseBtn.addEventListener('click', function() {
|
||||||
if (isUploading) {
|
|
||||||
uploadXhrs.forEach(function(xhr) { xhr.abort(); });
|
|
||||||
uploadXhrs = [];
|
|
||||||
isUploading = false;
|
|
||||||
elements.uploadModal.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
elements.uploadConfirm.addEventListener('click', function() {
|
|
||||||
elements.uploadModal.classList.remove('active');
|
elements.uploadModal.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -413,151 +621,7 @@
|
|||||||
if (e.target === elements.moveModal) elements.moveModal.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.newDirModal) elements.newDirModal.classList.remove('active');
|
||||||
if (e.target === elements.renameModal) elements.renameModal.classList.remove('active');
|
if (e.target === elements.renameModal) elements.renameModal.classList.remove('active');
|
||||||
if (e.target === elements.uploadModal && !isUploading) elements.uploadModal.classList.remove('active');
|
if (e.target === elements.uploadModal) elements.uploadModal.classList.remove('active');
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadFiles(files) {
|
|
||||||
if (!files || files.length === 0) return;
|
|
||||||
|
|
||||||
var fileArray = Array.from(files);
|
|
||||||
|
|
||||||
elements.uploadList.innerHTML = fileArray.map(function(file, index) {
|
|
||||||
return '<div class="upload-item" data-index="' + index + '">' +
|
|
||||||
'<span class="upload-item-icon">' + getFileIcon(file.name, false) + '</span>' +
|
|
||||||
'<div class="upload-item-info">' +
|
|
||||||
'<div class="upload-item-name">' + escapeHtml(file.name) + '</div>' +
|
|
||||||
'<div class="upload-item-status" id="upload-status-' + index + '">' +
|
|
||||||
'<span class="upload-item-size">' + formatFileSize(file.size) + '</span>' +
|
|
||||||
'<span id="upload-progress-text-' + index + '">等待中...</span>' +
|
|
||||||
'</div></div>' +
|
|
||||||
'<div class="upload-item-progress">' +
|
|
||||||
'<div class="upload-item-progress-bar" id="upload-item-progress-' + index + '"></div>' +
|
|
||||||
'</div></div>';
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
elements.uploadProgressContainer.classList.add('active');
|
|
||||||
elements.uploadProgressBar.style.width = '0%';
|
|
||||||
elements.uploadStats.classList.add('active');
|
|
||||||
elements.uploadSpeed.classList.add('active');
|
|
||||||
elements.uploadSpeed.textContent = '准备上传...';
|
|
||||||
elements.uploadCancel.style.display = 'block';
|
|
||||||
elements.uploadConfirm.style.display = 'none';
|
|
||||||
elements.uploadModal.classList.add('active');
|
|
||||||
|
|
||||||
var total = fileArray.length;
|
|
||||||
var totalBytes = fileArray.reduce(function(sum, f) { return sum + f.size; }, 0);
|
|
||||||
var uploadedBytes = 0;
|
|
||||||
var completedCount = 0;
|
|
||||||
uploadXhrs = [];
|
|
||||||
|
|
||||||
var loadedBytesMap = fileArray.map(function() { return 0; });
|
|
||||||
var finishedMap = fileArray.map(function() { return false; });
|
|
||||||
var lastTime = Date.now();
|
|
||||||
var lastLoaded = 0;
|
|
||||||
|
|
||||||
function updateSpeed() {
|
|
||||||
var now = Date.now();
|
|
||||||
var timeDiff = (now - lastTime) / 1000;
|
|
||||||
if (timeDiff < 0.5) return;
|
|
||||||
|
|
||||||
var currentLoaded = loadedBytesMap.reduce(function(sum, bytes, i) {
|
|
||||||
return sum + (finishedMap[i] ? fileArray[i].size : bytes);
|
|
||||||
}, 0);
|
|
||||||
var bytesDiff = currentLoaded - lastLoaded;
|
|
||||||
var speed = bytesDiff / timeDiff;
|
|
||||||
|
|
||||||
if (speed > 0 && completedCount < total) {
|
|
||||||
var remaining = totalBytes - currentLoaded;
|
|
||||||
var eta = remaining / speed;
|
|
||||||
var etaStr;
|
|
||||||
if (eta < 60) etaStr = Math.ceil(eta) + '秒';
|
|
||||||
else if (eta < 3600) etaStr = Math.ceil(eta / 60) + '分钟';
|
|
||||||
else etaStr = Math.ceil(eta / 3600) + '小时';
|
|
||||||
|
|
||||||
elements.uploadSpeed.textContent = formatFileSize(speed) + '/s · 剩余' + etaStr;
|
|
||||||
} else if (completedCount < total) {
|
|
||||||
elements.uploadSpeed.textContent = '处理中...';
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTime = now;
|
|
||||||
lastLoaded = currentLoaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileArray.forEach(function(file, index) {
|
|
||||||
var formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
uploadXhrs.push(xhr);
|
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', function(e) {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
loadedBytesMap[index] = e.loaded;
|
|
||||||
var percent = Math.min(99, Math.round((e.loaded / e.total) * 100));
|
|
||||||
|
|
||||||
document.getElementById('upload-progress-text-' + index).textContent = percent + '%';
|
|
||||||
document.getElementById('upload-item-progress-' + index).style.width = percent + '%';
|
|
||||||
|
|
||||||
var currentLoaded = loadedBytesMap.reduce(function(sum, bytes, i) {
|
|
||||||
return sum + (finishedMap[i] ? fileArray[i].size : bytes);
|
|
||||||
}, 0);
|
|
||||||
var allPercent = Math.min(99, Math.round((currentLoaded / totalBytes) * 100));
|
|
||||||
elements.uploadProgressBar.style.width = allPercent + '%';
|
|
||||||
|
|
||||||
updateSpeed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.addEventListener('load', function() {
|
|
||||||
finishedMap[index] = true;
|
|
||||||
loadedBytesMap[index] = file.size;
|
|
||||||
uploadedBytes += file.size;
|
|
||||||
completedCount++;
|
|
||||||
|
|
||||||
document.getElementById('upload-progress-text-' + index).textContent = '完成';
|
|
||||||
document.getElementById('upload-item-progress-' + index).style.background = '#4caf50';
|
|
||||||
document.getElementById('upload-item-progress-' + index).style.width = '100%';
|
|
||||||
|
|
||||||
var allPercent = Math.round((uploadedBytes / totalBytes) * 100);
|
|
||||||
elements.uploadProgressBar.style.width = allPercent + '%';
|
|
||||||
|
|
||||||
if (completedCount === total) {
|
|
||||||
isUploading = false;
|
|
||||||
elements.uploadCancel.style.display = 'none';
|
|
||||||
elements.uploadConfirm.style.display = 'block';
|
|
||||||
elements.uploadStats.textContent = '上传完成: 成功 ' + completedCount + ' 个';
|
|
||||||
elements.uploadSpeed.textContent = '';
|
|
||||||
loadFiles(currentPath);
|
|
||||||
} else {
|
|
||||||
elements.uploadSpeed.textContent = '处理中... (' + (total - completedCount) + '个文件)';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.addEventListener('error', function() {
|
|
||||||
finishedMap[index] = true;
|
|
||||||
loadedBytesMap[index] = file.size;
|
|
||||||
uploadedBytes += file.size;
|
|
||||||
completedCount++;
|
|
||||||
|
|
||||||
document.getElementById('upload-progress-text-' + index).textContent = '失败';
|
|
||||||
document.getElementById('upload-item-progress-' + index).style.background = '#f44336';
|
|
||||||
|
|
||||||
var allPercent = Math.round((uploadedBytes / totalBytes) * 100);
|
|
||||||
elements.uploadProgressBar.style.width = allPercent + '%';
|
|
||||||
|
|
||||||
if (completedCount === total) {
|
|
||||||
isUploading = false;
|
|
||||||
elements.uploadCancel.style.display = 'none';
|
|
||||||
elements.uploadConfirm.style.display = 'block';
|
|
||||||
elements.uploadStats.textContent = '上传完成: 成功 ' + completedCount + ' 个';
|
|
||||||
elements.uploadSpeed.textContent = '';
|
|
||||||
loadFiles(currentPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.open('POST', API_BASE + '/upload?path=' + encodeURIComponent(currentPath));
|
|
||||||
xhr.send(formData);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,16 +689,13 @@
|
|||||||
|
|
||||||
function renderDirectoryTree(files, currentPath) {
|
function renderDirectoryTree(files, currentPath) {
|
||||||
var dirs = (files || []).filter(function(f) { return f.isDir; });
|
var dirs = (files || []).filter(function(f) { return f.isDir; });
|
||||||
|
|
||||||
if (dirs.length === 0) {
|
if (dirs.length === 0) {
|
||||||
elements.dirTree.innerHTML = '<div class="empty-message"><span>📂</span><p>此目录为空</p></div>';
|
elements.dirTree.innerHTML = '<div class="empty-message"><span>📂</span><p>此目录为空</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.dirTree.innerHTML = dirs.map(function(dir) {
|
elements.dirTree.innerHTML = dirs.map(function(dir) {
|
||||||
return '<div class="dir-item" data-path="' + dir.path + '"><span>📁</span><span>' + escapeHtml(dir.name) + '</span></div>';
|
return '<div class="dir-item" data-path="' + dir.path + '"><span>📁</span><span>' + escapeHtml(dir.name) + '</span></div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
elements.dirTree.querySelectorAll('.dir-item').forEach(function(item) {
|
elements.dirTree.querySelectorAll('.dir-item').forEach(function(item) {
|
||||||
item.addEventListener('click', function() {
|
item.addEventListener('click', function() {
|
||||||
loadDirectoryTree(item.dataset.path);
|
loadDirectoryTree(item.dataset.path);
|
||||||
@@ -647,17 +708,13 @@
|
|||||||
elements.moveBreadcrumb.innerHTML = '<span class="crumb" data-path="/">根目录</span>';
|
elements.moveBreadcrumb.innerHTML = '<span class="crumb" data-path="/">根目录</span>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = path.split('/').filter(function(p) { return p; });
|
var parts = path.split('/').filter(function(p) { return p; });
|
||||||
var html = '<span class="crumb" data-path="/">根目录</span>';
|
var html = '<span class="crumb" data-path="/">根目录</span>';
|
||||||
|
|
||||||
parts.forEach(function(part, index) {
|
parts.forEach(function(part, index) {
|
||||||
var currentPath = '/' + parts.slice(0, index + 1).join('/');
|
var currentPathStr = '/' + parts.slice(0, index + 1).join('/');
|
||||||
html += '<span class="crumb" data-path="' + currentPath + '">' + escapeHtml(part) + '</span>';
|
html += '<span class="crumb" data-path="' + currentPathStr + '">' + escapeHtml(part) + '</span>';
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.moveBreadcrumb.innerHTML = html;
|
elements.moveBreadcrumb.innerHTML = html;
|
||||||
|
|
||||||
elements.moveBreadcrumb.querySelectorAll('.crumb').forEach(function(crumb) {
|
elements.moveBreadcrumb.querySelectorAll('.crumb').forEach(function(crumb) {
|
||||||
crumb.addEventListener('click', function() {
|
crumb.addEventListener('click', function() {
|
||||||
loadDirectoryTree(crumb.dataset.path);
|
loadDirectoryTree(crumb.dataset.path);
|
||||||
@@ -668,17 +725,14 @@
|
|||||||
function moveFile() {
|
function moveFile() {
|
||||||
var sourcePath = elements.moveSource.value;
|
var sourcePath = elements.moveSource.value;
|
||||||
var destPath = elements.moveDest.value;
|
var destPath = elements.moveDest.value;
|
||||||
|
|
||||||
if (!sourcePath || !destPath) {
|
if (!sourcePath || !destPath) {
|
||||||
showNotification('请选择目标位置', 'error');
|
showNotification('请选择目标位置', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourcePath === destPath) {
|
if (sourcePath === destPath) {
|
||||||
showNotification('源文件和目标位置相同', 'error');
|
showNotification('源文件和目标位置相同', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(API_BASE + '/move', {
|
fetch(API_BASE + '/move', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -713,14 +767,11 @@
|
|||||||
function renameFile() {
|
function renameFile() {
|
||||||
var path = elements.renamePath.value;
|
var path = elements.renamePath.value;
|
||||||
var newName = elements.renameName.value.trim();
|
var newName = elements.renameName.value.trim();
|
||||||
|
|
||||||
if (!path || !newName) {
|
if (!path || !newName) {
|
||||||
showNotification('请输入新名称', 'error');
|
showNotification('请输入新名称', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPath = path.substring(0, path.lastIndexOf('/') + 1) + newName;
|
var newPath = path.substring(0, path.lastIndexOf('/') + 1) + newName;
|
||||||
|
|
||||||
fetch(API_BASE + '/rename', {
|
fetch(API_BASE + '/rename', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -751,9 +802,7 @@
|
|||||||
showNotification('请输入文件夹名称', 'error');
|
showNotification('请输入文件夹名称', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = currentPath === '/' ? '/' + name : currentPath + '/' + name;
|
var path = currentPath === '/' ? '/' + name : currentPath + '/' + name;
|
||||||
|
|
||||||
fetch(API_BASE + '/dir', {
|
fetch(API_BASE + '/dir', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -779,7 +828,6 @@
|
|||||||
elements.previewTitle.textContent = name;
|
elements.previewTitle.textContent = name;
|
||||||
elements.previewBody.innerHTML = '<div class="preview-placeholder"><span class="icon">📄</span><p>加载中...</p></div>';
|
elements.previewBody.innerHTML = '<div class="preview-placeholder"><span class="icon">📄</span><p>加载中...</p></div>';
|
||||||
elements.previewModal.classList.add('active');
|
elements.previewModal.classList.add('active');
|
||||||
|
|
||||||
fetch(API_BASE + '/preview?path=' + encodeURIComponent(path))
|
fetch(API_BASE + '/preview?path=' + encodeURIComponent(path))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
var contentType = response.headers.get('Content-Type');
|
var contentType = response.headers.get('Content-Type');
|
||||||
@@ -822,21 +870,17 @@
|
|||||||
eventSource.close();
|
eventSource.close();
|
||||||
eventSource = null;
|
eventSource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
eventSource = new EventSource(API_BASE + '/watch');
|
eventSource = new EventSource(API_BASE + '/watch');
|
||||||
|
|
||||||
eventSource.onopen = function() {
|
eventSource.onopen = function() {
|
||||||
elements.connectionStatus.textContent = '● 已连接';
|
elements.connectionStatus.textContent = '● 已连接';
|
||||||
elements.connectionStatus.className = 'connected';
|
elements.connectionStatus.className = 'connected';
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onerror = function() {
|
eventSource.onerror = function() {
|
||||||
if (eventSource.readyState === EventSource.CLOSED) return;
|
if (eventSource.readyState === EventSource.CLOSED) return;
|
||||||
elements.connectionStatus.textContent = '● 已连接';
|
elements.connectionStatus.textContent = '● 已连接';
|
||||||
elements.connectionStatus.className = 'connected';
|
elements.connectionStatus.className = 'connected';
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.addEventListener('message', function(e) {
|
eventSource.addEventListener('message', function(e) {
|
||||||
try {
|
try {
|
||||||
var event = JSON.parse(e.data);
|
var event = JSON.parse(e.data);
|
||||||
@@ -852,12 +896,9 @@
|
|||||||
function handleWatchEvent(event) {
|
function handleWatchEvent(event) {
|
||||||
var inCurrentDir = currentPath === '/' || event.path.startsWith(currentPath + '/') || event.path === currentPath;
|
var inCurrentDir = currentPath === '/' || event.path.startsWith(currentPath + '/') || event.path === currentPath;
|
||||||
if (!inCurrentDir) return;
|
if (!inCurrentDir) return;
|
||||||
|
|
||||||
var pathParts = event.path.split('/').filter(function(p) { return p; });
|
var pathParts = event.path.split('/').filter(function(p) { return p; });
|
||||||
var eventDir = '/' + pathParts.slice(0, -1).join('/') || '/';
|
var eventDir = '/' + pathParts.slice(0, -1).join('/') || '/';
|
||||||
|
|
||||||
if (eventDir !== currentPath) return;
|
if (eventDir !== currentPath) return;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'create':
|
case 'create':
|
||||||
showNotification('新建文件: ' + event.name, 'info');
|
showNotification('新建文件: ' + event.name, 'info');
|
||||||
@@ -895,9 +936,7 @@
|
|||||||
loadFiles(currentPath);
|
loadFiles(currentPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.fileListBody.innerHTML = '<tr><td colspan="5" class="loading">搜索中...</td></tr>';
|
elements.fileListBody.innerHTML = '<tr><td colspan="5" class="loading">搜索中...</td></tr>';
|
||||||
|
|
||||||
searchAllDirectories('/', keyword.toLowerCase(), []);
|
searchAllDirectories('/', keyword.toLowerCase(), []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -911,7 +950,6 @@
|
|||||||
results.push(file);
|
results.push(file);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var dirs = files.filter(function(f) { return f.isDir; });
|
var dirs = files.filter(function(f) { return f.isDir; });
|
||||||
if (dirs.length > 0) {
|
if (dirs.length > 0) {
|
||||||
var promises = dirs.map(function(dir) {
|
var promises = dirs.map(function(dir) {
|
||||||
@@ -939,7 +977,6 @@
|
|||||||
|
|
||||||
function renderSearchResults(files, keyword) {
|
function renderSearchResults(files, keyword) {
|
||||||
var sortedFiles = files.sort(function(a, b) { return a.name.localeCompare(b.name); });
|
var sortedFiles = files.sort(function(a, b) { return a.name.localeCompare(b.name); });
|
||||||
|
|
||||||
var html = sortedFiles.map(function(file) {
|
var html = sortedFiles.map(function(file) {
|
||||||
return '<tr data-path="' + file.path + '" class="' + (selectedFiles.has(file.path) ? 'selected' : '') + '">' +
|
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-checkbox"><input type="checkbox" class="file-checkbox" data-path="' + file.path + '" ' + (selectedFiles.has(file.path) ? 'checked' : '') + '></td>' +
|
||||||
@@ -953,7 +990,6 @@
|
|||||||
'<button class="action-btn delete" data-action="delete" data-path="' + file.path + '" title="删除">🗑️</button>' +
|
'<button class="action-btn delete" data-action="delete" data-path="' + file.path + '" title="删除">🗑️</button>' +
|
||||||
'</div></td></tr>';
|
'</div></td></tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
elements.fileListBody.innerHTML = html;
|
elements.fileListBody.innerHTML = html;
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
}
|
}
|
||||||
|
|||||||
1
static/dropzone.min.css
vendored
Normal file
1
static/dropzone.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/dropzone.min.js
vendored
Normal file
1
static/dropzone.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@
|
|||||||
<title>📁 文件管理器</title>
|
<title>📁 文件管理器</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📁</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📁</text></svg>">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<link rel="stylesheet" href="/static/dropzone.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -124,16 +125,14 @@
|
|||||||
<button class="modal-close" id="upload-close">×</button>
|
<button class="modal-close" id="upload-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="upload-list" id="upload-list"></div>
|
<form action="/api/upload" class="dropzone" id="upload-dropzone">
|
||||||
<div class="upload-progress-container" id="upload-progress-container">
|
<input type="hidden" name="path" id="upload-path" value="/">
|
||||||
<div class="upload-progress-bar" id="upload-progress-bar"></div>
|
<div class="dz-message">拖拽文件到此处或点击上传</div>
|
||||||
</div>
|
</form>
|
||||||
<div class="upload-stats" id="upload-stats"></div>
|
<div class="upload-file-list" id="upload-file-list"></div>
|
||||||
<div class="upload-speed" id="upload-speed"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" id="upload-cancel" style="display: none;">取消</button>
|
<button class="btn btn-primary" id="upload-close-btn">关闭</button>
|
||||||
<button class="btn btn-primary" id="upload-confirm" style="display: none;">完成</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,8 +159,7 @@
|
|||||||
|
|
||||||
<div class="notification" id="notification"></div>
|
<div class="notification" id="notification"></div>
|
||||||
|
|
||||||
<input type="file" id="file-input" multiple style="display: none;">
|
<script src="/static/dropzone.min.js"></script>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
180
static/style.css
180
static/style.css
@@ -394,13 +394,17 @@ body {
|
|||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 450px;
|
max-width: 600px;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-modal .modal-content {
|
||||||
|
max-width: 650px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-large {
|
.modal-large {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
}
|
}
|
||||||
@@ -777,3 +781,177 @@ body {
|
|||||||
.upload-speed.active {
|
.upload-speed.active {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-file-list {
|
||||||
|
margin-top: 15px;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background: #fafafa;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-filename {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-size {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-progress-wrap {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-percent {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 6px;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: left;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-chunks {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #999;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-upload {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background: #2196f3 !important;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-status {
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-success-mark {
|
||||||
|
color: #4caf50;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-error-mark {
|
||||||
|
color: #f44336;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item.dz-success .dz-success-mark {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item.dz-error .dz-error-mark {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-remove {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-remove:hover {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item .dz-error-message {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #ffebee;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item.dz-error .dz-error-message {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item.dz-error .dz-filename {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone {
|
||||||
|
border: 2px dashed #e0e0e0 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
background: #fafafa !important;
|
||||||
|
min-height: 100px !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone:hover,
|
||||||
|
.dropzone.dz-drag-hover {
|
||||||
|
border-color: #2196f3 !important;
|
||||||
|
background: #f0f7ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone .dz-message {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone .dz-preview {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user