533 lines
14 KiB
Markdown
533 lines
14 KiB
Markdown
# 模块06:文件删除功能
|
||
|
||
---
|
||
|
||
## 🎨 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(标准布局)
|
||
|
||
---
|
||
|
||
## 6.1 功能概述
|
||
实现删除本地文件和SFTP服务器上文件的功能,支持单个文件删除和批量删除,包含删除确认机制。
|
||
|
||
## 6.2 后端设计
|
||
|
||
### 6.2.1 LocalFileService删除方法
|
||
|
||
```java
|
||
// 删除单个文件或目录
|
||
public boolean deleteFile(String path) throws Exception {
|
||
File file = new File(path);
|
||
if (!file.exists()) {
|
||
throw new Exception("文件不存在: " + path);
|
||
}
|
||
|
||
if (file.isDirectory()) {
|
||
return deleteDirectory(file);
|
||
} else {
|
||
return file.delete();
|
||
}
|
||
}
|
||
|
||
// 递归删除目录
|
||
private boolean deleteDirectory(File directory) throws Exception {
|
||
File[] files = directory.listFiles();
|
||
if (files != null) {
|
||
for (File file : files) {
|
||
if (file.isDirectory()) {
|
||
deleteDirectory(file);
|
||
} else {
|
||
if (!file.delete()) {
|
||
throw new Exception("删除文件失败: " + file.getPath());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return directory.delete();
|
||
}
|
||
|
||
// 批量删除
|
||
public BatchDeleteResult batchDelete(List<String> paths) {
|
||
BatchDeleteResult result = new BatchDeleteResult();
|
||
int successCount = 0;
|
||
int failCount = 0;
|
||
List<String> failedFiles = new ArrayList<>();
|
||
|
||
for (String path : paths) {
|
||
try {
|
||
deleteFile(path);
|
||
successCount++;
|
||
} catch (Exception e) {
|
||
failCount++;
|
||
failedFiles.add(path + " - " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
result.setSuccessCount(successCount);
|
||
result.setFailCount(failCount);
|
||
result.setFailedFiles(failedFiles);
|
||
return result;
|
||
}
|
||
|
||
// 批量删除结果类
|
||
public static class BatchDeleteResult {
|
||
private int successCount;
|
||
private int failCount;
|
||
private List<String> failedFiles;
|
||
|
||
// getters and setters
|
||
}
|
||
```
|
||
|
||
### 6.2.2 SftpService删除方法
|
||
|
||
```java
|
||
// 删除单个文件或目录
|
||
public boolean deleteFile(String sessionId, String path) throws Exception {
|
||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||
if (channel == null) {
|
||
throw new Exception("会话不存在或已断开");
|
||
}
|
||
|
||
try {
|
||
FileInfo fileInfo = getFileInfo(sessionId, path);
|
||
if (fileInfo.isDirectory()) {
|
||
deleteDirectoryRecursive(sessionId, path);
|
||
} else {
|
||
channel.rm(path);
|
||
}
|
||
return true;
|
||
} catch (SftpException e) {
|
||
throw new Exception("删除失败: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
// 递归删除SFTP目录
|
||
private void deleteDirectoryRecursive(String sessionId, String path) throws Exception {
|
||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||
if (channel == null) {
|
||
throw new Exception("会话不存在或已断开");
|
||
}
|
||
|
||
try {
|
||
// 列出目录内容
|
||
Vector<ChannelSftp.LsEntry> entries = channel.ls(path);
|
||
|
||
for (ChannelSftp.LsEntry entry : entries) {
|
||
String fileName = entry.getFilename();
|
||
if (".".equals(fileName) || "..".equals(fileName)) {
|
||
continue;
|
||
}
|
||
|
||
String fullPath = path.endsWith("/") ?
|
||
path + fileName :
|
||
path + "/" + fileName;
|
||
|
||
if (entry.getAttrs().isDir()) {
|
||
// 递归删除子目录
|
||
deleteDirectoryRecursive(sessionId, fullPath);
|
||
} else {
|
||
// 删除文件
|
||
channel.rm(fullPath);
|
||
}
|
||
}
|
||
|
||
// 删除空目录
|
||
channel.rmdir(path);
|
||
} catch (SftpException e) {
|
||
throw new Exception("删除目录失败: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
// 批量删除
|
||
public BatchDeleteResult batchDelete(String sessionId, List<String> paths) {
|
||
BatchDeleteResult result = new BatchDeleteResult();
|
||
int successCount = 0;
|
||
int failCount = 0;
|
||
List<String> failedFiles = new ArrayList<>();
|
||
|
||
for (String path : paths) {
|
||
try {
|
||
deleteFile(sessionId, path);
|
||
successCount++;
|
||
} catch (Exception e) {
|
||
failCount++;
|
||
failedFiles.add(path + " - " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
result.setSuccessCount(successCount);
|
||
result.setFailCount(failCount);
|
||
result.setFailedFiles(failedFiles);
|
||
return result;
|
||
}
|
||
```
|
||
|
||
### 6.2.3 FileController删除接口
|
||
|
||
```java
|
||
// 删除单个文件
|
||
@DeleteMapping("/delete")
|
||
public ApiResponse<Void> deleteFile(@RequestParam String sessionId,
|
||
@RequestParam String path) {
|
||
try {
|
||
if ("local".equals(sessionId)) {
|
||
localFileService.deleteFile(path);
|
||
} else {
|
||
sftpService.deleteFile(sessionId, path);
|
||
}
|
||
return ApiResponse.success("删除成功", null);
|
||
} catch (Exception e) {
|
||
return ApiResponse.error("删除失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
// 批量删除
|
||
@PostMapping("/batch-delete")
|
||
public ApiResponse<LocalFileService.BatchDeleteResult> batchDelete(@RequestBody Map<String, Object> request) {
|
||
try {
|
||
String sessionId = (String) request.get("sessionId");
|
||
@SuppressWarnings("unchecked")
|
||
List<String> paths = (List<String>) request.get("paths");
|
||
|
||
LocalFileService.BatchDeleteResult result;
|
||
if ("local".equals(sessionId)) {
|
||
result = localFileService.batchDelete(paths);
|
||
} else {
|
||
result = sftpService.batchDelete(sessionId, paths);
|
||
}
|
||
|
||
return ApiResponse.success("删除完成", result);
|
||
} catch (Exception e) {
|
||
return ApiResponse.error("批量删除失败: " + e.getMessage());
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6.3 前端设计
|
||
|
||
### 6.3.1 删除交互
|
||
|
||
```javascript
|
||
// 删除选中的文件
|
||
function deleteSelectedFiles(panelId) {
|
||
const selectedFiles = panelState[panelId].selectedFiles;
|
||
|
||
if (selectedFiles.length === 0) {
|
||
alert('请先选择要删除的文件');
|
||
return;
|
||
}
|
||
|
||
const sessionId = panelState[panelId].sessionId;
|
||
|
||
// 确认对话框
|
||
let message;
|
||
if (selectedFiles.length === 1) {
|
||
const fileName = getFileNameFromPath(selectedFiles[0]);
|
||
message = `确定要删除 "${fileName}" 吗?`;
|
||
} else {
|
||
message = `确定要删除选中的 ${selectedFiles.length} 个文件吗?`;
|
||
}
|
||
|
||
if (confirm(message)) {
|
||
deleteFiles(sessionId, selectedFiles);
|
||
}
|
||
}
|
||
|
||
// 删除文件
|
||
function deleteFiles(sessionId, paths) {
|
||
if (paths.length === 1) {
|
||
// 单个删除
|
||
$.ajax({
|
||
url: '/api/files/delete',
|
||
method: 'DELETE',
|
||
data: {
|
||
sessionId: sessionId,
|
||
path: paths[0]
|
||
},
|
||
success: function(response) {
|
||
if (response.success) {
|
||
alert('删除成功');
|
||
refreshCurrentPanel();
|
||
} else {
|
||
alert('删除失败: ' + response.message);
|
||
}
|
||
},
|
||
error: handleError
|
||
});
|
||
} else {
|
||
// 批量删除
|
||
$.ajax({
|
||
url: '/api/files/batch-delete',
|
||
method: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({
|
||
sessionId: sessionId,
|
||
paths: paths
|
||
}),
|
||
success: function(response) {
|
||
if (response.success) {
|
||
const result = response.data;
|
||
let message = `成功删除 ${result.successCount} 个文件`;
|
||
if (result.failCount > 0) {
|
||
message += `,失败 ${result.failCount} 个\n`;
|
||
message += '失败详情:\n' + result.failedFiles.join('\n');
|
||
}
|
||
alert(message);
|
||
refreshCurrentPanel();
|
||
} else {
|
||
alert('批量删除失败: ' + response.message);
|
||
}
|
||
},
|
||
error: handleError
|
||
});
|
||
}
|
||
}
|
||
|
||
// 刷新当前面板
|
||
function refreshCurrentPanel() {
|
||
const activePanelId = getActivePanelId();
|
||
loadFiles(activePanelId);
|
||
}
|
||
|
||
// 从路径获取文件名
|
||
function getFileNameFromPath(path) {
|
||
const index = path.lastIndexOf('/');
|
||
if (index === -1) {
|
||
return path;
|
||
}
|
||
return path.substring(index + 1);
|
||
}
|
||
```
|
||
|
||
### 6.3.2 删除按钮
|
||
|
||
```html
|
||
<div class="toolbar">
|
||
<button onclick="deleteSelectedFiles('left')">删除左侧</button>
|
||
<button onclick="deleteSelectedFiles('right')">删除右侧</button>
|
||
</div>
|
||
```
|
||
|
||
### 6.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="renameFile(\'' +
|
||
panelId + '\', \'' + path + '\')">重命名</div>');
|
||
|
||
$('body').append(menu);
|
||
|
||
// 点击其他地方关闭菜单
|
||
$(document).one('click', function() {
|
||
menu.remove();
|
||
});
|
||
}
|
||
|
||
// 删除单个文件(通过右键菜单)
|
||
function deleteFile(panelId, path) {
|
||
const sessionId = panelState[panelId].sessionId;
|
||
const fileName = getFileNameFromPath(path);
|
||
|
||
if (confirm(`确定要删除 "${fileName}" 吗?`)) {
|
||
deleteFiles(sessionId, [path]);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6.4 安全措施
|
||
|
||
### 6.4.1 删除确认
|
||
|
||
- 必须用户确认后才能执行删除
|
||
- 显示将被删除的文件数量和名称
|
||
- 防止误操作
|
||
|
||
### 6.4.2 权限检查
|
||
|
||
```java
|
||
// 检查文件删除权限
|
||
private void checkDeletePermission(File file) throws Exception {
|
||
if (!file.canWrite()) {
|
||
throw new Exception("没有删除权限: " + file.getPath());
|
||
}
|
||
|
||
// 检查是否为系统文件
|
||
String systemPaths = "C:\\Windows,C:\\Program Files,C:\\System32";
|
||
String[] paths = systemPaths.split(",");
|
||
for (String systemPath : paths) {
|
||
if (file.getPath().toLowerCase().startsWith(systemPath.toLowerCase())) {
|
||
throw new Exception("系统文件,禁止删除: " + file.getPath());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.4.3 操作日志
|
||
|
||
```java
|
||
// 记录删除操作
|
||
@Autowired
|
||
private OperationLogService logService;
|
||
|
||
public boolean deleteFile(String path) throws Exception {
|
||
File file = new File(path);
|
||
|
||
try {
|
||
checkDeletePermission(file);
|
||
boolean result = deleteFileInternal(file);
|
||
|
||
// 记录操作日志
|
||
logService.logOperation("delete", "local", path, null, result, null);
|
||
|
||
return result;
|
||
} catch (Exception e) {
|
||
// 记录失败日志
|
||
logService.logOperation("delete", "local", path, null, false, e.getMessage());
|
||
throw e;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.4.4 回收站机制(可选)
|
||
|
||
```java
|
||
// 移动到回收站而不是直接删除
|
||
public boolean moveToRecycleBin(String path) throws Exception {
|
||
File file = new File(path);
|
||
if (!file.exists()) {
|
||
throw new Exception("文件不存在: " + path);
|
||
}
|
||
|
||
// 创建回收站目录
|
||
File recycleBin = new File(getRecycleBinPath());
|
||
if (!recycleBin.exists()) {
|
||
recycleBin.mkdirs();
|
||
}
|
||
|
||
// 生成唯一文件名(避免重名)
|
||
String newPath = recycleBin.getPath() + File.separator +
|
||
file.getName() + "_" + System.currentTimeMillis();
|
||
|
||
return file.renameTo(new File(newPath));
|
||
}
|
||
|
||
private String getRecycleBinPath() {
|
||
return System.getProperty("user.home") + File.separator + ".sftp-manager-recycle";
|
||
}
|
||
```
|
||
|
||
## 实施步骤
|
||
|
||
1. **更新LocalFileService**:添加删除方法
|
||
|
||
2. **更新SftpService**:添加删除方法
|
||
|
||
3. **更新FileController**:添加删除接口
|
||
|
||
4. **添加前端删除功能**
|
||
|
||
5. **编译测试**
|
||
```
|
||
mvn clean compile
|
||
```
|
||
|
||
6. **启动服务**
|
||
```
|
||
mvn spring-boot:run
|
||
```
|
||
|
||
## 测试验证
|
||
|
||
### 1. 删除单个文件
|
||
|
||
```bash
|
||
curl -X DELETE "http://localhost:8080/sftp-manager/api/files/delete?sessionId=local&path=C:/test/file.txt"
|
||
```
|
||
|
||
### 2. 批量删除
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/sftp-manager/api/files/batch-delete \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"sessionId": "local",
|
||
"paths": ["C:/test/file1.txt", "C:/test/file2.txt"]
|
||
}'
|
||
```
|
||
|
||
### 3. 删除目录
|
||
|
||
```bash
|
||
curl -X DELETE "http://localhost:8080/sftp-manager/api/files/delete?sessionId=local&path=C:/test/folder"
|
||
```
|
||
|
||
## 注意事项
|
||
|
||
1. **递归删除**:删除目录时需要递归删除所有子文件和子目录
|
||
2. **权限检查**:确保有删除权限,避免删除系统文件
|
||
3. **错误处理**:部分文件删除失败时,继续删除其他文件,最后返回结果
|
||
4. **确认机制**:必须用户确认后才能执行删除
|
||
5. **日志记录**:记录所有删除操作,便于审计
|
||
|
||
## 下一步
|
||
|
||
完成模块06后,继续模块07:文件重命名功能
|