17 KiB
17 KiB
模块08:新建文件夹功能
🎨 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(标准布局)
8.1 功能概述
实现在本地文件系统和SFTP服务器上创建新文件夹的功能。
8.2 后端设计
8.2.1 LocalFileService创建目录方法
// 创建目录
public boolean createDirectory(String path) throws Exception {
File directory = new File(path);
if (directory.exists()) {
throw new Exception("目录已存在: " + path);
}
// 验证目录名
String dirName = directory.getName();
if (!isValidDirectoryName(dirName)) {
throw new Exception("目录名包含非法字符: " + dirName);
}
// 检查父目录是否存在
File parentDir = directory.getParentFile();
if (parentDir != null && !parentDir.exists()) {
// 父目录不存在,询问是否创建多级目录
throw new Exception("父目录不存在: " + parentDir.getPath());
}
boolean result = directory.mkdirs();
if (!result) {
throw new Exception("创建目录失败");
}
return true;
}
// 创建多级目录
public boolean createDirectories(String path) throws Exception {
File directory = new File(path);
if (directory.exists()) {
throw new Exception("目录已存在: " + path);
}
// 验证路径中的每个目录名
String[] parts = path.split("[/\\\\]");
for (String part : parts) {
if (!isValidDirectoryName(part)) {
throw new Exception("路径包含非法字符: " + part);
}
}
boolean result = directory.mkdirs();
if (!result) {
throw new Exception("创建目录失败");
}
return true;
}
// 验证目录名是否有效
private boolean isValidDirectoryName(String dirName) {
if (dirName == null || dirName.isEmpty()) {
return false;
}
// Windows非法字符
String illegalChars = "\\/:*?\"<>|";
for (char c : illegalChars.toCharArray()) {
if (dirName.indexOf(c) != -1) {
return false;
}
}
// 检查长度限制
if (dirName.length() > 255) {
return false;
}
// 检查保留名称(Windows)
String upperName = dirName.toUpperCase();
String[] reservedNames = {"CON", "PRN", "AUX", "NUL"};
for (String reserved : reservedNames) {
if (upperName.equals(reserved)) {
return false;
}
}
return true;
}
8.2.2 SftpService创建目录方法
// 创建目录
public boolean createDirectory(String sessionId, String path) throws Exception {
ChannelSftp channel = sessionManager.getSession(sessionId);
if (channel == null) {
throw new Exception("会话不存在或已断开");
}
try {
// 检查目录是否已存在
try {
channel.stat(path);
throw new Exception("目录已存在: " + path);
} catch (SftpException e) {
if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
throw e;
}
}
// 创建目录
channel.mkdir(path);
return true;
} catch (SftpException e) {
throw new Exception("创建目录失败: " + e.getMessage(), e);
}
}
// 创建多级目录
public boolean createDirectories(String sessionId, String path) throws Exception {
ChannelSftp channel = sessionManager.getSession(sessionId);
if (channel == null) {
throw new Exception("会话不存在或已断开");
}
try {
// 检查目录是否已存在
try {
channel.stat(path);
throw new Exception("目录已存在: " + path);
} catch (SftpException e) {
if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
throw e;
}
}
// 递归创建多级目录
createDirectoriesRecursive(channel, path);
return true;
} catch (SftpException e) {
throw new Exception("创建目录失败: " + e.getMessage(), e);
}
}
// 递归创建多级目录
private void createDirectoriesRecursive(ChannelSftp channel, String path) throws SftpException {
// 如果是根目录,直接返回
if (path.equals("/") || path.isEmpty()) {
return;
}
// 检查父目录是否存在
String parentPath = getParentPath(path);
if (!parentPath.equals(path)) {
try {
channel.stat(parentPath);
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
// 父目录不存在,递归创建
createDirectoriesRecursive(channel, parentPath);
} else {
throw e;
}
}
}
// 创建当前目录
channel.mkdir(path);
}
// 获取父路径
private String getParentPath(String path) {
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
int index = path.lastIndexOf("/");
if (index == -1) {
return "/";
}
if (index == 0) {
return "/";
}
return path.substring(0, index);
}
8.2.3 FileController创建目录接口
// 创建目录
@PostMapping("/mkdir")
public ApiResponse<Void> createDirectory(@RequestBody Map<String, String> request) {
try {
String sessionId = request.get("sessionId");
String path = request.get("path");
if (path == null || path.isEmpty()) {
return ApiResponse.error("路径不能为空");
}
boolean result;
if ("local".equals(sessionId)) {
result = localFileService.createDirectory(path);
} else {
result = sftpService.createDirectory(sessionId, path);
}
if (result) {
return ApiResponse.success("创建成功", null);
} else {
return ApiResponse.error("创建失败");
}
} catch (Exception e) {
return ApiResponse.error("创建失败: " + e.getMessage());
}
}
// 创建多级目录
@PostMapping("/mkdir-p")
public ApiResponse<Void> createDirectories(@RequestBody Map<String, String> request) {
try {
String sessionId = request.get("sessionId");
String path = request.get("path");
if (path == null || path.isEmpty()) {
return ApiResponse.error("路径不能为空");
}
boolean result;
if ("local".equals(sessionId)) {
result = localFileService.createDirectories(path);
} else {
result = sftpService.createDirectories(sessionId, path);
}
if (result) {
return ApiResponse.success("创建成功", null);
} else {
return ApiResponse.error("创建失败");
}
} catch (Exception e) {
return ApiResponse.error("创建失败: " + e.getMessage());
}
}
8.3 前端设计
8.3.1 新建文件夹交互
// 显示新建文件夹对话框
function showMkdirDialog(panelId) {
const sessionId = panelState[panelId].sessionId;
const currentPath = panelState[panelId].currentPath;
const folderName = prompt('请输入文件夹名称:', '新建文件夹');
if (folderName && folderName.trim() !== '') {
createDirectory(sessionId, currentPath, folderName);
}
}
// 创建目录
function createDirectory(sessionId, parentPath, folderName) {
// 验证文件夹名称
if (!validateDirectoryName(folderName)) {
return;
}
// 构建完整路径
let fullPath;
if ("local".equals(sessionId)) {
fullPath = parentPath + File.separator + folderName;
} else {
fullPath = parentPath.endsWith('/') ?
parentPath + folderName :
parentPath + '/' + folderName;
}
$.ajax({
url: '/api/files/mkdir',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
sessionId: sessionId,
path: fullPath
}),
success: function(response) {
if (response.success) {
alert('文件夹创建成功');
refreshCurrentPanel();
} else {
alert('创建失败: ' + response.message);
}
},
error: handleError
});
}
// 验证目录名
function validateDirectoryName(dirName) {
if (!dirName || dirName.trim() === '') {
alert('文件夹名称不能为空');
return false;
}
// Windows非法字符
const illegalChars = /\\\/:\*\?"<>\|/;
if (illegalChars.test(dirName)) {
alert('文件夹名称包含非法字符');
return false;
}
// 长度限制
if (dirName.length > 255) {
alert('文件夹名称过长(最大255字符)');
return false;
}
return true;
}
// 刷新当前面板
function refreshCurrentPanel() {
const activePanelId = getActivePanelId();
loadFiles(activePanelId);
}
8.3.2 新建文件夹按钮
<div class="toolbar">
<button onclick="showMkdirDialog('left')">新建文件夹</button>
</div>
8.3.3 右键菜单新建文件夹
// 在右键菜单中添加新建文件夹选项
function showContextMenu(event, panelId) {
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="showMkdirDialog(\'' +
panelId + '\')">新建文件夹</div>');
$('body').append(menu);
$(document).one('click', function() {
menu.remove();
});
}
8.3.4 快捷键支持
// 监听键盘事件
$(document).on('keydown', function(e) {
// Ctrl+Shift+N: 新建文件夹
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
const activePanelId = getActivePanelId();
showMkdirDialog(activePanelId);
e.preventDefault();
}
});
8.3.5 在线输入(高级功能)
// 在文件列表中直接输入新文件夹名称
function showInlineMkdir(panelId) {
const fileList = $(`#${panelId}-file-list`);
// 创建新建文件夹项
const newItem = $(`
<div class="file-item new-folder">
<span class="file-icon">📁</span>
<span class="file-name">
<input type="text" class="new-folder-input" placeholder="输入文件夹名称">
</span>
</div>
`);
// 插入到列表顶部
fileList.prepend(newItem);
// 聚焦输入框
const input = newItem.find('.new-folder-input');
input.focus();
// 失去焦点时创建
input.on('blur', function() {
const folderName = input.val().trim();
if (folderName) {
createDirectory(panelState[panelId].sessionId,
panelState[panelId].currentPath,
folderName);
}
newItem.remove();
});
// 按Enter创建
input.on('keypress', function(e) {
if (e.which === 13) {
input.blur();
}
});
// 按Esc取消
input.on('keydown', function(e) {
if (e.which === 27) {
newItem.remove();
}
});
}
8.4 输入验证
8.4.1 前端验证
// 验证目录名
function validateDirectoryName(dirName) {
// 基本验证
if (!dirName || dirName.trim() === '') {
alert('文件夹名称不能为空');
return false;
}
// 去除首尾空格
dirName = dirName.trim();
// Windows非法字符
const illegalChars = /\\\/:\*\?"<>\|/;
if (illegalChars.test(dirName)) {
alert('文件夹名称包含非法字符: \\ / : * ? " < > |');
return false;
}
// 不能以点开头或结尾(Linux/Mac隐藏目录)
if (dirName.startsWith('.') || dirName.endsWith('.')) {
alert('文件夹名称不能以点开头或结尾');
return false;
}
// 长度限制
if (dirName.length > 255) {
alert('文件夹名称过长(最大255字符)');
return false;
}
// Windows保留名称
const upperName = dirName.toUpperCase();
const reservedNames = ['CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
if (reservedNames.includes(upperName)) {
alert('文件夹名称是系统保留名称');
return false;
}
return true;
}
8.4.2 后端验证
// 验证目录名(后端)
private boolean isValidDirectoryName(String dirName) {
if (dirName == null || dirName.isEmpty()) {
return false;
}
// 去除首尾空格
dirName = dirName.trim();
// Windows非法字符
String illegalChars = "\\/:*?\"<>|";
for (char c : illegalChars.toCharArray()) {
if (dirName.indexOf(c) != -1) {
return false;
}
}
// 不能以点开头或结尾
if (dirName.startsWith(".") || dirName.endsWith(".")) {
return false;
}
// 长度限制
if (dirName.length() > 255) {
return false;
}
// Windows保留名称
String upperName = dirName.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;
}
8.4.3 路径处理
// 拼接路径
function joinPath(parentPath, childName) {
// 移除子名称的首尾空格
childName = childName.trim();
// 判断是否为本地路径
const isLocal = parentPath.includes('\\') ||
parentPath.includes('C:') ||
parentPath.includes('D:');
if (isLocal) {
// Windows路径
if (parentPath.endsWith('\\') || parentPath.endsWith('/')) {
return parentPath + childName;
} else {
return parentPath + '\\' + childName;
}
} else {
// Linux/SFTP路径
if (parentPath.endsWith('/')) {
return parentPath + childName;
} else {
return parentPath + '/' + childName;
}
}
}
实施步骤
-
更新LocalFileService:添加创建目录方法
-
更新SftpService:添加创建目录方法
-
更新FileController:添加创建目录接口
-
添加前端创建目录功能
-
编译测试
mvn clean compile -
启动服务
mvn spring-boot:run
测试验证
1. 创建单级目录
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \
-H "Content-Type: application/json" \
-d '{
"sessionId": "local",
"path": "C:/test/newfolder"
}'
2. 创建多级目录
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir-p \
-H "Content-Type: application/json" \
-d '{
"sessionId": "local",
"path": "C:/test/level1/level2/level3"
}'
3. 在SFTP上创建目录
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \
-H "Content-Type: application/json" \
-d '{
"sessionId": "sftp-uuid",
"path": "/home/user/newfolder"
}'
4. 测试非法目录名
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \
-H "Content-Type: application/json" \
-d '{
"sessionId": "local",
"path": "C:/test/folder<name"
}'
注意事项
- 目录名验证:严格验证目录名,防止非法字符
- 路径拼接:正确处理不同操作系统的路径分隔符
- 权限检查:确保有权限在指定位置创建目录
- 同名检查:创建前检查目录是否已存在
- 父目录存在性:单级创建时检查父目录是否存在
下一步
完成模块08后,继续模块09:双面板UI界面设计