687 lines
17 KiB
Markdown
687 lines
17 KiB
Markdown
# 模块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创建目录方法
|
||
|
||
```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界面设计
|