Files
sftp-manager/docs/10-模式切换功能.md
liu 14289beb66 Initial commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 10:10:11 +08:00

20 KiB
Raw Blame History

模块10模式切换功能


🎨 UI设计系统概览

完整设计系统文档请参考: UI设计系统.md

核心设计原则

  • 现代简约:界面清晰,层次分明
  • 专业高效:减少操作步骤,提升工作效率
  • 一致性:统一的视觉语言和交互模式
  • 可访问性符合WCAG 2.1 AA标准

关键设计令牌

颜色系统:

  • 主色:#0d6efd(操作按钮、选中状态)
  • 成功:#198754(连接成功状态)
  • 危险:#dc3545(删除操作、错误提示)
  • 深灰:#212529(导航栏背景)
  • 浅灰:#e9ecef(工具栏背景)

字体系统:

  • 字体族:系统字体栈(-apple-system, Segoe UI, Roboto等
  • 正文14px行高1.5
  • 标题20-32px行高1.2-1.4
  • 小号文字12px文件大小、日期等

间距系统:

  • 基础单位8px
  • 标准间距16px1rem
  • 组件内边距8px-16px

组件规范:

  • 导航栏高度48px深色背景
  • 工具栏浅灰背景按钮间距8px
  • 文件项最小高度44px悬停效果150ms
  • 按钮圆角4px过渡150ms

交互规范:

  • 悬停效果150ms过渡
  • 触摸目标最小44x44px
  • 键盘导航Tab、Enter、Delete、F2、F5、Esc
  • 焦点状态2px蓝色轮廓

响应式断点:

  • 移动端:< 768px双面板垂直排列
  • 平板768px - 1024px
  • 桌面:> 1024px标准布局

10.1 功能概述

实现左侧和右侧面板在"本地文件"和"SFTP服务器"模式之间切换支持同时连接多个SFTP服务器。

10.2 模式状态管理

10.2.1 JavaScript状态管理

// 面板状态管理
const panelState = {
    left: {
        mode: 'local',           // 'local' 或 'sftp'
        sessionId: 'local',      // 'local' 或 SFTP会话ID
        currentPath: '',         // 当前路径
        selectedFiles: []        // 选中的文件
    },
    right: {
        mode: 'local',
        sessionId: 'local',
        currentPath: '',
        selectedFiles: []
    }
};

// 活跃连接映射
const activeConnections = {};

// 已保存的连接列表
let savedConnections = [];

10.2.2 模式切换逻辑

// 模式切换
function onModeChange(panelId) {
    const mode = $(`#${panelId}-mode`).val();
    panelState[panelId].mode = mode;

    if (mode === 'local') {
        switchToLocalMode(panelId);
    } else {
        switchToSftpMode(panelId);
    }
}

// 切换到本地模式
function switchToLocalMode(panelId) {
    updateStatus(`正在切换到本地模式...`);

    // 隐藏SFTP连接选择器
    $(`#${panelId}-connection`).hide();

    // 更新会话ID
    panelState[panelId].sessionId = 'local';

    // 设置默认路径
    panelState[panelId].currentPath = getDefaultLocalPath();

    // 加载本地文件列表
    loadFiles(panelId);

    updateStatus('已切换到本地模式');
}

// 切换到SFTP模式
function switchToSftpMode(panelId) {
    updateStatus(`正在切换到SFTP模式...`);

    // 显示SFTP连接选择器
    $(`#${panelId}-connection`).show();

    // 加载已保存的连接列表
    loadSavedConnections(panelId);

    // 检查是否有活跃的SFTP连接
    const activeSessionId = findActiveSftpSession(panelId);
    if (activeSessionId) {
        // 使用已有连接
        panelState[panelId].sessionId = activeSessionId;
        $(`#${panelId}-connection`).val(activeSessionId);
        loadSftpCurrentPath(panelId, activeSessionId);
    } else {
        // 提示用户选择或创建连接
        alert('请选择一个SFTP连接或创建新连接');
    }

    updateStatus('已切换到SFTP模式');
}

// 获取默认本地路径
function getDefaultLocalPath() {
    return System.getProperty('user.home');
}

// 查找活跃的SFTP会话
function findActiveSftpSession(panelId) {
    const connectionSelect = $(`#${panelId}-connection`);
    const sessionId = connectionSelect.val();
    if (sessionId && sessionId !== 'local') {
        return sessionId;
    }
    return null;
}

// 加载SFTP当前路径
function loadSftpCurrentPath(panelId, sessionId) {
    $.ajax({
        url: '/api/files/path',
        method: 'GET',
        data: {sessionId: sessionId},
        success: function(response) {
            if (response.success) {
                panelState[panelId].currentPath = response.data.path;
                loadFiles(panelId);
            } else {
                alert('获取路径失败: ' + response.message);
            }
        },
        error: handleError
    });
}

10.3 连接管理UI

10.3.1 连接列表加载

// 加载已保存的连接列表
function loadSavedConnections(panelId) {
    $.ajax({
        url: '/api/connection/list',
        method: 'GET',
        success: function(response) {
            if (response.success) {
                savedConnections = response.data;
                updateConnectionSelect(panelId, savedConnections);
            } else {
                alert('加载连接列表失败: ' + response.message);
            }
        },
        error: handleError
    });
}

// 更新连接选择器
function updateConnectionSelect(panelId, connections) {
    const select = $(`#${panelId}-connection`);
    select.empty();

    // 添加选项
    if (connections.length === 0) {
        select.append('<option value="">请选择连接</option>');
    } else {
        connections.forEach(conn => {
            const option = $('<option>');
            option.val(conn.id);
            option.text(`${conn.name} (${conn.username}@${conn.host})`);
            select.append(option);
        });
    }
}

// 加载活跃连接列表
function loadActiveConnections() {
    $.ajax({
        url: '/api/connection/active',
        method: 'GET',
        success: function(response) {
            if (response.success) {
                activeConnections = response.data;
                updateConnectionPanels();
            } else {
                console.error('加载活跃连接失败:', response.message);
            }
        },
        error: handleError
    });
}

// 更新连接面板
function updateConnectionPanels() {
    Object.keys(panelState).forEach(panelId => {
        if (panelState[panelId].mode === 'sftp') {
            updateConnectionSelect(panelId, savedConnections);
            // 选中当前活跃的连接
            const currentSessionId = panelState[panelId].sessionId;
            if (currentSessionId !== 'local') {
                // 找到对应的连接ID并选中
                Object.entries(activeConnections).forEach(([sessionId, conn]) => {
                    if (sessionId === currentSessionId) {
                        $(`#${panelId}-connection`).val(conn.id);
                    }
                });
            }
        }
    });
}

10.3.2 连接管理对话框

// 显示连接管理对话框
function showConnectionDialog() {
    $('#connectionModal').modal('show');
    loadConnectionList();
}

// 加载连接列表
function loadConnectionList() {
    const connectionList = $('#connection-list');
    connectionList.empty();

    if (savedConnections.length === 0) {
        connectionList.html('<p class="text-muted text-center">暂无保存的连接</p>');
        return;
    }

    savedConnections.forEach(conn => {
        const item = $(`
            <div class="connection-item" data-id="${conn.id}">
                <div>
                    <div class="connection-name">${conn.name}</div>
                    <div class="connection-info">${conn.username}@${conn.host}:${conn.port}</div>
                </div>
                <div class="connection-actions">
                    <button class="btn btn-sm btn-primary" onclick="connectToSftp(${conn.id})">连接</button>
                    <button class="btn btn-sm btn-danger" onclick="deleteConnection(${conn.id})">删除</button>
                </div>
            </div>
        `);
        connectionList.append(item);
    });
}

// 显示添加连接对话框
function showAddConnectionDialog() {
    $('#connectionModal').modal('hide');
    $('#addConnectionModal').modal('show');
    $('#connection-form')[0].reset();
}

// 保存连接
function saveConnection() {
    const formData = $('#connection-form').serializeObject();

    $.ajax({
        url: '/api/connection/save',
        method: 'POST',
        contentType: 'application/json',
        data: JSON.stringify(formData),
        success: function(response) {
            if (response.success) {
                alert('连接保存成功');
                $('#addConnectionModal').modal('hide');
                $('#connectionModal').modal('show');
                loadSavedConnections('left');
                loadSavedConnections('right');
            } else {
                alert('保存失败: ' + response.message);
            }
        },
        error: handleError
    });
}

// 删除连接
function deleteConnection(id) {
    if (confirm('确定要删除此连接吗?')) {
        $.ajax({
            url: '/api/connection/' + id,
            method: 'DELETE',
            success: function(response) {
                if (response.success) {
                    alert('删除成功');
                    loadConnectionList();
                    loadSavedConnections('left');
                    loadSavedConnections('right');
                } else {
                    alert('删除失败: ' + response.message);
                }
            },
            error: handleError
        });
    }
}

// 序列化表单为对象
$.fn.serializeObject = function() {
    const o = {};
    const a = this.serializeArray();
    $.each(a, function() {
        if (o[this.name] !== undefined) {
            if (!o[this.name].push) {
                o[this.name] = [o[this.name]];
            }
            o[this.name].push(this.value || '');
        } else {
            o[this.name] = this.value || '';
        }
    });
    return o;
};

10.3.3 SFTP连接

// 连接到SFTP服务器
function connectToSftp(connectionId) {
    // 查找连接配置
    const connection = savedConnections.find(c => c.id === connectionId);
    if (!connection) {
        alert('连接不存在');
        return;
    }

    // 检查是否已连接
    const existingSession = findSessionByConnectionId(connectionId);
    if (existingSession) {
        alert('该连接已在使用中');
        return;
    }

    updateStatus(`正在连接到 ${connection.name}...`);

    $.ajax({
        url: '/api/connection/connect',
        method: 'POST',
        contentType: 'application/json',
        data: JSON.stringify(connection),
        success: function(response) {
            if (response.success) {
                const sessionId = response.data;

                // 更新活跃连接列表
                activeConnections[sessionId] = connection;

                // 更新选择器
                $(`#left-connection`).val(connectionId);
                $(`#right-connection`).val(connectionId);

                // 更新面板状态
                updatePanelStateWithConnection(sessionId, connection);

                // 加载活跃连接到选择器
                updateActiveConnectionOptions();

                alert('连接成功');
                updateStatus('已连接到 ' + connection.name);
            } else {
                alert('连接失败: ' + response.message);
                updateStatus('连接失败: ' + response.message);
            }
        },
        error: function(xhr, status, error) {
            alert('连接失败: ' + error);
            updateStatus('连接失败: ' + error);
        }
    });
}

// 查找会话
function findSessionByConnectionId(connectionId) {
    return Object.entries(activeConnections).find(([sessionId, conn]) => conn.id === connectionId);
}

// 更新面板状态
function updatePanelStateWithConnection(sessionId, connection) {
    // 更新左侧面板如果是SFTP模式
    if (panelState.left.mode === 'sftp') {
        panelState.left.sessionId = sessionId;
        $(`#left-connection`).val(connection.id);
        loadSftpCurrentPath('left', sessionId);
    }

    // 更新右侧面板如果是SFTP模式
    if (panelState.right.mode === 'sftp') {
        panelState.right.sessionId = sessionId;
        $(`#right-connection`).val(connection.id);
        loadSftpCurrentPath('right', sessionId);
    }
}

// 更新活跃连接选项
function updateActiveConnectionOptions() {
    Object.keys(panelState).forEach(panelId => {
        if (panelState[panelId].mode === 'sftp') {
            const select = $(`#${panelId}-connection`);
            select.empty();

            // 添加选项
            if (Object.keys(activeConnections).length === 0) {
                select.append('<option value="">请先连接</option>');
            } else {
                Object.entries(activeConnections).forEach(([sessionId, conn]) => {
                    const option = $('<option>');
                    option.val(sessionId);
                    option.text(`${conn.name} (${conn.username}@${conn.host})`);
                    select.append(option);
                });
            }

            // 选中的是当前会话
            if (panelState[panelId].sessionId !== 'local') {
                select.val(panelState[panelId].sessionId);
            }
        }
    });
}

// 连接切换
function onConnectionChange(panelId) {
    const sessionId = $(`#${panelId}-connection`).val();
    if (!sessionId) {
        return;
    }

    panelState[panelId].sessionId = sessionId;
    loadSftpCurrentPath(panelId, sessionId);
}

10.4 连接状态显示

10.4.1 连接状态指示器

<!-- 在面板头部添加状态指示器 -->
<div class="panel-header">
    <select class="form-select form-select-sm panel-mode" id="left-mode" onchange="onModeChange('left')">
        <option value="local">本地文件</option>
        <option value="sftp">SFTP服务器</option>
    </select>
    <div class="connection-status" id="left-status" style="display:none;">
        <span class="status-dot" data-status="connected"></span>
        <span class="status-text">已连接</span>
    </div>
    <select class="form-select form-select-sm connection-select" id="left-connection" style="display:none;" onchange="onConnectionChange('left')">
    </select>
</div>

10.4.2 状态样式

/* 连接状态 */
.connection-status {
    display: flex;
    align-items: center;
    padding: 0 8px;
}

.status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    display: inline-block;
    margin-right: 5px;
}

.status-dot[data-status="connected"] {
    background-color: #28a745;
}

.status-dot[data-status="disconnected"] {
    background-color: #dc3545;
}

.status-dot[data-status="connecting"] {
    background-color: #ffc107;
    animation: pulse 1s infinite;
}

@keyframes pulse {
    0%, 100% {
        opacity: 1;
    }
    50% {
        opacity: 0.5;
    }
}

.status-text {
    font-size: 12px;
    color: #666;
}

.connection-status.connected .status-text {
    color: #28a745;
}

.connection-status.disconnected .status-text {
    color: #dc3545;
}

.connection-status.connecting .status-text {
    color: #ffc107;
}

10.4.3 状态更新逻辑

// 更新连接状态显示
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');
    statusDiv.addClass(status);

    statusDot.attr('data-status', status);
    statusText.text(text);
}

// 显示连接状态
function showConnectionStatus(panelId, sessionId) {
    const statusDiv = $(`#${panelId}-status`);
    if (statusDiv.length === 0) {
        return;
    }

    if (sessionId === 'local') {
        statusDiv.hide();
    } else {
        statusDiv.show();
        const connection = activeConnections[sessionId];
        if (connection) {
            updateConnectionStatus(panelId, 'connected', `已连接: ${connection.name}`);
        } else {
            updateConnectionStatus(panelId, 'disconnected', '未连接');
        }
    }
}

// 更新模式切换时的状态
function onModeChange(panelId) {
    const mode = $(`#${panelId}-mode`).val();
    panelState[panelId].mode = mode;

    if (mode === 'local') {
        switchToLocalMode(panelId);
        showConnectionStatus(panelId, 'local');
    } else {
        switchToSftpMode(panelId);
    }
}

10.5 断开连接

// 断开SFTP连接
function disconnectFromSftp(sessionId) {
    updateStatus('正在断开连接...');

    $.ajax({
        url: '/api/connection/disconnect',
        method: 'POST',
        contentType: 'application/json',
        data: JSON.stringify({sessionId: sessionId}),
        success: function(response) {
            if (response.success) {
                // 从活跃连接中移除
                delete activeConnections[sessionId];

                // 更新面板状态
                updatePanelsAfterDisconnect(sessionId);

                // 更新连接选项
                updateActiveConnectionOptions();

                alert('已断开连接');
                updateStatus('连接已断开');
            } else {
                alert('断开连接失败: ' + response.message);
            }
        },
        error: handleError
    });
}

// 更新断开连接后的面板
function updatePanelsAfterDisconnect(sessionId) {
    Object.keys(panelState).forEach(panelId => {
        if (panelState[panelId].sessionId === sessionId) {
            panelState[panelId].sessionId = 'local';
            panelState[panelId].mode = 'local';
            $(`#${panelId}-mode`).val('local');
            onModeChange(panelId);
        }
    });
}

// 在连接项中添加断开按钮
function loadConnectionList() {
    const connectionList = $('#connection-list');
    connectionList.empty();

    if (savedConnections.length === 0) {
        connectionList.html('<p class="text-muted text-center">暂无保存的连接</p>');
        return;
    }

    savedConnections.forEach(conn => {
        // 查找是否有活跃会话
        const activeSession = findSessionByConnectionId(conn.id);

        const item = $(`
            <div class="connection-item" data-id="${conn.id}">
                <div>
                    <div class="connection-name">${conn.name}</div>
                    <div class="connection-info">${conn.username}@${conn.host}:${conn.port}</div>
                </div>
                <div class="connection-actions">
                    ${activeSession ?
                        '<button class="btn btn-sm btn-warning" onclick="disconnectFromSftp(\'' +
                        activeSession[0] + '\')">断开</button>' :
                        '<button class="btn btn-sm btn-primary" onclick="connectToSftp(' +
                        conn.id + ')">连接</button>'
                    }
                    <button class="btn btn-sm btn-danger" onclick="deleteConnection(${conn.id})">删除</button>
                </div>
            </div>
        `);
        connectionList.append(item);
    });
}

实施步骤

  1. 更新app.js:添加模式切换逻辑

  2. 更新index.html:添加连接管理对话框和状态指示器

  3. 测试功能

    • 测试本地/SFTP模式切换
    • 测试连接管理功能
    • 测试多连接管理
    • 测试连接状态显示

注意事项

  1. 状态同步:确保前后端状态同步
  2. 错误处理:连接失败时的友好提示
  3. 会话管理正确管理多个SFTP会话
  4. 性能优化:避免频繁刷新连接列表
  5. 用户体验:提供清晰的状态反馈

下一步

完成模块10后继续模块11API接口设计规范