Files
sftp-manager/docs/07-文件重命名功能.md
liu 14289beb66 Initial commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 10:10:11 +08:00

607 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块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新建文件夹功能