15 KiB
15 KiB
模块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重命名方法
// 重命名文件
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重命名方法
// 重命名文件
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重命名接口
// 重命名文件
@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 重命名交互
// 显示重命名对话框
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 重命名按钮
<div class="toolbar">
<button onclick="showRenameDialog('left', getSelectedPath('left'))">重命名</button>
</div>
// 获取选中文件的路径
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 右键菜单重命名
// 在右键菜单中添加重命名选项
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 在线编辑(高级功能)
// 双击文件名进行重命名
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 前端验证
// 验证文件名
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 保留扩展名
// 提取扩展名
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 同名检查
// 检查同名文件是否存在
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
});
}
实施步骤
-
更新LocalFileService:添加重命名和验证方法
-
更新SftpService:添加重命名方法
-
更新FileController:添加重命名接口
-
添加前端重命名功能
-
编译测试
mvn clean compile -
启动服务
mvn spring-boot:run
测试验证
1. 重命名文件
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. 测试非法文件名
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. 测试重名
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"
}'
注意事项
- 文件名验证:严格验证文件名,防止非法字符
- 同名检查:重命名前检查目标名称是否已存在
- 扩展名处理:可选择保留原扩展名
- 用户确认:重名时需要用户确认是否覆盖
- 路径处理:正确处理不同操作系统的路径分隔符
下一步
完成模块07后,继续模块08:新建文件夹功能