17 KiB
17 KiB
模块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本地文件服务
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文件服务
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文件控制器
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 文件列表展示
<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样式
.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实现
// 文件列表加载
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 文件图标
根据文件类型显示不同图标:
- 目录:📁
- 普通文件:📄
- 图片:🖼️
- 视频文件:🎬
- 音频文件:🎵
- 压缩文件:📦
- 代码文件:💻
实施步骤
-
创建LocalFileService服务
touch src/main/java/com/sftp/manager/service/LocalFileService.java -
创建SftpService服务
touch src/main/java/com/sftp/manager/service/SftpService.java -
创建FileController控制器
touch src/main/java/com/sftp/manager/controller/FileController.java -
编译测试
mvn clean compile -
启动服务
mvn spring-boot:run
测试验证
1. 测试本地文件列表
curl -X POST http://localhost:8080/sftp-manager/api/files/list \
-H "Content-Type: application/json" \
-d '{"sessionId":"local","path":"C:/Users"}'
2. 测试SFTP文件列表
curl -X POST http://localhost:8080/sftp-manager/api/files/list \
-H "Content-Type: application/json" \
-d '{"sessionId":"sftp-uuid","path":"/home/user"}'
3. 获取当前路径
curl "http://localhost:8080/sftp-manager/api/files/path?sessionId=local"
注意事项
- 权限检查:确保有权限访问指定路径
- 路径安全:防止路径遍历攻击
- 性能优化:大目录可考虑分页加载
- 错误处理:友好提示文件不存在或无权限
下一步
完成模块04后,继续模块05:文件上传下载功能