# 模块04:文件浏览功能 --- ## 🎨 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(标准布局) --- ## 4.1 功能概述 实现本地文件系统和SFTP服务器文件系统的浏览功能,支持目录导航、文件列表展示。 ## 4.2 后端设计 ### 4.2.1 LocalFileService本地文件服务 ```java package com.sftp.manager.service; import com.sftp.manager.model.FileInfo; import org.springframework.stereotype.Service; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; @Service public class LocalFileService { public List listFiles(String path) throws Exception { List files = new ArrayList<>(); File directory = new File(path); if (!directory.exists() || !directory.isDirectory()) { throw new Exception("目录不存在: " + path); } File[] fileArray = directory.listFiles(); if (fileArray != null) { for (File file : fileArray) { FileInfo fileInfo = new FileInfo(); fileInfo.setName(file.getName()); fileInfo.setPath(file.getAbsolutePath()); fileInfo.setSize(file.length()); fileInfo.setIsDirectory(file.isDirectory()); // 获取修改时间 BasicFileAttributes attrs = Files.readAttributes( file.toPath(), BasicFileAttributes.class); fileInfo.setModifiedTime(LocalDateTime.ofInstant( attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault())); // 设置权限(仅Windows) if (file.canRead() && file.canWrite()) { fileInfo.setPermissions("-rw-r--r--"); } else if (file.canRead()) { fileInfo.setPermissions("-r--r--r--"); } files.add(fileInfo); } } return files; } public boolean fileExists(String path) { return new File(path).exists(); } public FileInfo getFileInfo(String path) throws Exception { File file = new File(path); if (!file.exists()) { throw new Exception("文件不存在: " + path); } FileInfo fileInfo = new FileInfo(); fileInfo.setName(file.getName()); fileInfo.setPath(file.getAbsolutePath()); fileInfo.setSize(file.length()); fileInfo.setIsDirectory(file.isDirectory()); BasicFileAttributes attrs = Files.readAttributes( file.toPath(), BasicFileAttributes.class); fileInfo.setModifiedTime(LocalDateTime.ofInstant( attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault())); return fileInfo; } public String getParentPath(String path) { File file = new File(path); return file.getParent(); } } ``` ### 4.2.2 SftpService SFTP文件服务 ```java package com.sftp.manager.service; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.SftpException; import com.sftp.manager.model.FileInfo; import com.sftp.manager.service.SessionManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.Vector; @Service public class SftpService { @Autowired private SessionManager sessionManager; public List listFiles(String sessionId, String path) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { List files = new ArrayList<>(); Vector entries = channel.ls(path); for (ChannelSftp.LsEntry entry : entries) { String fileName = entry.getFilename(); // 跳过.和.. if (".".equals(fileName) || "..".equals(fileName)) { continue; } FileInfo fileInfo = new FileInfo(); fileInfo.setName(fileName); fileInfo.setPath(path.endsWith("/") ? path + fileName : path + "/" + fileName); ChannelSftp.LsEntry attrs = entry; fileInfo.setSize(attrs.getSize()); fileInfo.setIsDirectory(attrs.getAttrs().isDir()); // 获取修改时间 int mtime = attrs.getAttrs().getMTime(); fileInfo.setModifiedTime(LocalDateTime.ofInstant( Instant.ofEpochSecond(mtime), ZoneId.systemDefault())); // 获取权限 fileInfo.setPermissions(attrs.getAttrs().getPermissionsString()); files.add(fileInfo); } return files; } catch (SftpException e) { throw new Exception("列出文件失败: " + e.getMessage(), e); } } public String pwd(String sessionId) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { return channel.pwd(); } catch (SftpException e) { throw new Exception("获取当前路径失败: " + e.getMessage(), e); } } public void cd(String sessionId, String path) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { channel.cd(path); } catch (SftpException e) { throw new Exception("切换目录失败: " + e.getMessage(), e); } } public FileInfo getFileInfo(String sessionId, String path) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { ChannelSftp.LsEntry entry = channel.stat(path); FileInfo fileInfo = new FileInfo(); fileInfo.setName(entry.getFilename()); fileInfo.setPath(path); fileInfo.setSize(entry.getSize()); fileInfo.setIsDirectory(entry.getAttrs().isDir()); int mtime = entry.getAttrs().getMTime(); fileInfo.setModifiedTime(LocalDateTime.ofInstant( Instant.ofEpochSecond(mtime), ZoneId.systemDefault())); fileInfo.setPermissions(entry.getAttrs().getPermissionsString()); return fileInfo; } catch (SftpException e) { throw new Exception("获取文件信息失败: " + e.getMessage(), e); } } } ``` ### 4.2.3 FileController文件控制器 ```java package com.sftp.manager.controller; import com.sftp.manager.dto.ApiResponse; import com.sftp.manager.dto.FileListRequest; import com.sftp.manager.dto.FileOperationRequest; import com.sftp.manager.model.FileInfo; import com.sftp.manager.service.LocalFileService; import com.sftp.manager.service.SftpService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/files") @CrossOrigin(origins = "*") public class FileController { @Autowired private LocalFileService localFileService; @Autowired private SftpService sftpService; @PostMapping("/list") public ApiResponse> listFiles(@RequestBody FileListRequest request) { try { String sessionId = request.getSessionId(); String path = request.getPath(); List files; if ("local".equals(sessionId)) { files = localFileService.listFiles(path); } else { files = sftpService.listFiles(sessionId, path); } return ApiResponse.success("查询成功", files); } catch (Exception e) { return ApiResponse.error("列出文件失败: " + e.getMessage()); } } @PostMapping("/info") public ApiResponse getFileInfo(@RequestBody FileOperationRequest request) { try { String sessionId = request.getSessionId(); String path = request.getPath(); FileInfo fileInfo; if ("local".equals(sessionId)) { fileInfo = localFileService.getFileInfo(path); } else { fileInfo = sftpService.getFileInfo(sessionId, path); } return ApiResponse.success("查询成功", fileInfo); } catch (Exception e) { return ApiResponse.error("获取文件信息失败: " + e.getMessage()); } } @GetMapping("/path") public ApiResponse> getCurrentPath(@RequestParam String sessionId) { try { Map result = new java.util.HashMap<>(); if ("local".equals(sessionId)) { result.put("path", System.getProperty("user.home")); } else { String path = sftpService.pwd(sessionId); result.put("path", path); } return ApiResponse.success("查询成功", result); } catch (Exception e) { return ApiResponse.error("获取路径失败: " + e.getMessage()); } } } ``` ## 4.3 前端设计 ### 4.3.1 文件列表展示 ```html
``` ### 4.3.2 CSS样式 ```css .file-list { flex: 1; overflow-y: auto; background-color: #fff; } .file-item { padding: 8px 10px; cursor: pointer; display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; transition: background-color 0.2s; } .file-item:hover { background-color: #f8f9fa; } .file-item.selected { background-color: #007bff; color: white; } .file-icon { margin-right: 10px; width: 20px; text-align: center; } .file-name { flex: 1; } .file-size { margin-left: 10px; font-size: 12px; color: #666; } .file-date { margin-left: 10px; font-size: 12px; color: #666; } ``` ### 4.3.3 JavaScript实现 ```javascript // 文件列表加载 function loadFiles(panelId) { const sessionId = panelState[panelId].sessionId; const path = panelState[panelId].currentPath; $.ajax({ url: '/api/files/list', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sessionId: sessionId, path: path }), success: function(response) { if (response.success) { renderFileList(panelId, response.data); updatePathInput(panelId, path); } else { alert(response.message); } }, error: handleError }); } // 渲染文件列表 function renderFileList(panelId, files) { const fileList = $(`#${panelId}-file-list`); fileList.empty(); files.forEach(file => { const icon = file.isDirectory ? '📁' : '📄'; const size = file.isDirectory ? '' : formatFileSize(file.size); const date = formatDate(file.modifiedTime); const item = $(`
${icon} ${file.name} ${size} ${date}
`); item.on('click', function() { selectFile(panelId, $(this)); }); item.on('dblclick', function() { if (file.isDirectory) { enterDirectory(panelId, file.path); } }); fileList.append(item); }); } // 进入目录 function enterDirectory(panelId, path) { panelState[panelId].currentPath = path; loadFiles(panelId); } // 返回上级目录 function goUp(panelId) { const currentPath = panelState[panelId].currentPath; const parentPath = getParentPath(currentPath); if (parentPath && parentPath !== currentPath) { panelState[panelId].currentPath = parentPath; loadFiles(panelId); } } // 选择文件 function selectFile(panelId, element) { $(`#${panelId}-file-list .file-item`).removeClass('selected'); element.addClass('selected'); const path = element.data('path'); panelState[panelId].selectedFiles = [path]; } // 格式化文件大小 function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } // 格式化日期 function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleString('zh-CN'); } // 获取父路径 function getParentPath(path) { if (path === '/' || path.indexOf('/') === -1) { return path; } return path.substring(0, path.lastIndexOf('/')); } // 更新路径输入框 function updatePathInput(panelId, path) { $(`#${panelId}-path`).val(path); } ``` ## 4.4 特殊处理 ### 4.4.1 路径分隔符处理 - Windows系统:使用反斜杠(\) - Linux/SFTP系统:使用正斜杠(/) - 统一显示格式,底层自动转换 ### 4.4.2 隐藏文件处理 - 本地文件:可选择过滤以.开头的文件(Linux/Mac) - SFTP文件:由服务器返回 ### 4.4.3 文件图标 根据文件类型显示不同图标: - 目录:📁 - 普通文件:📄 - 图片:🖼️ - 视频文件:🎬 - 音频文件:🎵 - 压缩文件:📦 - 代码文件:💻 ## 实施步骤 1. **创建LocalFileService服务** ``` touch src/main/java/com/sftp/manager/service/LocalFileService.java ``` 2. **创建SftpService服务** ``` touch src/main/java/com/sftp/manager/service/SftpService.java ``` 3. **创建FileController控制器** ``` touch src/main/java/com/sftp/manager/controller/FileController.java ``` 4. **编译测试** ``` mvn clean compile ``` 5. **启动服务** ``` mvn spring-boot:run ``` ## 测试验证 ### 1. 测试本地文件列表 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/list \ -H "Content-Type: application/json" \ -d '{"sessionId":"local","path":"C:/Users"}' ``` ### 2. 测试SFTP文件列表 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/list \ -H "Content-Type: application/json" \ -d '{"sessionId":"sftp-uuid","path":"/home/user"}' ``` ### 3. 获取当前路径 ```bash curl "http://localhost:8080/sftp-manager/api/files/path?sessionId=local" ``` ## 注意事项 1. **权限检查**:确保有权限访问指定路径 2. **路径安全**:防止路径遍历攻击 3. **性能优化**:大目录可考虑分页加载 4. **错误处理**:友好提示文件不存在或无权限 ## 下一步 完成模块04后,继续模块05:文件上传下载功能