# 模块08:新建文件夹功能 --- ## 🎨 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(标准布局) --- ## 8.1 功能概述 实现在本地文件系统和SFTP服务器上创建新文件夹的功能。 ## 8.2 后端设计 ### 8.2.1 LocalFileService创建目录方法 ```java // 创建目录 public boolean createDirectory(String path) throws Exception { File directory = new File(path); if (directory.exists()) { throw new Exception("目录已存在: " + path); } // 验证目录名 String dirName = directory.getName(); if (!isValidDirectoryName(dirName)) { throw new Exception("目录名包含非法字符: " + dirName); } // 检查父目录是否存在 File parentDir = directory.getParentFile(); if (parentDir != null && !parentDir.exists()) { // 父目录不存在,询问是否创建多级目录 throw new Exception("父目录不存在: " + parentDir.getPath()); } boolean result = directory.mkdirs(); if (!result) { throw new Exception("创建目录失败"); } return true; } // 创建多级目录 public boolean createDirectories(String path) throws Exception { File directory = new File(path); if (directory.exists()) { throw new Exception("目录已存在: " + path); } // 验证路径中的每个目录名 String[] parts = path.split("[/\\\\]"); for (String part : parts) { if (!isValidDirectoryName(part)) { throw new Exception("路径包含非法字符: " + part); } } boolean result = directory.mkdirs(); if (!result) { throw new Exception("创建目录失败"); } return true; } // 验证目录名是否有效 private boolean isValidDirectoryName(String dirName) { if (dirName == null || dirName.isEmpty()) { return false; } // Windows非法字符 String illegalChars = "\\/:*?\"<>|"; for (char c : illegalChars.toCharArray()) { if (dirName.indexOf(c) != -1) { return false; } } // 检查长度限制 if (dirName.length() > 255) { return false; } // 检查保留名称(Windows) String upperName = dirName.toUpperCase(); String[] reservedNames = {"CON", "PRN", "AUX", "NUL"}; for (String reserved : reservedNames) { if (upperName.equals(reserved)) { return false; } } return true; } ``` ### 8.2.2 SftpService创建目录方法 ```java // 创建目录 public boolean createDirectory(String sessionId, String path) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { // 检查目录是否已存在 try { channel.stat(path); throw new Exception("目录已存在: " + path); } catch (SftpException e) { if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) { throw e; } } // 创建目录 channel.mkdir(path); return true; } catch (SftpException e) { throw new Exception("创建目录失败: " + e.getMessage(), e); } } // 创建多级目录 public boolean createDirectories(String sessionId, String path) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { // 检查目录是否已存在 try { channel.stat(path); throw new Exception("目录已存在: " + path); } catch (SftpException e) { if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) { throw e; } } // 递归创建多级目录 createDirectoriesRecursive(channel, path); return true; } catch (SftpException e) { throw new Exception("创建目录失败: " + e.getMessage(), e); } } // 递归创建多级目录 private void createDirectoriesRecursive(ChannelSftp channel, String path) throws SftpException { // 如果是根目录,直接返回 if (path.equals("/") || path.isEmpty()) { return; } // 检查父目录是否存在 String parentPath = getParentPath(path); if (!parentPath.equals(path)) { try { channel.stat(parentPath); } catch (SftpException e) { if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { // 父目录不存在,递归创建 createDirectoriesRecursive(channel, parentPath); } else { throw e; } } } // 创建当前目录 channel.mkdir(path); } // 获取父路径 private String getParentPath(String path) { if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } int index = path.lastIndexOf("/"); if (index == -1) { return "/"; } if (index == 0) { return "/"; } return path.substring(0, index); } ``` ### 8.2.3 FileController创建目录接口 ```java // 创建目录 @PostMapping("/mkdir") public ApiResponse createDirectory(@RequestBody Map request) { try { String sessionId = request.get("sessionId"); String path = request.get("path"); if (path == null || path.isEmpty()) { return ApiResponse.error("路径不能为空"); } boolean result; if ("local".equals(sessionId)) { result = localFileService.createDirectory(path); } else { result = sftpService.createDirectory(sessionId, path); } if (result) { return ApiResponse.success("创建成功", null); } else { return ApiResponse.error("创建失败"); } } catch (Exception e) { return ApiResponse.error("创建失败: " + e.getMessage()); } } // 创建多级目录 @PostMapping("/mkdir-p") public ApiResponse createDirectories(@RequestBody Map request) { try { String sessionId = request.get("sessionId"); String path = request.get("path"); if (path == null || path.isEmpty()) { return ApiResponse.error("路径不能为空"); } boolean result; if ("local".equals(sessionId)) { result = localFileService.createDirectories(path); } else { result = sftpService.createDirectories(sessionId, path); } if (result) { return ApiResponse.success("创建成功", null); } else { return ApiResponse.error("创建失败"); } } catch (Exception e) { return ApiResponse.error("创建失败: " + e.getMessage()); } } ``` ## 8.3 前端设计 ### 8.3.1 新建文件夹交互 ```javascript // 显示新建文件夹对话框 function showMkdirDialog(panelId) { const sessionId = panelState[panelId].sessionId; const currentPath = panelState[panelId].currentPath; const folderName = prompt('请输入文件夹名称:', '新建文件夹'); if (folderName && folderName.trim() !== '') { createDirectory(sessionId, currentPath, folderName); } } // 创建目录 function createDirectory(sessionId, parentPath, folderName) { // 验证文件夹名称 if (!validateDirectoryName(folderName)) { return; } // 构建完整路径 let fullPath; if ("local".equals(sessionId)) { fullPath = parentPath + File.separator + folderName; } else { fullPath = parentPath.endsWith('/') ? parentPath + folderName : parentPath + '/' + folderName; } $.ajax({ url: '/api/files/mkdir', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sessionId: sessionId, path: fullPath }), success: function(response) { if (response.success) { alert('文件夹创建成功'); refreshCurrentPanel(); } else { alert('创建失败: ' + response.message); } }, error: handleError }); } // 验证目录名 function validateDirectoryName(dirName) { if (!dirName || dirName.trim() === '') { alert('文件夹名称不能为空'); return false; } // Windows非法字符 const illegalChars = /\\\/:\*\?"<>\|/; if (illegalChars.test(dirName)) { alert('文件夹名称包含非法字符'); return false; } // 长度限制 if (dirName.length > 255) { alert('文件夹名称过长(最大255字符)'); return false; } return true; } // 刷新当前面板 function refreshCurrentPanel() { const activePanelId = getActivePanelId(); loadFiles(activePanelId); } ``` ### 8.3.2 新建文件夹按钮 ```html
``` ### 8.3.3 右键菜单新建文件夹 ```javascript // 在右键菜单中添加新建文件夹选项 function showContextMenu(event, panelId) { event.preventDefault(); const menu = $('
'); menu.css({ position: 'absolute', left: event.pageX + 'px', top: event.pageY + 'px' }); menu.append(''); $('body').append(menu); $(document).one('click', function() { menu.remove(); }); } ``` ### 8.3.4 快捷键支持 ```javascript // 监听键盘事件 $(document).on('keydown', function(e) { // Ctrl+Shift+N: 新建文件夹 if (e.ctrlKey && e.shiftKey && e.key === 'N') { const activePanelId = getActivePanelId(); showMkdirDialog(activePanelId); e.preventDefault(); } }); ``` ### 8.3.5 在线输入(高级功能) ```javascript // 在文件列表中直接输入新文件夹名称 function showInlineMkdir(panelId) { const fileList = $(`#${panelId}-file-list`); // 创建新建文件夹项 const newItem = $(`
📁
`); // 插入到列表顶部 fileList.prepend(newItem); // 聚焦输入框 const input = newItem.find('.new-folder-input'); input.focus(); // 失去焦点时创建 input.on('blur', function() { const folderName = input.val().trim(); if (folderName) { createDirectory(panelState[panelId].sessionId, panelState[panelId].currentPath, folderName); } newItem.remove(); }); // 按Enter创建 input.on('keypress', function(e) { if (e.which === 13) { input.blur(); } }); // 按Esc取消 input.on('keydown', function(e) { if (e.which === 27) { newItem.remove(); } }); } ``` ## 8.4 输入验证 ### 8.4.1 前端验证 ```javascript // 验证目录名 function validateDirectoryName(dirName) { // 基本验证 if (!dirName || dirName.trim() === '') { alert('文件夹名称不能为空'); return false; } // 去除首尾空格 dirName = dirName.trim(); // Windows非法字符 const illegalChars = /\\\/:\*\?"<>\|/; if (illegalChars.test(dirName)) { alert('文件夹名称包含非法字符: \\ / : * ? " < > |'); return false; } // 不能以点开头或结尾(Linux/Mac隐藏目录) if (dirName.startsWith('.') || dirName.endsWith('.')) { alert('文件夹名称不能以点开头或结尾'); return false; } // 长度限制 if (dirName.length > 255) { alert('文件夹名称过长(最大255字符)'); return false; } // Windows保留名称 const upperName = dirName.toUpperCase(); const 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.includes(upperName)) { alert('文件夹名称是系统保留名称'); return false; } return true; } ``` ### 8.4.2 后端验证 ```java // 验证目录名(后端) private boolean isValidDirectoryName(String dirName) { if (dirName == null || dirName.isEmpty()) { return false; } // 去除首尾空格 dirName = dirName.trim(); // Windows非法字符 String illegalChars = "\\/:*?\"<>|"; for (char c : illegalChars.toCharArray()) { if (dirName.indexOf(c) != -1) { return false; } } // 不能以点开头或结尾 if (dirName.startsWith(".") || dirName.endsWith(".")) { return false; } // 长度限制 if (dirName.length() > 255) { return false; } // Windows保留名称 String upperName = dirName.toUpperCase(); String[] reservedNames = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}; for (String reserved : reservedNames) { if (upperName.equals(reserved)) { return false; } } return true; } ``` ### 8.4.3 路径处理 ```javascript // 拼接路径 function joinPath(parentPath, childName) { // 移除子名称的首尾空格 childName = childName.trim(); // 判断是否为本地路径 const isLocal = parentPath.includes('\\') || parentPath.includes('C:') || parentPath.includes('D:'); if (isLocal) { // Windows路径 if (parentPath.endsWith('\\') || parentPath.endsWith('/')) { return parentPath + childName; } else { return parentPath + '\\' + childName; } } else { // Linux/SFTP路径 if (parentPath.endsWith('/')) { return parentPath + childName; } else { return parentPath + '/' + childName; } } } ``` ## 实施步骤 1. **更新LocalFileService**:添加创建目录方法 2. **更新SftpService**:添加创建目录方法 3. **更新FileController**:添加创建目录接口 4. **添加前端创建目录功能** 5. **编译测试** ``` mvn clean compile ``` 6. **启动服务** ``` mvn spring-boot:run ``` ## 测试验证 ### 1. 创建单级目录 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \ -H "Content-Type: application/json" \ -d '{ "sessionId": "local", "path": "C:/test/newfolder" }' ``` ### 2. 创建多级目录 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir-p \ -H "Content-Type: application/json" \ -d '{ "sessionId": "local", "path": "C:/test/level1/level2/level3" }' ``` ### 3. 在SFTP上创建目录 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \ -H "Content-Type: application/json" \ -d '{ "sessionId": "sftp-uuid", "path": "/home/user/newfolder" }' ``` ### 4. 测试非法目录名 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \ -H "Content-Type: application/json" \ -d '{ "sessionId": "local", "path": "C:/test/folder