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,632 @@
# 模块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
- 标准间距16px1rem
- 组件内边距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<FileInfo> listFiles(String path) throws Exception {
List<FileInfo> 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<FileInfo> listFiles(String sessionId, String path) throws Exception {
ChannelSftp channel = sessionManager.getSession(sessionId);
if (channel == null) {
throw new Exception("会话不存在或已断开");
}
try {
List<FileInfo> files = new ArrayList<>();
Vector<ChannelSftp.LsEntry> 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<List<FileInfo>> listFiles(@RequestBody FileListRequest request) {
try {
String sessionId = request.getSessionId();
String path = request.getPath();
List<FileInfo> 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<FileInfo> 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<Map<String, String>> getCurrentPath(@RequestParam String sessionId) {
try {
Map<String, String> 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
<div class="panel" id="left-panel">
<div class="panel-header">
<select class="panel-mode" onchange="onModeChange('left')">
<option value="local">本地文件</option>
<option value="sftp">SFTP服务器</option>
</select>
<select class="connection-select" style="display:none;" onchange="onConnectionChange('left')">
<!-- SFTP连接列表 -->
</select>
</div>
<div class="path-bar">
<button onclick="goUp('left')"></button>
<input type="text" class="path-input" id="left-path" readonly>
</div>
<div class="file-list" id="left-file-list">
<!-- 文件列表项 -->
</div>
</div>
```
### 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 = $(`
<div class="file-item" data-path="${file.path}" data-is-dir="${file.isDirectory}">
<span class="file-icon">${icon}</span>
<span class="file-name">${file.name}</span>
<span class="file-size">${size}</span>
<span class="file-date">${date}</span>
</div>
`);
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文件上传下载功能