606
docs/07-文件重命名功能.md
Normal file
606
docs/07-文件重命名功能.md
Normal 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
|
||||
- 标准间距: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<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:新建文件夹功能
|
||||
Reference in New Issue
Block a user