Enhance file manager UI/UX and routing.

Improve layout, styling, file type badges, accessibility, and add cross-panel transfer actions from the context menu.
This commit is contained in:
liumangmang
2026-02-05 17:06:30 +08:00
parent 72641eb7d7
commit 56c40410dc
5 changed files with 494 additions and 26 deletions

View File

@@ -45,12 +45,22 @@ function initApp() {
// 绑定键盘事件
bindKeyboardEvents();
// 默认高亮左侧面板
setActivePanel('left');
// 文件项拖拽(跨面板传输)
initDragAndDrop();
// 面板点击切换活动面板
$('#left-panel').on('click', function() { activePanelId = 'left'; });
$('#right-panel').on('click', function() { activePanelId = 'right'; });
$('#left-panel').on('click', function() { setActivePanel('left'); });
$('#right-panel').on('click', function() { setActivePanel('right'); });
}
// 设置当前活动面板并应用高亮样式
function setActivePanel(panelId) {
activePanelId = panelId;
$('#left-panel, #right-panel').removeClass('active-panel');
$('#' + panelId + '-panel').addClass('active-panel');
}
// 初始化面板路径
@@ -188,11 +198,12 @@ function renderFileList(panelId, files) {
}
});
// 右键菜单:删除
// 右键菜单
item.on('contextmenu', function(e) {
e.preventDefault();
const itemPath = $(this).data('path');
if (itemPath) showContextMenu(e, panelId, itemPath);
const itemIsDir = $(this).data('is-dir');
if (itemPath) showContextMenu(e, panelId, itemPath, itemIsDir);
});
item.attr('draggable', true);
@@ -202,21 +213,27 @@ function renderFileList(panelId, files) {
// 获取文件图标
function getFileIcon(fileName, isDirectory) {
if (isDirectory) return '📁';
if (isDirectory) {
return '<span class="file-type file-type-dir">DIR</span>';
}
const ext = (fileName.substring(fileName.lastIndexOf('.') + 1) || '').toLowerCase();
const iconMap = {
'txt': '📄', 'doc': '📄', 'docx': '📄',
'xls': '📊', 'xlsx': '📊', 'ppt': '📊', 'pptx': '📊',
'pdf': '📕',
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️',
'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬',
'mp3': '🎵', 'wav': '🎵',
'zip': '📦', 'rar': '📦', '7z': '📦',
'java': '☕', 'js': '💻', 'py': '🐍', 'php': '🐘',
'html': '🌐', 'css': '🎨', 'json': '📋'
const typeMap = {
'txt': 'TXT', 'md': 'TXT',
'doc': 'DOC', 'docx': 'DOC',
'xls': 'XLS', 'xlsx': 'XLS',
'ppt': 'PPT', 'pptx': 'PPT',
'pdf': 'PDF',
'jpg': 'IMG', 'jpeg': 'IMG', 'png': 'IMG', 'gif': 'IMG', 'webp': 'IMG',
'mp4': 'VID', 'avi': 'VID', 'mkv': 'VID', 'mov': 'VID',
'mp3': 'AUD', 'wav': 'AUD', 'flac': 'AUD',
'zip': 'ZIP', 'rar': 'ZIP', '7z': 'ZIP',
'java': 'CODE', 'js': 'CODE', 'ts': 'CODE', 'py': 'CODE', 'php': 'CODE', 'go': 'CODE',
'html': 'WEB', 'css': 'WEB',
'json': 'JSON', 'yml': 'YML', 'yaml': 'YML'
};
return iconMap[ext] || '📄';
const label = typeMap[ext] || (ext ? ext.toUpperCase().slice(0, 4) : 'FILE');
return '<span class="file-type">' + label + '</span>';
}
// 格式化文件大小
@@ -515,7 +532,22 @@ function updateConnectionSelect(panelId) {
select.empty().append('<option value="">选择连接</option>');
// 为避免同一主机/端口/用户名的连接在下拉框中重复显示,
// 这里按「username@host:port」进行去重只保留第一条会话记录
const seenKeys = new Set();
$.each(activeConnections, function(sessionId, conn) {
if (!conn) {
return;
}
const dedupKey = (conn.username || '') + '@' +
(conn.host || '') + ':' + (conn.port || 22);
if (seenKeys.has(dedupKey)) {
return; // 已有同一连接信息,跳过重复项
}
seenKeys.add(dedupKey);
const name = (conn && (conn.name || conn.host)) ? (conn.name || conn.host) : ('会话 ' + (sessionId || '').substring(0, 13));
select.append('<option value="' + sessionId + '">' + $('<div>').text(name).html() + '</option>');
});
@@ -1255,7 +1287,7 @@ function performDelete(sessionId, paths, panelId) {
}
// 文件列表右键菜单
function showContextMenu(event, panelId, path) {
function showContextMenu(event, panelId, path, isDirectory) {
$('.context-menu').remove();
const menu = $('<div class="context-menu dropdown-menu show"></div>');
menu.css({
@@ -1264,6 +1296,29 @@ function showContextMenu(event, panelId, path) {
top: event.clientY + 'px',
zIndex: 1050
});
// 跨面板传输(根据当前面板添加对应方向的菜单项)
if (panelId === 'left') {
const transferRightItem = $('<a class="dropdown-item" href="#">传输到右侧</a>');
transferRightItem.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
menu.remove();
transferSingleBetweenPanels('left', 'right', path, isDirectory);
});
menu.append(transferRightItem);
} else if (panelId === 'right') {
const transferLeftItem = $('<a class="dropdown-item" href="#">传输到左侧</a>');
transferLeftItem.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
menu.remove();
transferSingleBetweenPanels('right', 'left', path, isDirectory);
});
menu.append(transferLeftItem);
}
menu.append('<div class="dropdown-divider"></div>');
const deleteItem = $('<a class="dropdown-item text-danger" href="#">删除</a>');
deleteItem.on('click', function(e) {
e.preventDefault();
@@ -1297,6 +1352,60 @@ function showContextMenu(event, panelId, path) {
});
}
// 右键菜单:单个文件跨面板传输
function transferSingleBetweenPanels(sourcePanelId, targetPanelId, path, isDirectory) {
if (!path) {
return;
}
const sourceSessionId = panelState[sourcePanelId].sessionId;
const targetSessionId = panelState[targetPanelId].sessionId;
const targetPath = panelState[targetPanelId].currentPath;
if (!targetPath) {
alert('目标面板路径无效,请先选择目标目录');
return;
}
const fileName = getFileNameFromPath(path);
const isDir = isDirectory === true || isDirectory === 'true' || isDirectory === 'True';
const recursive = isDir; // 若为目录,则递归传输
showTransferCountProgress(0, 1, fileName);
updateTransferProgress(0, '传输中 ' + (fileName || ''), true); // 无流式进度,使用动画条
$.ajax({
url: API_BASE + 'api/files/transfer',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
sourceSessionId: sourceSessionId,
sourcePaths: [path],
targetSessionId: targetSessionId,
targetPath: targetPath,
recursive: recursive
}),
success: function(response) {
showTransferProgress(false);
if (response.success) {
updateStatus('传输成功');
} else {
alert('传输失败: ' + (response.message || '未知错误'));
updateStatus('传输失败');
}
loadFiles(targetPanelId);
},
error: function(xhr, status, error) {
showTransferProgress(false);
const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
alert('传输失败: ' + errMsg);
updateStatus('传输失败');
}
});
updateStatus('正在传输 "' + (fileName || path) + '" ...');
}
// 右键菜单:删除单个文件
function deleteFileByPath(panelId, path) {
const sessionId = panelState[panelId].sessionId;