# 模块09:双面板UI界面设计 --- ## 🎨 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(标准布局) --- ## 9.1 界面布局 ### 9.1.1 整体HTML结构 ```html SFTP文件管理器
就绪
``` ## 9.2 样式设计 ### 9.2.1 主样式(style.css) ```css /* 全局样式 */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 14px; background-color: #f5f5f5; overflow: hidden; } /* 应用容器 */ .app-container { display: flex; flex-direction: column; height: 100vh; background-color: #fff; } /* 导航栏 */ .navbar { flex-shrink: 0; padding: 8px 16px; } .navbar-brand { font-size: 18px; font-weight: 600; } /* 工具栏 */ .toolbar { flex-shrink: 0; padding: 8px 16px; display: flex; align-items: center; gap: 8px; } /* 双面板容器 */ .panels-container { display: flex; flex: 1; overflow: hidden; } /* 面板 */ .panel { flex: 1; display: flex; flex-direction: column; border-right: 1px solid #dee2e6; overflow: hidden; } .panel:last-child { border-right: none; } /* 面板头部 */ .panel-header { flex-shrink: 0; padding: 8px; background-color: #f8f9fa; border-bottom: 1px solid #dee2e6; display: flex; gap: 8px; } .panel-mode { flex: 1; } .connection-select { flex: 2; } /* 路径栏 */ .path-bar { flex-shrink: 0; padding: 8px; display: flex; gap: 8px; background-color: #fff; border-bottom: 1px solid #dee2e6; } .path-input { flex: 1; } /* 文件列表 */ .file-list { flex: 1; overflow-y: auto; overflow-x: hidden; } /* 文件项 */ .file-item { padding: 8px 12px; cursor: pointer; display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; transition: background-color 0.15s; user-select: none; } .file-item:hover { background-color: #f8f9fa; } .file-item.selected { background-color: #007bff; color: white; } .file-icon { margin-right: 10px; width: 20px; text-align: center; font-size: 16px; } .file-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .file-size { margin-left: 10px; min-width: 80px; text-align: right; font-size: 12px; color: #666; } .file-date { margin-left: 10px; min-width: 140px; text-align: right; font-size: 12px; color: #666; } /* 选中状态下的文件大小和日期 */ .file-item.selected .file-size, .file-item.selected .file-date { color: rgba(255, 255, 255, 0.8); } /* 状态栏 */ .status-bar { flex-shrink: 0; padding: 4px 16px; font-size: 12px; color: #666; } /* 上下文菜单 */ .context-menu { position: absolute; background-color: #fff; border: 1px solid #ccc; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); z-index: 1000; min-width: 120px; } .menu-item { padding: 8px 12px; cursor: pointer; user-select: none; } .menu-item:hover { background-color: #007bff; color: white; } /* 新建文件夹项 */ .new-folder { background-color: #e7f3ff; } .new-folder-input { width: 100%; padding: 2px 4px; border: 1px solid #007bff; outline: none; font-size: 14px; } /* 连接列表 */ .connection-item { padding: 8px 12px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; } .connection-item:hover { background-color: #f8f9fa; } .connection-name { font-weight: 600; } .connection-info { font-size: 12px; color: #666; } .connection-actions { display: flex; gap: 4px; } .connection-actions button { padding: 2px 8px; font-size: 12px; } ``` ## 9.3 JavaScript基础逻辑 ### 9.3.1 状态管理(app.js) ```javascript // 面板状态 const panelState = { left: { mode: 'local', // 'local' 或 'sftp' sessionId: 'local', // 'local' 或 SFTP会话ID currentPath: '', // 当前路径 selectedFiles: [] // 选中的文件 }, right: { mode: 'local', sessionId: 'local', currentPath: '', selectedFiles: [] } }; // 活跃连接列表 let activeConnections = {}; // 初始化 $(document).ready(function() { // 设置初始路径 panelState.left.currentPath = getDefaultLocalPath(); panelState.right.currentPath = getDefaultLocalPath(); // 加载文件列表 loadFiles('left'); loadFiles('right'); // 绑定键盘事件 bindKeyboardEvents(); }); // 获取默认本地路径 function getDefaultLocalPath() { return System.getProperty('user.home'); } ``` ### 9.3.2 文件列表加载 ```javascript // 加载文件列表 function loadFiles(panelId) { const sessionId = panelState[panelId].sessionId; const path = panelState[panelId].currentPath; updateStatus(`正在加载 ${panelId === 'left' ? '左' : '右'}面板文件列表...`); $.ajax({ url: '/api/files/list', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sessionId: sessionId, path: path }), success: function(response) { if (response.success) { renderFileList(panelId, response.data); updatePathInput(panelId, path); updateStatus('就绪'); } else { alert('加载文件列表失败: ' + response.message); updateStatus('错误: ' + response.message); } }, error: function(xhr, status, error) { alert('加载文件列表失败: ' + error); updateStatus('错误: ' + error); } }); } // 渲染文件列表 function renderFileList(panelId, files) { const fileList = $(`#${panelId}-file-list`); fileList.empty(); if (files.length === 0) { fileList.html('
文件夹为空
'); return; } files.forEach(file => { const icon = getFileIcon(file.name, file.isDirectory); const size = file.isDirectory ? '' : formatFileSize(file.size); const date = formatDate(file.modifiedTime); const item = $(`
${icon} ${file.name} ${size} ${date}
`); // 单击选择 item.on('click', function(e) { if (!e.ctrlKey) { $(`#${panelId}-file-list .file-item`).removeClass('selected'); } item.toggleClass('selected'); updateSelectedFiles(panelId); }); // 双击进入目录 item.on('dblclick', function() { if (file.isDirectory) { enterDirectory(panelId, file.path); } }); // 右键菜单 item.on('contextmenu', function(e) { e.preventDefault(); showContextMenu(e, panelId, file.path, file.isDirectory); }); 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) { 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 updatePathInput(panelId, path) { $(`#${panelId}-path`).val(path); } // 更新选中文件列表 function updateSelectedFiles(panelId) { const selected = $(`#${panelId}-file-list .file-item.selected`); panelState[panelId].selectedFiles = selected.map(function() { return $(this).data('path'); }).get(); } // 更新状态栏 function updateStatus(text) { $('#status-text').text(text); } ``` ## 9.4 响应式设计 ### 9.4.1 移动端适配 ```css /* 小屏幕设备 */ @media (max-width: 768px) { .panels-container { flex-direction: column; } .panel { height: 50%; border-right: none; border-bottom: 1px solid #dee2e6; } .file-size, .file-date { display: none; } .toolbar { flex-wrap: wrap; } .navbar-brand { font-size: 16px; } } /* 极小屏幕设备 */ @media (max-width: 576px) { .toolbar button { padding: 2px 8px; font-size: 12px; } .file-item { padding: 6px 8px; } } ``` ## 9.5 交互设计 ### 9.5.1 文件选择 ```javascript // 全选 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); } ``` ### 9.5.2 拖拽操作 ```javascript // 文件拖拽初始化 function initDragAndDrop() { $('.file-item').attr('draggable', true); // 拖拽开始 $('.file-item').on('dragstart', function(e) { const panelId = $(this).closest('.panel').attr('id'); const path = $(this).data('path'); e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ panelId: panelId, path: path })); $(this).addClass('dragging'); }); // 拖拽结束 $('.file-item').on('dragend', function() { $(this).removeClass('dragging'); }); // 拖拽悬停 $('.file-list').on('dragover', function(e) { e.preventDefault(); const panelId = $(this).closest('.panel').attr('id'); $(this).addClass('drag-over'); }); // 拖拽离开 $('.file-list').on('dragleave', function() { $(this).removeClass('drag-over'); }); // 拖拽放置 $('.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'); handleFileDrop(data, targetPanelId); } catch (err) { console.error('拖拽失败:', err); } }); } // 处理文件拖放 function handleFileDrop(data, targetPanelId) { const sourcePanelId = data.panelId; const sourcePath = data.path; const targetPath = panelState[targetPanelId].currentPath; if (sourcePanelId === targetPanelId) { // 同一面板,可以移动文件 // 这里可以实现文件移动功能 alert('文件移动功能开发中...'); } else { // 跨面板传输 $.ajax({ url: '/api/files/transfer', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sourceSessionId: panelState[sourcePanelId].sessionId, sourcePath: sourcePath, targetSessionId: panelState[targetPanelId].sessionId, targetPath: targetPath }), success: function(response) { if (response.success) { alert('传输成功'); loadFiles(targetPanelId); } else { alert('传输失败: ' + response.message); } }, error: handleError }); } } ``` ### 9.5.3 快捷键支持 ```javascript // 绑定键盘事件 function bindKeyboardEvents() { $(document).on('keydown', function(e) { // Delete: 删除选中文件 if (e.key === 'Delete') { deleteSelectedFiles(getActivePanelId()); e.preventDefault(); } // F2: 重命名 if (e.key === 'F2') { const panelId = getActivePanelId(); const selected = panelState[panelId].selectedFiles; if (selected.length === 1) { showRenameDialog(panelId, selected[0]); } e.preventDefault(); } // F5: 刷新 if (e.key === 'F5') { refreshPanels(); e.preventDefault(); } // Backspace: 返回上级目录 if (e.key === 'Backspace') { const panelId = getActivePanelId(); goUp(panelId); e.preventDefault(); } // Ctrl+A: 全选 if (e.ctrlKey && (e.key === 'a' || e.key === 'A')) { selectAll(getActivePanelId()); e.preventDefault(); } // Esc: 取消选择 if (e.key === 'Escape') { const panelId = getActivePanelId(); deselectAll(panelId); e.preventDefault(); } // Ctrl+Shift+N: 新建文件夹 if (e.ctrlKey && e.shiftKey && (e.key === 'n' || e.key === 'N')) { showMkdirDialog(getActivePanelId()); e.preventDefault(); } }); } // 获取当前活动面板 function getActivePanelId() { // 简单实现:返回左侧面板 // 可以根据鼠标位置等逻辑判断 return 'left'; } ``` ## 实施步骤 1. **创建index.html**:包含完整的HTML结构 2. **创建style.css**:编写样式文件 3. **创建app.js**:编写JavaScript基础逻辑 4. **测试界面**: ``` # 将文件放置到正确位置 cp index.html src/main/resources/templates/ cp style.css src/main/resources/static/css/ cp app.js src/main/resources/static/js/ # 启动服务 mvn spring-boot:run ``` 5. **访问测试**:http://localhost:8080/sftp-manager ## 注意事项 1. **CDN资源**:确保网络可以访问Bootstrap和jQuery的CDN 2. **静态资源**:确保静态文件路径正确 3. **浏览器兼容性**:测试主流浏览器兼容性 4. **响应式**:在不同屏幕尺寸下测试界面 5. **性能优化**:大文件列表考虑虚拟滚动 ## 下一步 完成模块09后,继续模块10:模式切换功能