Files
sftp-manager/src/main/resources/static/js/app.js

1499 lines
51 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.
// 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('<div class="text-center text-muted p-3">无法获取路径</div>');
updateStatus('就绪');
}
},
error: function(xhr, status, error) {
$(`#${panelId}-file-list`).html('<div class="text-center text-danger p-3">加载失败: ' + error + '</div>');
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('<div class="text-center text-danger p-3">' + (response.message || '加载失败') + '</div>');
updateStatus('错误: ' + (response.message || '加载失败'));
}
},
error: function(xhr, status, error) {
const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
$(`#${panelId}-file-list`).html('<div class="text-center text-danger p-3">加载失败: ' + errMsg + '</div>');
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('<div class="text-center text-danger p-3">数据格式错误,请刷新重试</div>');
return;
}
if (files.length === 0) {
fileList.html('<div class="text-center text-muted p-3">文件夹为空</div>');
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 = $('<div>').text(file.path).html();
const escapedName = $('<div>').text(file.name).html();
const item = $('<div class="file-item" data-path="' + escapedPath + '" data-is-dir="' + isDir + '">' +
'<span class="file-icon">' + icon + '</span>' +
'<span class="file-name" title="' + escapedName + '">' + escapedName + '</span>' +
'<span class="file-size">' + size + '</span>' +
'<span class="file-date">' + date + '</span>' +
'</div>');
// 单击选择
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('<div class="text-center text-muted p-3">请选择SFTP连接或从连接管理中连接</div>');
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('<div class="text-center text-muted p-3">请选择SFTP连接</div>');
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('<option value="">选择连接</option>');
$.each(activeConnections, function(sessionId, conn) {
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>');
});
// 恢复当前面板的选中连接,避免左右两侧被同步成同一个
if (currentSessionId && activeConnections[currentSessionId]) {
select.val(currentSessionId);
} else {
select.val('');
}
}
// 刷新面板(先更新活跃连接,再加载左右面板文件,避免竞态和错误数据)
function refreshPanels() {
$.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);
}
});
}
loadFiles('left');
loadFiles('right');
updateStatus('已刷新');
},
error: function() {
loadFiles('left');
loadFiles('right');
updateStatus('已刷新');
}
});
}
// 切换是否展示隐藏文件
function toggleShowHidden() {
showHiddenFiles = !showHiddenFiles;
const btn = $('#btn-show-hidden');
if (showHiddenFiles) {
btn.removeClass('btn-outline-secondary').addClass('btn-secondary');
btn.text('隐藏文件已显示');
} else {
btn.removeClass('btn-secondary').addClass('btn-outline-secondary');
btn.text('显示隐藏文件');
}
loadFiles('left');
loadFiles('right');
updateStatus(showHiddenFiles ? '已显示隐藏文件' : '已隐藏隐藏文件');
}
// 键盘事件
function bindKeyboardEvents() {
$(document).on('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'Backspace') {
goUp(activePanelId);
e.preventDefault();
}
if (e.key === 'F5') {
refreshPanels();
e.preventDefault();
}
if (e.key === 'Escape') {
deselectAll(activePanelId);
e.preventDefault();
}
// Ctrl+A: 全选
if (e.ctrlKey && (e.key === 'a' || e.key === 'A')) {
selectAll(activePanelId);
e.preventDefault();
}
if (e.key === 'Delete') {
deleteSelectedFiles(activePanelId);
e.preventDefault();
}
if (e.key === 'F2') {
showRenameDialog();
e.preventDefault();
}
// Ctrl+Shift+N: 新建文件夹
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
showMkdirDialog(activePanelId);
e.preventDefault();
}
});
}
// 获取当前活动面板ID
function getActivePanelId() {
return activePanelId;
}
// 刷新当前面板(删除后刷新列表)
function refreshCurrentPanel() {
loadFiles(activePanelId);
}
// 从路径获取文件名
function getFileNameFromPath(path) {
if (!path) return '';
const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
if (idx === -1) return path;
return path.substring(idx + 1);
}
// ========== 连接管理模块03==========
function ensureBootstrap() {
if (typeof bootstrap === 'undefined') {
alert('Bootstrap 未加载,请检查网络连接或暂时关闭广告拦截器后刷新页面');
return false;
}
return true;
}
function showConnectionDialog() {
loadConnectionList();
if (!ensureBootstrap()) return;
new bootstrap.Modal(document.getElementById('connectionModal')).show();
}
function loadConnectionList() {
$.ajax({
url: API_BASE + 'api/connection/list',
method: 'GET',
success: function(response) {
const list = $('#connection-list');
list.empty();
if (response.success && response.data && response.data.length > 0) {
response.data.forEach(function(conn) {
const activeSession = findSessionByConnectionId(conn.id);
const connName = $('<div>').text(conn.name || '').html();
const connInfo = (conn.host || '') + ':' + (conn.port || 22);
let actions = '';
if (activeSession) {
const sessionId = activeSession[0];
actions = '<button class="btn btn-sm btn-warning" onclick="disconnectFromSftp(\'' + sessionId + '\')">断开</button>' +
'<button class="btn btn-sm btn-danger" onclick="deleteConnection(' + conn.id + ')">删除</button>';
} else {
actions = '<button class="btn btn-sm btn-primary" onclick="connectToServer(' + conn.id + ')">连接</button>' +
'<button class="btn btn-sm btn-danger" onclick="deleteConnection(' + conn.id + ')">删除</button>';
}
list.append(
'<div class="connection-item" data-id="' + conn.id + '">' +
'<div><span class="connection-name">' + connName + '</span>' +
'<div class="connection-info">' + connInfo + '</div></div>' +
'<div class="connection-actions">' + actions + '</div></div>'
);
});
} else {
list.html('<div class="text-muted p-3">暂无已保存的连接,请添加连接</div>');
}
}
});
}
// 根据连接 ID 查找活跃会话 [sessionId, conn]
function findSessionByConnectionId(connectionId) {
const id = Number(connectionId);
const entries = Object.entries(activeConnections || {});
for (let i = 0; i < entries.length; i++) {
const sessionId = entries[i][0];
const conn = entries[i][1];
if (conn && (conn.id === id || conn.id === connectionId)) {
return [sessionId, conn];
}
}
return null;
}
// 删除已保存的连接
function deleteConnection(id) {
if (!confirm('确定要删除此连接吗?')) return;
$.ajax({
url: API_BASE + 'api/connection/' + id,
method: 'DELETE',
success: function(response) {
if (response.success) {
updateStatus('删除成功');
loadConnectionList();
loadActiveConnections();
updateConnectionSelect('left');
updateConnectionSelect('right');
} else {
alert('删除失败: ' + (response.message || '未知错误'));
}
},
error: function(xhr) {
alert('删除失败: ' + (xhr.responseJSON && xhr.responseJSON.message || xhr.statusText));
}
});
}
// 断开 SFTP 连接
function disconnectFromSftp(sessionId) {
updateStatus('正在断开连接...');
$.ajax({
url: API_BASE + 'api/connection/disconnect',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ sessionId: sessionId }),
success: function(response) {
if (response.success) {
delete activeConnections[sessionId];
updatePanelsAfterDisconnect(sessionId);
updateConnectionSelect('left');
updateConnectionSelect('right');
['left', 'right'].forEach(function(panelId) {
showConnectionStatus(panelId, panelState[panelId].sessionId);
});
loadConnectionList();
updateStatus('已断开连接');
} else {
alert('断开失败: ' + (response.message || '未知错误'));
}
},
error: function(xhr) {
alert('断开失败: ' + (xhr.responseJSON && xhr.responseJSON.message || xhr.statusText));
}
});
}
// 断开连接后更新使用该会话的面板为本地模式
function updatePanelsAfterDisconnect(sessionId) {
['left', 'right'].forEach(function(panelId) {
if (panelState[panelId].sessionId === sessionId) {
panelState[panelId].sessionId = 'local';
panelState[panelId].mode = 'local';
panelState[panelId].currentPath = '';
$('#' + panelId + '-mode').val('local');
$('#' + panelId + '-connection').hide().val('');
$('#' + panelId + '-status').hide();
initPanelPath(panelId);
}
});
}
// 连接成功后只更新当前活动面板为新连接,另一侧保持原选择,便于左右选不同 SFTP
function updatePanelStateWithConnection(sessionId, conn) {
const panelId = activePanelId;
if (panelState[panelId].mode === 'sftp') {
panelState[panelId].sessionId = sessionId;
$('#' + panelId + '-connection').val(sessionId);
initPanelPath(panelId);
showConnectionStatus(panelId, sessionId);
}
// 另一侧保持原有连接选择不变
}
function showAddConnectionDialog() {
document.getElementById('connection-form').reset();
if (!ensureBootstrap()) return;
new bootstrap.Modal(document.getElementById('addConnectionModal')).show();
}
function saveConnection() {
const form = document.getElementById('connection-form');
const data = {
name: form.name.value,
host: form.host.value,
port: parseInt(form.port.value, 10) || 22,
username: form.username.value,
password: form.password.value || null,
privateKeyPath: form.privateKeyPath.value || null,
passPhrase: form.passPhrase.value || null
};
$.ajax({
url: API_BASE + 'api/connection/save',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function(response) {
if (response.success) {
if (typeof bootstrap !== 'undefined') {
bootstrap.Modal.getInstance(document.getElementById('addConnectionModal')).hide();
}
loadConnectionList();
} else {
alert('保存失败: ' + (response.message || '未知错误'));
}
},
error: function(xhr) {
alert('保存失败: ' + (xhr.responseJSON && xhr.responseJSON.message || xhr.statusText));
}
});
}
function connectToServer(connId) {
$.ajax({
url: API_BASE + 'api/connection/' + connId,
method: 'GET',
success: function(response) {
if (response.success && response.data) {
const conn = response.data;
if (findSessionByConnectionId(conn.id)) {
alert('该连接已在使用中');
return;
}
updateStatus('正在连接 ' + (conn.name || conn.host) + '...');
$.ajax({
url: API_BASE + 'api/connection/connect',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
host: conn.host,
port: conn.port,
username: conn.username,
password: conn.password,
privateKeyPath: conn.privateKeyPath,
passPhrase: conn.passPhrase
}),
success: function(res) {
if (res.success) {
const sessionId = res.data;
activeConnections[sessionId] = conn;
updateConnectionSelect('left');
updateConnectionSelect('right');
updatePanelStateWithConnection(sessionId, conn);
loadConnectionList();
if (typeof bootstrap !== 'undefined') {
bootstrap.Modal.getInstance(document.getElementById('connectionModal')).hide();
}
updateStatus('已连接到 ' + (conn.name || conn.host));
} else {
alert('连接失败: ' + (res.message || '未知错误'));
updateStatus('连接失败');
}
},
error: function(xhr) {
alert('连接失败: ' + (xhr.responseJSON && xhr.responseJSON.message || xhr.statusText));
updateStatus('连接失败');
}
});
} else {
alert('连接不存在');
}
},
error: function(xhr) {
alert('获取连接失败: ' + (xhr.responseJSON && xhr.responseJSON.message || xhr.statusText));
}
});
}
// ========== 文件上传下载功能模块05==========
// 获取目标面板ID当前活动面板
function getTargetPanelId() {
return activePanelId;
}
// 获取源面板ID当前活动面板
function getSourcePanelId() {
return activePanelId;
}
// 上传文件
function uploadFiles() {
document.getElementById('file-input').click();
}
// 初始化文件上传功能
$(document).ready(function() {
// 文件选择
$('#file-input').on('change', function(e) {
const files = e.target.files;
if (files && files.length > 0) {
uploadFilesToTarget(files);
}
// 清空input以便可以重复选择同一文件
$(this).val('');
});
// 拖拽上传 - 为每个面板添加拖拽支持
['left', 'right'].forEach(function(panelId) {
const panel = $(`#${panelId}-panel`);
panel.on('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
$(this).addClass('drag-over');
});
panel.on('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
$(this).removeClass('drag-over');
});
panel.on('drop', function(e) {
e.preventDefault();
e.stopPropagation();
$(this).removeClass('drag-over');
const files = e.originalEvent.dataTransfer.files;
if (files && files.length > 0) {
activePanelId = panelId;
uploadFilesToTarget(files);
}
});
});
});
// 上传文件到目标面板
function uploadFilesToTarget(files) {
const targetPanelId = getTargetPanelId();
const targetSessionId = panelState[targetPanelId].sessionId;
const targetPath = panelState[targetPanelId].currentPath;
if (!targetPath) {
alert('请先选择目标目录');
return;
}
Array.from(files).forEach(function(file) {
uploadSingleFile(file, targetSessionId, targetPath, targetPanelId);
});
}
// 上传单个文件
function uploadSingleFile(file, targetSessionId, targetPath, targetPanelId) {
const formData = new FormData();
formData.append('file', file);
formData.append('targetSessionId', targetSessionId);
formData.append('targetPath', targetPath);
showTransferProgress(true, '上传: ' + file.name + ' 0%');
updateTransferProgress(0, '上传: ' + file.name + ' 0%');
$.ajax({
url: API_BASE + 'api/files/upload',
method: 'POST',
data: formData,
processData: false,
contentType: false,
xhr: function() {
const xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
updateTransferProgress(percent, '上传: ' + file.name + ' ' + percent + '%');
}
});
return xhr;
},
success: function(response) {
if (response.success) {
updateStatus(file.name + ' 上传成功');
loadFiles(targetPanelId);
} else {
alert('上传失败: ' + (response.message || '未知错误'));
}
},
error: function(xhr, status, error) {
const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
alert('上传失败: ' + errMsg);
},
complete: function() {
showTransferProgress(false);
}
});
}
// 显示/隐藏传输进度区域(上传、下载、跨面板传输共用)
function showTransferProgress(show, label) {
const progressDiv = $('#transfer-progress');
progressDiv.css('display', show ? 'inline-flex' : 'none');
if (label !== undefined) {
$('#transfer-progress-label').text(label || '');
}
}
// 更新传输进度条与标签
// indeterminate: 当后端无流式进度时(如跨面板传输 API设为 true 显示动画进度条
function updateTransferProgress(percent, label, indeterminate) {
const progressBar = $('#transfer-progress-bar');
if (indeterminate) {
progressBar.css('width', '100%');
progressBar.text('');
progressBar.addClass('progress-bar-striped progress-bar-animated');
} else {
progressBar.removeClass('progress-bar-striped progress-bar-animated');
progressBar.css('width', percent + '%');
progressBar.text(percent + '%');
}
if (label !== undefined) {
$('#transfer-progress-label').text(label);
}
}
// 显示上传进度(兼容旧调用)
function showUploadProgress(show) {
showTransferProgress(show);
}
// 更新上传进度(带文件名)
function updateUploadProgress(percent, fileName) {
const label = fileName ? '上传: ' + fileName + ' ' + percent + '%' : percent + '%';
updateTransferProgress(percent, label);
}
// 下载文件(带进度,使用 XHR
function downloadFile(sessionId, path) {
const url = API_BASE + 'api/files/download?sessionId=' +
encodeURIComponent(sessionId) +
'&path=' + encodeURIComponent(path);
const fileName = getFileNameFromPath(path) || '下载';
showTransferProgress(true, '下载: ' + fileName + ' 0%');
updateTransferProgress(0, '下载: ' + fileName + ' 0%');
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('progress', function(e) {
if (e.lengthComputable && e.total > 0) {
const percent = Math.round((e.loaded / e.total) * 100);
updateTransferProgress(percent, '下载: ' + fileName + ' ' + percent + '%');
} else if (e.loaded > 0) {
const kb = (e.loaded / 1024).toFixed(1);
updateTransferProgress(0, '下载: ' + fileName + ' 已接收 ' + kb + ' KB');
}
});
xhr.addEventListener('load', function() {
showTransferProgress(false);
if (xhr.status !== 200) {
alert('下载失败: ' + (xhr.statusText || 'HTTP ' + xhr.status));
return;
}
const blob = new Blob([xhr.response]);
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(blobUrl);
updateStatus(fileName + ' 下载完成');
});
xhr.addEventListener('error', function() {
showTransferProgress(false);
alert('下载失败: 网络错误');
});
xhr.send();
}
// 下载选中的文件
function downloadFiles() {
const panelId = getSourcePanelId();
const selectedFiles = panelState[panelId].selectedFiles || [];
const sessionId = panelState[panelId].sessionId;
if (selectedFiles.length === 0) {
alert('请先选择要下载的文件');
return;
}
selectedFiles.forEach(function(item) {
if (!item || !item.path) return;
if (item.isDirectory) {
if (!confirm('当前选中包含文件夹,下载时将递归包含其所有子目录和文件,可能耗时较长,是否继续?')) {
return;
}
}
downloadFile(sessionId, item.path);
});
}
// 按方向执行跨面板传输(源面板 -> 目标面板)
function doTransfer(sourcePanelId, targetPanelId) {
const sourceSessionId = panelState[sourcePanelId].sessionId;
const targetSessionId = panelState[targetPanelId].sessionId;
const targetPath = panelState[targetPanelId].currentPath;
const selectedFiles = panelState[sourcePanelId].selectedFiles || [];
if (selectedFiles.length === 0) {
alert('请在' + (sourcePanelId === 'left' ? '左侧' : '右侧') + '面板选择要传输的文件');
return;
}
if (!targetPath) {
alert('目标面板路径无效,请先选择目标目录');
return;
}
let completed = 0;
let failed = 0;
const total = selectedFiles.length;
showTransferCountProgress(0, total, '');
updateTransferProgress(0, '传输中 (0/' + total + ')', true); // 后端无流式进度,使用动画条
const sourcePaths = selectedFiles
.filter(function(item) { return item && item.path; })
.map(function(item) { return item.path; });
const hasDirectory = selectedFiles.some(function(item) { return item && item.isDirectory; });
const recursive = hasDirectory; // 若包含目录,则自动递归
$.ajax({
url: API_BASE + 'api/files/transfer',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
sourceSessionId: sourceSessionId,
sourcePaths: sourcePaths,
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('正在传输 ' + total + ' 个项目...');
}
// 传输到右侧:左侧面板选中的文件 -> 右侧面板
function transferToRight() {
doTransfer('left', 'right');
}
// 传输到左侧:右侧面板选中的文件 -> 左侧面板
function transferToLeft() {
doTransfer('right', 'left');
}
// ========== 文件删除功能模块06==========
// 工具栏删除:删除当前活动面板中选中的文件
function deleteFiles() {
deleteSelectedFiles(activePanelId);
}
// 删除指定面板中选中的文件(含确认)
function deleteSelectedFiles(panelId) {
const selectedFiles = panelState[panelId].selectedFiles;
if (selectedFiles.length === 0) {
alert('请先选择要删除的文件');
return;
}
const sessionId = panelState[panelId].sessionId;
let message;
if (selectedFiles.length === 1) {
const fileName = getFileNameFromPath(selectedFiles[0]);
message = '确定要删除 "' + fileName + '" 吗?此操作不可恢复。';
} else {
message = '确定要删除选中的 ' + selectedFiles.length + ' 个文件/文件夹吗?此操作不可恢复。';
}
if (confirm(message)) {
performDelete(sessionId, selectedFiles, panelId);
}
}
// 执行删除请求(单个走 DELETE多个走批量 POST
function performDelete(sessionId, paths, panelId) {
if (!panelId) panelId = activePanelId;
if (paths.length === 1) {
$.ajax({
url: API_BASE + 'api/files/delete',
method: 'DELETE',
data: {
sessionId: sessionId,
path: paths[0]
},
success: function(response) {
if (response.success) {
updateStatus('删除成功');
loadFiles(panelId);
} else {
alert('删除失败: ' + (response.message || '未知错误'));
}
},
error: function(xhr, status, error) {
const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
alert('删除失败: ' + errMsg);
}
});
} else {
$.ajax({
url: API_BASE + 'api/files/batch-delete',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
sessionId: sessionId,
paths: paths
}),
success: function(response) {
if (response.success) {
const result = response.data;
let msg = '成功删除 ' + result.successCount + ' 个';
if (result.failCount > 0) {
msg += ',失败 ' + result.failCount + ' 个。\n失败详情:\n' + (result.failedFiles || []).join('\n');
}
alert(msg);
updateStatus(msg);
loadFiles(panelId);
} else {
alert('批量删除失败: ' + (response.message || '未知错误'));
}
},
error: function(xhr, status, error) {
const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
alert('批量删除失败: ' + errMsg);
}
});
}
}
// 文件列表右键菜单
function showContextMenu(event, panelId, path) {
$('.context-menu').remove();
const menu = $('<div class="context-menu dropdown-menu show"></div>');
menu.css({
position: 'fixed',
left: event.clientX + 'px',
top: event.clientY + 'px',
zIndex: 1050
});
const deleteItem = $('<a class="dropdown-item text-danger" href="#">删除</a>');
deleteItem.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
menu.remove();
deleteFileByPath(panelId, path);
});
menu.append(deleteItem);
const renameItem = $('<a class="dropdown-item" href="#">重命名</a>');
renameItem.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
menu.remove();
showRenameDialog(panelId, path);
});
menu.append(renameItem);
const mkdirItem = $('<a class="dropdown-item" href="#">新建文件夹</a>');
mkdirItem.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
menu.remove();
showMkdirDialog(panelId);
});
menu.append(mkdirItem);
$('body').append(menu);
$(document).one('click', function() {
menu.remove();
});
}
// 右键菜单:删除单个文件
function deleteFileByPath(panelId, path) {
const sessionId = panelState[panelId].sessionId;
const fileName = getFileNameFromPath(path);
if (confirm('确定要删除 "' + fileName + '" 吗?此操作不可恢复。')) {
performDelete(sessionId, [path], panelId);
}
}
// ========== 文件重命名功能模块07==========
// 验证文件名(前端)
function validateFileName(fileName) {
if (!fileName || fileName.trim() === '') {
alert('文件名不能为空');
return false;
}
const illegalChars = /[\\/:*?"<>|]/;
if (illegalChars.test(fileName)) {
alert('文件名不能包含以下字符: \\ / : * ? " < > |');
return false;
}
if (fileName.length > 255) {
alert('文件名过长(最大 255 个字符)');
return false;
}
return true;
}
// 获取选中项路径(仅当选中一个时返回,否则提示并返回 null
function getSelectedPath(panelId) {
const selectedFiles = panelState[panelId].selectedFiles;
if (selectedFiles.length === 0) {
alert('请先选择一个文件或文件夹');
return null;
}
if (selectedFiles.length > 1) {
alert('一次只能重命名一个文件或文件夹');
return null;
}
return selectedFiles[0];
}
// 显示重命名对话框(无参时从当前活动面板取选中项;有参时用于右键菜单)
function showRenameDialog(panelId, path) {
if (path === undefined) {
panelId = getActivePanelId();
path = getSelectedPath(panelId);
if (!path) return;
}
const sessionId = panelState[panelId].sessionId;
const oldName = getFileNameFromPath(path);
const newName = prompt('请输入新名称:', oldName);
if (newName === null) return; // 用户取消
if (newName.trim() === '') {
alert('名称不能为空');
return;
}
if (newName.trim() === oldName) {
updateStatus('名称未更改');
return;
}
if (!validateFileName(newName.trim())) return;
doRename(sessionId, path, newName.trim(), panelId);
}
// 执行重命名请求
function doRename(sessionId, oldPath, newName, panelId) {
if (!panelId) panelId = getActivePanelId();
$.ajax({
url: API_BASE + 'api/files/rename',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
sessionId: sessionId,
oldPath: oldPath,
newName: newName
}),
success: function(response) {
if (response.success) {
updateStatus('重命名成功');
loadFiles(panelId);
} else {
alert('重命名失败: ' + (response.message || '未知错误'));
}
},
error: function(xhr, status, error) {
const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
alert('重命名失败: ' + errMsg);
}
});
}
// ========== 新建文件夹功能模块08==========
// 显示新建文件夹对话框(无参时对当前活动面板;有参时用于右键菜单等)
function showMkdirDialog(panelId) {
if (panelId === undefined) panelId = getActivePanelId();
const sessionId = panelState[panelId].sessionId;
const currentPath = panelState[panelId].currentPath;
if (!currentPath) {
alert('当前路径无效,请先选择目录');
return;
}
const folderName = prompt('请输入文件夹名称:', '新建文件夹');
if (folderName === null) return;
if (folderName.trim() === '') {
alert('文件夹名称不能为空');
return;
}
if (!validateDirectoryName(folderName.trim())) return;
createDirectory(sessionId, currentPath, folderName.trim(), panelId);
}
// 创建目录
function createDirectory(sessionId, parentPath, folderName, panelId) {
if (!validateDirectoryName(folderName)) return;
var fullPath = joinPath(parentPath, folderName, sessionId);
$.ajax({
url: API_BASE + 'api/files/mkdir',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
sessionId: sessionId,
path: fullPath
}),
success: function(response) {
if (response.success) {
updateStatus('文件夹创建成功');
loadFiles(panelId || getActivePanelId());
} else {
alert('创建失败: ' + (response.message || '未知错误'));
}
},
error: function(xhr, status, error) {
var errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
alert('创建失败: ' + errMsg);
}
});
}
// 拼接路径(父路径 + 子名称)
function joinPath(parentPath, childName, sessionId) {
childName = (childName || '').trim();
var isLocal = sessionId === 'local';
if (isLocal) {
if (parentPath.endsWith('\\') || parentPath.endsWith('/')) {
return parentPath + childName;
}
return parentPath + '\\' + childName;
}
if (parentPath.endsWith('/')) {
return parentPath + childName;
}
return parentPath + '/' + childName;
}
// 验证目录名(前端)
function validateDirectoryName(dirName) {
if (!dirName || dirName.trim() === '') {
alert('文件夹名称不能为空');
return false;
}
dirName = dirName.trim();
var illegalChars = /[\\/:*?"<>|]/;
if (illegalChars.test(dirName)) {
alert('文件夹名称不能包含以下字符: \\ / : * ? " < > |');
return false;
}
if (dirName.startsWith('.') || dirName.endsWith('.')) {
alert('文件夹名称不能以点开头或结尾');
return false;
}
if (dirName.length > 255) {
alert('文件夹名称过长(最大 255 个字符)');
return false;
}
var upperName = dirName.toUpperCase();
var reservedNames = ['CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
if (reservedNames.indexOf(upperName) !== -1) {
alert('文件夹名称不能使用系统保留名称');
return false;
}
return true;
}