715 lines
20 KiB
Markdown
715 lines
20 KiB
Markdown
# 模块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
|
||
- 标准间距:16px(1rem)
|
||
- 组件内边距: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后,继续模块11:API接口设计规范
|