// SFTP Manager 前端业务逻辑 // API 基础路径(使用相对路径以支持 context-path) const API_BASE = ''; // 面板状态 const panelState = { left: { mode: 'local', sessionId: 'local', currentPath: '', selectedFiles: [] }, right: { mode: 'local', sessionId: 'local', currentPath: '', selectedFiles: [] } }; // 活跃连接列表 { sessionId: Connection } let activeConnections = {}; // 当前活动面板(用于快捷键) let activePanelId = 'left'; // 是否展示隐藏文件(默认不展示) let showHiddenFiles = false; // 初始化 $(document).ready(function() { initApp(); }); // 应用初始化 function initApp() { // 获取初始路径并加载文件 initPanelPath('left'); initPanelPath('right'); // 加载活跃连接 loadActiveConnections(); // 绑定键盘事件 bindKeyboardEvents(); // 文件项拖拽(跨面板传输) initDragAndDrop(); // 面板点击切换活动面板 $('#left-panel').on('click', function() { activePanelId = 'left'; }); $('#right-panel').on('click', function() { activePanelId = 'right'; }); } // 初始化面板路径 function initPanelPath(panelId) { const sessionId = panelState[panelId].sessionId; updateStatus(`正在加载${panelId === 'left' ? '左' : '右'}面板...`); $.ajax({ url: API_BASE + 'api/files/path', method: 'GET', data: { sessionId: sessionId }, success: function(response) { if (response.success && response.data && response.data.path) { panelState[panelId].currentPath = response.data.path; loadFiles(panelId); } else { $(`#${panelId}-file-list`).html('
无法获取路径
'); updateStatus('就绪'); } }, error: function(xhr, status, error) { $(`#${panelId}-file-list`).html('
加载失败: ' + error + '
'); updateStatus('错误: ' + error); } }); } // 加载文件列表 function loadFiles(panelId) { const sessionId = panelState[panelId].sessionId; const path = panelState[panelId].currentPath; if (!path) { initPanelPath(panelId); return; } updateStatus(`正在加载 ${panelId === 'left' ? '左' : '右'}面板文件列表...`); $.ajax({ url: API_BASE + 'api/files/list', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sessionId: sessionId, path: path, showHidden: showHiddenFiles }), success: function(response) { if (response.success) { renderFileList(panelId, response.data); updatePathInput(panelId, path); updateStatus('就绪'); } else { $(`#${panelId}-file-list`).html('
' + (response.message || '加载失败') + '
'); updateStatus('错误: ' + (response.message || '加载失败')); } }, error: function(xhr, status, error) { const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error; $(`#${panelId}-file-list`).html('
加载失败: ' + errMsg + '
'); updateStatus('错误: ' + errMsg); } }); } // 排序文件列表:文件夹在前,同类型按名称(不区分大小写) function sortFileList(files) { if (!Array.isArray(files) || files.length === 0) return files; return files.slice().sort(function(a, b) { const aDir = a.isDirectory === true || a.isDirectory === 'true' || a.directory === true || a.directory === 'true'; const bDir = b.isDirectory === true || b.isDirectory === 'true' || b.directory === true || b.directory === 'true'; if (aDir !== bDir) return aDir ? -1 : 1; const nameA = (a.name || '').toLowerCase(); const nameB = (b.name || '').toLowerCase(); return nameA.localeCompare(nameB, 'zh-CN', { sensitivity: 'base' }); }); } // 渲染文件列表(仅当 data 为数组时渲染,避免错误数据结构被当成文件显示) function renderFileList(panelId, files) { const fileList = $(`#${panelId}-file-list`); fileList.empty(); if (!Array.isArray(files)) { fileList.html('
数据格式错误,请刷新重试
'); return; } if (files.length === 0) { fileList.html('
文件夹为空
'); return; } files = sortFileList(files); files.forEach(function(file) { // 确保 isDirectory 字段存在且为布尔值 // 兼容多种可能的字段名和值类型 const isDir = file.isDirectory === true || file.isDirectory === 'true' || file.directory === true || file.directory === 'true'; const icon = getFileIcon(file.name, isDir); const size = isDir ? '' : formatFileSize(file.size); const date = formatDate(file.modifiedTime); const escapedPath = $('
').text(file.path).html(); const escapedName = $('
').text(file.name).html(); const item = $('
' + '' + icon + '' + '' + escapedName + '' + '' + size + '' + '' + date + '' + '
'); // 单击选择 item.on('click', function(e) { if (!e.ctrlKey) { fileList.find('.file-item').removeClass('selected'); } item.toggleClass('selected'); updateSelectedFiles(panelId); }); // 双击进入目录 item.on('dblclick', function(e) { e.stopPropagation(); const itemPath = $(this).data('path'); const itemIsDir = $(this).data('is-dir'); // 处理布尔值和字符串形式的布尔值 const isDirectory = itemIsDir === true || itemIsDir === 'true' || itemIsDir === 'True'; if (isDirectory && itemPath) { enterDirectory(panelId, itemPath); } }); // 右键菜单:删除 item.on('contextmenu', function(e) { e.preventDefault(); const itemPath = $(this).data('path'); if (itemPath) showContextMenu(e, panelId, itemPath); }); item.attr('draggable', true); fileList.append(item); }); } // 获取文件图标 function getFileIcon(fileName, isDirectory) { if (isDirectory) return '📁'; 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': '📋' }; return iconMap[ext] || '📄'; } // 格式化文件大小 function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (Math.round(bytes / Math.pow(k, i) * 100) / 100) + ' ' + sizes[i]; } // 格式化日期 function formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } // 进入目录 function enterDirectory(panelId, path) { panelState[panelId].currentPath = path; loadFiles(panelId); } // 返回上级目录 function goUp(panelId) { const currentPath = panelState[panelId].currentPath; const parentPath = getParentPath(currentPath); if (parentPath && parentPath !== currentPath) { panelState[panelId].currentPath = parentPath; loadFiles(panelId); } else { updateStatus('已是根目录'); } } // 获取父路径(兼容 Windows 和 Unix) function getParentPath(path) { if (!path) return null; path = path.replace(/\/$/, ''); const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); if (idx <= 0) return path; return path.substring(0, idx) || path.substring(0, idx + 1); } // 更新选中文件列表 function updateSelectedFiles(panelId) { const selected = $(`#${panelId}-file-list .file-item.selected`); panelState[panelId].selectedFiles = selected.map(function() { return { path: $(this).data('path'), isDirectory: $(this).data('is-dir') === true || $(this).data('is-dir') === 'true' || $(this).data('is-dir') === 'True' }; }).get(); } // 全选 function selectAll(panelId) { $(`#${panelId}-file-list .file-item`).addClass('selected'); updateSelectedFiles(panelId); } // 取消选择 function deselectAll(panelId) { $(`#${panelId}-file-list .file-item`).removeClass('selected'); updateSelectedFiles(panelId); } // 反选 function invertSelection(panelId) { $(`#${panelId}-file-list .file-item`).toggleClass('selected'); updateSelectedFiles(panelId); } // 文件项拖拽初始化(跨面板拖放传输) function initDragAndDrop() { $(document).on('dragstart', '.file-item', function(e) { const panelId = $(this).closest('.panel').attr('id').replace('-panel', ''); const path = $(this).data('path'); e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ panelId: panelId, path: path })); e.originalEvent.dataTransfer.effectAllowed = 'copyMove'; $(this).addClass('dragging'); }); $(document).on('dragend', '.file-item', function() { $(this).removeClass('dragging'); }); $('#left-file-list, #right-file-list').on('dragover', function(e) { e.preventDefault(); e.originalEvent.dataTransfer.dropEffect = 'copy'; $(this).addClass('drag-over'); }); $('#left-file-list, #right-file-list').on('dragleave', function(e) { if (!$(this).find(e.relatedTarget).length) { $(this).removeClass('drag-over'); } }); $('#left-file-list, #right-file-list').on('drop', function(e) { e.preventDefault(); $(this).removeClass('drag-over'); try { const data = JSON.parse(e.originalEvent.dataTransfer.getData('text/plain')); const targetPanelId = $(this).closest('.panel').attr('id').replace('-panel', ''); handleFileDrop(data, targetPanelId); } catch (err) { console.error('拖拽失败:', err); } }); } // 显示跨面板传输进度(按文件个数) function showTransferCountProgress(current, total, fileName) { const label = total > 1 ? '传输中 (' + current + '/' + total + ')' + (fileName ? ' ' + fileName : '') : '传输中 ' + (fileName || ''); const percent = total > 0 ? Math.round((current / total) * 100) : 0; showTransferProgress(true, label); updateTransferProgress(percent, label); } // 处理文件拖放(同面板移动 / 跨面板传输) function handleFileDrop(data, targetPanelId) { const sourcePanelId = data.panelId; const sourcePath = data.path; const targetPath = panelState[targetPanelId].currentPath; if (sourcePanelId === targetPanelId) { updateStatus('同面板内移动功能开发中,请使用「传输到右侧」或「传输到左侧」按钮'); return; } const sourceSessionId = panelState[sourcePanelId].sessionId; const targetSessionId = panelState[targetPanelId].sessionId; const fileName = getFileNameFromPath(sourcePath); 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: [sourcePath], targetSessionId: targetSessionId, targetPath: targetPath, recursive: true }), success: function(response) { showTransferProgress(false); if (response.success) { updateStatus('传输成功'); loadFiles(targetPanelId); } else { alert('传输失败: ' + (response.message || '未知错误')); } }, error: function(xhr, status, error) { showTransferProgress(false); const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error; alert('传输失败: ' + errMsg); } }); } // 更新路径输入框 function updatePathInput(panelId, path) { $(`#${panelId}-path`).val(path || ''); } // 更新状态栏 function updateStatus(text) { $('#status-text').text(text || '就绪'); } // 模式切换(本地 / SFTP) function onModeChange(panelId) { const select = $(`#${panelId}-mode`); const connSelect = $(`#${panelId}-connection`); const statusDiv = $(`#${panelId}-status`); const mode = select.val(); panelState[panelId].mode = mode; panelState[panelId].selectedFiles = []; if (mode === 'local') { panelState[panelId].sessionId = 'local'; connSelect.hide(); statusDiv.hide(); initPanelPath(panelId); updateStatus('已切换到本地模式'); } else { connSelect.show(); updateConnectionSelect(panelId); const sessionId = connSelect.val(); if (sessionId) { panelState[panelId].sessionId = sessionId; initPanelPath(panelId); showConnectionStatus(panelId, sessionId); updateStatus('已切换到SFTP模式'); } else { $(`#${panelId}-file-list`).html('
请选择SFTP连接或从连接管理中连接
'); showConnectionStatus(panelId, null); updateStatus('请选择SFTP连接'); } } } // 连接切换 function onConnectionChange(panelId) { const sessionId = $(`#${panelId}-connection`).val(); panelState[panelId].sessionId = sessionId || 'local'; if (sessionId) { initPanelPath(panelId); showConnectionStatus(panelId, sessionId); } else { $(`#${panelId}-file-list`).html('
请选择SFTP连接
'); showConnectionStatus(panelId, null); } } // 更新连接状态显示(状态:connected / disconnected / connecting) function updateConnectionStatus(panelId, status, text) { const statusDiv = $(`#${panelId}-status`); if (statusDiv.length === 0) return; const statusDot = statusDiv.find('.status-dot'); const statusText = statusDiv.find('.status-text'); statusDiv.removeClass('connected disconnected connecting').addClass(status); statusDot.attr('data-status', status); statusText.text(text || ''); } // 根据 sessionId 显示/隐藏连接状态 function showConnectionStatus(panelId, sessionId) { const statusDiv = $(`#${panelId}-status`); if (statusDiv.length === 0) return; if (panelState[panelId].mode === 'local') { statusDiv.hide(); return; } statusDiv.show(); if (sessionId && activeConnections[sessionId]) { const conn = activeConnections[sessionId]; // 仅显示「已连接」,具体连接名由下拉框展示,避免与下拉框重复显示 IP/名称 updateConnectionStatus(panelId, 'connected', '已连接'); } else { updateConnectionStatus(panelId, 'disconnected', '未连接'); } } // 加载活跃连接 function loadActiveConnections() { $.ajax({ url: API_BASE + 'api/connection/active', method: 'GET', success: function(response) { if (response.success && response.data) { activeConnections = response.data; updateConnectionSelect('left'); updateConnectionSelect('right'); ['left', 'right'].forEach(function(panelId) { if (panelState[panelId].mode === 'sftp') { showConnectionStatus(panelId, panelState[panelId].sessionId); } }); } } }); } // 更新连接下拉框(优先显示连接名或主机,避免暴露 sftp-uuid) // 重建选项后会恢复当前面板的选中值,保证左右面板可独立选择不同 SFTP 连接 function updateConnectionSelect(panelId) { const select = $(`#${panelId}-connection`); const currentSessionId = panelState[panelId] && panelState[panelId].sessionId; select.empty().append(''); $.each(activeConnections, function(sessionId, conn) { const name = (conn && (conn.name || conn.host)) ? (conn.name || conn.host) : ('会话 ' + (sessionId || '').substring(0, 13)); select.append('