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,686 @@
# 模块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
- 标准间距16px1rem
- 组件内边距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创建目录方法
```java
// 创建目录
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创建目录方法
```java
// 创建目录
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创建目录接口
```java
// 创建目录
@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 新建文件夹交互
```javascript
// 显示新建文件夹对话框
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 新建文件夹按钮
```html
<div class="toolbar">
<button onclick="showMkdirDialog('left')">新建文件夹</button>
</div>
```
### 8.3.3 右键菜单新建文件夹
```javascript
// 在右键菜单中添加新建文件夹选项
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 快捷键支持
```javascript
// 监听键盘事件
$(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 在线输入(高级功能)
```javascript
// 在文件列表中直接输入新文件夹名称
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 前端验证
```javascript
// 验证目录名
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 后端验证
```java
// 验证目录名(后端)
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 路径处理
```javascript
// 拼接路径
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;
}
}
}
```
## 实施步骤
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/mkdir \
-H "Content-Type: application/json" \
-d '{
"sessionId": "local",
"path": "C:/test/newfolder"
}'
```
### 2. 创建多级目录
```bash
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上创建目录
```bash
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. 测试非法目录名
```bash
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \
-H "Content-Type: application/json" \
-d '{
"sessionId": "local",
"path": "C:/test/folder<name"
}'
```
## 注意事项
1. **目录名验证**:严格验证目录名,防止非法字符
2. **路径拼接**:正确处理不同操作系统的路径分隔符
3. **权限检查**:确保有权限在指定位置创建目录
4. **同名检查**:创建前检查目录是否已存在
5. **父目录存在性**:单级创建时检查父目录是否存在
## 下一步
完成模块08后继续模块09双面板UI界面设计