# 模块07:文件重命名功能 --- ## 🎨 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(标准布局) --- ## 7.1 功能概述 实现重命名本地文件和SFTP服务器上文件的功能,支持单个文件重命名。 ## 7.2 后端设计 ### 7.2.1 LocalFileService重命名方法 ```java // 重命名文件 public boolean renameFile(String oldPath, String newPath) throws Exception { File oldFile = new File(oldPath); File newFile = new File(newPath); if (!oldFile.exists()) { throw new Exception("源文件不存在: " + oldPath); } if (newFile.exists()) { throw new Exception("目标文件已存在: " + newPath); } // 检查新文件名是否有效 String newFileName = newFile.getName(); if (!isValidFileName(newFileName)) { throw new Exception("文件名包含非法字符: " + newFileName); } boolean result = oldFile.renameTo(newFile); if (!result) { throw new Exception("重命名失败"); } return true; } // 验证文件名是否有效 private boolean isValidFileName(String fileName) { if (fileName == null || fileName.isEmpty()) { return false; } // Windows非法字符 String illegalChars = "\\/:*?\"<>|"; for (char c : illegalChars.toCharArray()) { if (fileName.indexOf(c) != -1) { return false; } } // 检查长度限制 if (fileName.length() > 255) { return false; } // 检查保留名称(Windows) String upperName = fileName.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; } // 获取文件扩展名 public String getFileExtension(String fileName) { int index = fileName.lastIndexOf('.'); if (index == -1 || index == fileName.length() - 1) { return ""; } return fileName.substring(index); } // 获取文件名(不带扩展名) public String getFileNameWithoutExtension(String fileName) { int index = fileName.lastIndexOf('.'); if (index == -1) { return fileName; } return fileName.substring(0, index); } ``` ### 7.2.2 SftpService重命名方法 ```java // 重命名文件 public boolean renameFile(String sessionId, String oldPath, String newPath) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { // 检查源文件是否存在 channel.stat(oldPath); // 检查目标文件是否已存在 try { channel.stat(newPath); throw new Exception("目标文件已存在: " + newPath); } catch (SftpException e) { // 文件不存在,可以重命名 } // 执行重命名 channel.rename(oldPath, newPath); return true; } catch (SftpException e) { throw new Exception("重命名失败: " + e.getMessage(), e); } } ``` ### 7.2.3 FileController重命名接口 ```java // 重命名文件 @PostMapping("/rename") public ApiResponse renameFile(@RequestBody Map request) { try { String sessionId = request.get("sessionId"); String oldPath = request.get("oldPath"); String newName = request.get("newName"); if (newName == null || newName.isEmpty()) { return ApiResponse.error("新文件名不能为空"); } // 构建新路径 String newPath; if ("local".equals(sessionId)) { File oldFile = new File(oldPath); File parentDir = oldFile.getParentFile(); newPath = parentDir.getPath() + File.separator + newName; localFileService.renameFile(oldPath, newPath); } else { // 获取父目录 String parentPath = getParentPath(oldPath); newPath = parentPath.endsWith("/") ? parentPath + newName : parentPath + "/" + newName; sftpService.renameFile(sessionId, oldPath, newPath); } return ApiResponse.success("重命名成功", null); } catch (Exception e) { return ApiResponse.error("重命名失败: " + e.getMessage()); } } // 获取父路径 private String getParentPath(String path) { if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } int index = path.lastIndexOf("/"); if (index == -1) { return "/"; } return path.substring(0, index); } ``` ## 7.3 前端设计 ### 7.3.1 重命名交互 ```javascript // 显示重命名对话框 function showRenameDialog(panelId, path) { const sessionId = panelState[panelId].sessionId; const oldName = getFileNameFromPath(path); const newName = prompt('请输入新文件名:', oldName); if (newName && newName !== oldName) { renameFile(sessionId, path, newName); } } // 重命名文件 function renameFile(sessionId, oldPath, newName) { $.ajax({ url: '/api/files/rename', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sessionId: sessionId, oldPath: oldPath, newName: newName }), success: function(response) { if (response.success) { alert('重命名成功'); refreshCurrentPanel(); } else { alert('重命名失败: ' + response.message); } }, error: handleError }); } // 从路径获取文件名 function getFileNameFromPath(path) { let separator = path.includes('\\') ? '\\' : '/'; const index = path.lastIndexOf(separator); if (index === -1) { return path; } return path.substring(index + 1); } // 刷新当前面板 function refreshCurrentPanel() { const activePanelId = getActivePanelId(); loadFiles(activePanelId); } // 获取当前活动面板ID function getActivePanelId() { return 'left'; // 默认返回左侧,可根据实际逻辑调整 } ``` ### 7.3.2 重命名按钮 ```html
``` ```javascript // 获取选中文件的路径 function getSelectedPath(panelId) { const selectedFiles = panelState[panelId].selectedFiles; if (selectedFiles.length === 0) { alert('请先选择一个文件'); return null; } if (selectedFiles.length > 1) { alert('一次只能重命名一个文件'); return null; } return selectedFiles[0]; } ``` ### 7.3.3 右键菜单重命名 ```javascript // 在右键菜单中添加重命名选项 function showContextMenu(event, panelId, path) { event.preventDefault(); const menu = $('
'); menu.css({ position: 'absolute', left: event.pageX + 'px', top: event.pageY + 'px' }); menu.append(''); menu.append(''); $('body').append(menu); $(document).one('click', function() { menu.remove(); }); } ``` ### 7.3.4 在线编辑(高级功能) ```javascript // 双击文件名进行重命名 function enableInlineRename() { $('.file-item').on('dblclick', function(e) { const fileItem = $(this); const isDirectory = fileItem.data('is-dir'); // 如果是目录,进入目录;如果是文件,进入重命名模式 if (!isDirectory) { const path = fileItem.data('path'); showRenameDialogInline(fileItem, path); } }); } // 在线重命名 function showRenameDialogInline(fileItem, path) { const nameElement = fileItem.find('.file-name'); const currentName = nameElement.text(); // 创建输入框 const input = $(''); input.val(currentName); // 替换文件名显示为输入框 nameElement.html(input); input.focus(); input.select(); // 失去焦点时保存 input.on('blur', function() { const newName = input.val(); if (newName && newName !== currentName) { renameFile(panelState.left.sessionId, path, newName); } else { // 取消重命名,恢复原名称 nameElement.text(currentName); } }); // 按Enter保存 input.on('keypress', function(e) { if (e.which === 13) { input.blur(); } }); // 按Esc取消 input.on('keydown', function(e) { if (e.which === 27) { nameElement.text(currentName); } }); } ``` ## 7.4 输入验证 ### 7.4.1 前端验证 ```javascript // 验证文件名 function validateFileName(fileName) { if (!fileName || fileName.trim() === '') { alert('文件名不能为空'); return false; } // Windows非法字符 const illegalChars = /\\\/:\*\?"<>\|/; if (illegalChars.test(fileName)) { alert('文件名包含非法字符'); return false; } // 长度限制 if (fileName.length > 255) { alert('文件名过长(最大255字符)'); return false; } return true; } // 修改重命名对话框 function showRenameDialog(panelId, path) { const oldName = getFileNameFromPath(path); while (true) { const newName = prompt('请输入新文件名:', oldName); if (newName === null) { // 用户取消 return; } if (newName === '' || newName === oldName) { // 无效输入 continue; } if (validateFileName(newName)) { renameFile(panelState[panelId].sessionId, path, newName); break; } } } ``` ### 7.4.2 保留扩展名 ```javascript // 提取扩展名 function getFileExtension(fileName) { const index = fileName.lastIndexOf('.'); if (index === -1 || index === fileName.length - 1) { return ''; } return fileName.substring(index); } // 修改重命名对话框(保留扩展名) function showRenameDialog(panelId, path) { const sessionId = panelState[panelId].sessionId; const oldName = getFileNameFromPath(path); const extension = getFileExtension(oldName); const baseName = oldName.substring(0, oldName.length - extension.length); const newName = prompt('请输入新文件名:', baseName); if (newName && newName !== baseName) { // 保留原扩展名 const fullName = newName + extension; renameFile(sessionId, path, fullName); } } ``` ### 7.4.3 同名检查 ```javascript // 检查同名文件是否存在 function checkFileExists(sessionId, parentPath, fileName, callback) { const fullPath = parentPath.endsWith('/') ? parentPath + fileName : parentPath + '/' + fileName; $.ajax({ url: '/api/files/info', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sessionId: sessionId, path: fullPath }), success: function(response) { // 文件存在 callback(true); }, error: function() { // 文件不存在 callback(false); } }); } // 重命名时检查同名 function renameFile(sessionId, oldPath, newName) { const parentPath = getParentPath(oldPath); checkFileExists(sessionId, parentPath, newName, function(exists) { if (exists) { if (confirm('目标文件已存在,是否覆盖?')) { doRename(sessionId, oldPath, newName); } } else { doRename(sessionId, oldPath, newName); } }); } function doRename(sessionId, oldPath, newName) { // 原重命名逻辑 $.ajax({ url: '/api/files/rename', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sessionId: sessionId, oldPath: oldPath, newName: newName }), success: function(response) { if (response.success) { alert('重命名成功'); refreshCurrentPanel(); } else { alert('重命名失败: ' + response.message); } }, error: handleError }); } ``` ## 实施步骤 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/rename \ -H "Content-Type: application/json" \ -d '{ "sessionId": "local", "oldPath": "C:/test/old.txt", "newName": "new.txt" }' ``` ### 2. 测试非法文件名 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/rename \ -H "Content-Type: application/json" \ -d '{ "sessionId": "local", "oldPath": "C:/test/test.txt", "newName": "test<.txt" }' ``` ### 3. 测试重名 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/rename \ -H "Content-Type: application/json" \ -d '{ "sessionId": "local", "oldPath": "C:/test/file1.txt", "newName": "file2.txt" }' ``` ## 注意事项 1. **文件名验证**:严格验证文件名,防止非法字符 2. **同名检查**:重命名前检查目标名称是否已存在 3. **扩展名处理**:可选择保留原扩展名 4. **用户确认**:重名时需要用户确认是否覆盖 5. **路径处理**:正确处理不同操作系统的路径分隔符 ## 下一步 完成模块07后,继续模块08:新建文件夹功能