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

715 lines
20 KiB
Markdown
Raw Permalink 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.
# 模块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状态管理
```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 模式切换逻辑
```javascript
// 模式切换
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 连接列表加载
```javascript
// 加载已保存的连接列表
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 连接管理对话框
```javascript
// 显示连接管理对话框
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连接
```javascript
// 连接到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 连接状态指示器
```html
<!-- 在面板头部添加状态指示器 -->
<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 状态样式
```css
/* 连接状态 */
.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 状态更新逻辑
```javascript
// 更新连接状态显示
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 断开连接
```javascript
// 断开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接口设计规范