14 KiB
14 KiB
模块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删除方法
// 删除单个文件或目录
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删除方法
// 删除单个文件或目录
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删除接口
// 删除单个文件
@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 删除交互
// 删除选中的文件
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 删除按钮
<div class="toolbar">
<button onclick="deleteSelectedFiles('left')">删除左侧</button>
<button onclick="deleteSelectedFiles('right')">删除右侧</button>
</div>
6.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="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 权限检查
// 检查文件删除权限
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 操作日志
// 记录删除操作
@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 回收站机制(可选)
// 移动到回收站而不是直接删除
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";
}
实施步骤
-
更新LocalFileService:添加删除方法
-
更新SftpService:添加删除方法
-
更新FileController:添加删除接口
-
添加前端删除功能
-
编译测试
mvn clean compile -
启动服务
mvn spring-boot:run
测试验证
1. 删除单个文件
curl -X DELETE "http://localhost:8080/sftp-manager/api/files/delete?sessionId=local&path=C:/test/file.txt"
2. 批量删除
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. 删除目录
curl -X DELETE "http://localhost:8080/sftp-manager/api/files/delete?sessionId=local&path=C:/test/folder"
注意事项
- 递归删除:删除目录时需要递归删除所有子文件和子目录
- 权限检查:确保有删除权限,避免删除系统文件
- 错误处理:部分文件删除失败时,继续删除其他文件,最后返回结果
- 确认机制:必须用户确认后才能执行删除
- 日志记录:记录所有删除操作,便于审计
下一步
完成模块06后,继续模块07:文件重命名功能