Initial commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
liu
2026-02-03 10:10:11 +08:00
commit 14289beb66
45 changed files with 15479 additions and 0 deletions

View File

@@ -0,0 +1,606 @@
# 模块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
- 标准间距16px1rem
- 组件内边距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<Void> renameFile(@RequestBody Map<String, String> 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
<div class="toolbar">
<button onclick="showRenameDialog('left', getSelectedPath('left'))">重命名</button>
</div>
```
```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 = $('<div class="context-menu">');
menu.css({
position: 'absolute',
left: event.pageX + 'px',
top: event.pageY + 'px'
});
menu.append('<div class="menu-item" onclick="deleteFile(\'' +
panelId + '\', \'' + path + '\')">删除</div>');
menu.append('<div class="menu-item" onclick="showRenameDialog(\'' +
panelId + '\', \'' + path + '\')">重命名</div>');
$('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 type="text" class="rename-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新建文件夹功能