899
docs/09-双面板UI界面设计.md
Normal file
899
docs/09-双面板UI界面设计.md
Normal file
@@ -0,0 +1,899 @@
|
||||
# 模块09:双面板UI界面设计
|
||||
|
||||
---
|
||||
|
||||
## 🎨 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(标准布局)
|
||||
|
||||
---
|
||||
|
||||
## 9.1 界面布局
|
||||
|
||||
### 9.1.1 整体HTML结构
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SFTP文件管理器</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">SFTP文件管理器</span>
|
||||
<div>
|
||||
<button class="btn btn-primary btn-sm" onclick="showConnectionDialog()">连接管理</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar bg-light border-bottom">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="uploadFiles()">上传</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="downloadFiles()">下载</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="transferFiles()">传输到右侧</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFiles()">删除</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="showRenameDialog()">重命名</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="showMkdirDialog()">新建文件夹</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshPanels()">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 双面板区域 -->
|
||||
<div class="panels-container">
|
||||
<!-- 左面板 -->
|
||||
<div class="panel" id="left-panel">
|
||||
<div class="panel-header">
|
||||
<select class="form-select form-select-sm panel-mode" id="left-mode" onchange="onModeChange('left')">
|
||||
<option value="local">本地文件</option>
|
||||
<option value="sftp">SFTP服务器</option>
|
||||
</select>
|
||||
<select class="form-select form-select-sm connection-select" id="left-connection" style="display:none;" onchange="onConnectionChange('left')">
|
||||
<!-- SFTP连接列表动态生成 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="path-bar">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="goUp('left')">↑</button>
|
||||
<input type="text" class="form-control form-control-sm path-input" id="left-path" readonly>
|
||||
</div>
|
||||
<div class="file-list" id="left-file-list">
|
||||
<!-- 文件列表项动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右面板 -->
|
||||
<div class="panel" id="right-panel">
|
||||
<div class="panel-header">
|
||||
<select class="form-select form-select-sm panel-mode" id="right-mode" onchange="onModeChange('right')">
|
||||
<option value="local">本地文件</option>
|
||||
<option value="sftp">SFTP服务器</option>
|
||||
</select>
|
||||
<select class="form-select form-select-sm connection-select" id="right-connection" style="display:none;" onchange="onConnectionChange('right')">
|
||||
<!-- SFTP连接列表动态生成 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="path-bar">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="goUp('right')">↑</button>
|
||||
<input type="text" class="form-control form-control-sm path-input" id="right-path" readonly>
|
||||
</div>
|
||||
<div class="file-list" id="right-file-list">
|
||||
<!-- 文件列表项动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<div class="status-bar bg-light border-top">
|
||||
<span id="status-text">就绪</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接管理对话框 -->
|
||||
<div class="modal fade" id="connectionModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">连接管理</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-primary" onclick="showAddConnectionDialog()">添加连接</button>
|
||||
</div>
|
||||
<div class="connection-list" id="connection-list">
|
||||
<!-- 连接列表动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加连接对话框 -->
|
||||
<div class="modal fade" id="addConnectionModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">添加SFTP连接</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="connection-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">连接名称</label>
|
||||
<input type="text" class="form-control" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">主机地址</label>
|
||||
<input type="text" class="form-control" name="host" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">端口</label>
|
||||
<input type="number" class="form-control" name="port" value="22" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">用户名</label>
|
||||
<input type="text" class="form-control" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">密码</label>
|
||||
<input type="password" class="form-control" name="password">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">私钥路径(可选)</label>
|
||||
<input type="text" class="form-control" name="privateKeyPath">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">私钥密码(可选)</label>
|
||||
<input type="password" class="form-control" name="passPhrase">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveConnection()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## 9.2 样式设计
|
||||
|
||||
### 9.2.1 主样式(style.css)
|
||||
|
||||
```css
|
||||
/* 全局样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 14px;
|
||||
background-color: #f5f5f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 应用容器 */
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.navbar {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 双面板容器 */
|
||||
.panels-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 面板 */
|
||||
.panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #dee2e6;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 面板头部 */
|
||||
.panel-header {
|
||||
flex-shrink: 0;
|
||||
padding: 8px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.panel-mode {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.connection-select {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
/* 路径栏 */
|
||||
.path-bar {
|
||||
flex-shrink: 0;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.path-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 文件列表 */
|
||||
.file-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 文件项 */
|
||||
.file-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background-color 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
margin-left: 10px;
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.file-date {
|
||||
margin-left: 10px;
|
||||
min-width: 140px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 选中状态下的文件大小和日期 */
|
||||
.file-item.selected .file-size,
|
||||
.file-item.selected .file-date {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 状态栏 */
|
||||
.status-bar {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 16px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 上下文菜单 */
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1000;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 新建文件夹项 */
|
||||
.new-folder {
|
||||
background-color: #e7f3ff;
|
||||
}
|
||||
|
||||
.new-folder-input {
|
||||
width: 100%;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #007bff;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 连接列表 */
|
||||
.connection-item {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.connection-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.connection-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.connection-info {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.connection-actions button {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
```
|
||||
|
||||
## 9.3 JavaScript基础逻辑
|
||||
|
||||
### 9.3.1 状态管理(app.js)
|
||||
|
||||
```javascript
|
||||
// 面板状态
|
||||
const panelState = {
|
||||
left: {
|
||||
mode: 'local', // 'local' 或 'sftp'
|
||||
sessionId: 'local', // 'local' 或 SFTP会话ID
|
||||
currentPath: '', // 当前路径
|
||||
selectedFiles: [] // 选中的文件
|
||||
},
|
||||
right: {
|
||||
mode: 'local',
|
||||
sessionId: 'local',
|
||||
currentPath: '',
|
||||
selectedFiles: []
|
||||
}
|
||||
};
|
||||
|
||||
// 活跃连接列表
|
||||
let activeConnections = {};
|
||||
|
||||
// 初始化
|
||||
$(document).ready(function() {
|
||||
// 设置初始路径
|
||||
panelState.left.currentPath = getDefaultLocalPath();
|
||||
panelState.right.currentPath = getDefaultLocalPath();
|
||||
|
||||
// 加载文件列表
|
||||
loadFiles('left');
|
||||
loadFiles('right');
|
||||
|
||||
// 绑定键盘事件
|
||||
bindKeyboardEvents();
|
||||
});
|
||||
|
||||
// 获取默认本地路径
|
||||
function getDefaultLocalPath() {
|
||||
return System.getProperty('user.home');
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3.2 文件列表加载
|
||||
|
||||
```javascript
|
||||
// 加载文件列表
|
||||
function loadFiles(panelId) {
|
||||
const sessionId = panelState[panelId].sessionId;
|
||||
const path = panelState[panelId].currentPath;
|
||||
|
||||
updateStatus(`正在加载 ${panelId === 'left' ? '左' : '右'}面板文件列表...`);
|
||||
|
||||
$.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);
|
||||
updateStatus('就绪');
|
||||
} else {
|
||||
alert('加载文件列表失败: ' + response.message);
|
||||
updateStatus('错误: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
alert('加载文件列表失败: ' + error);
|
||||
updateStatus('错误: ' + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染文件列表
|
||||
function renderFileList(panelId, files) {
|
||||
const fileList = $(`#${panelId}-file-list`);
|
||||
fileList.empty();
|
||||
|
||||
if (files.length === 0) {
|
||||
fileList.html('<div class="text-center text-muted p-3">文件夹为空</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
files.forEach(file => {
|
||||
const icon = getFileIcon(file.name, 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" title="${file.name}">${file.name}</span>
|
||||
<span class="file-size">${size}</span>
|
||||
<span class="file-date">${date}</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// 单击选择
|
||||
item.on('click', function(e) {
|
||||
if (!e.ctrlKey) {
|
||||
$(`#${panelId}-file-list .file-item`).removeClass('selected');
|
||||
}
|
||||
item.toggleClass('selected');
|
||||
updateSelectedFiles(panelId);
|
||||
});
|
||||
|
||||
// 双击进入目录
|
||||
item.on('dblclick', function() {
|
||||
if (file.isDirectory) {
|
||||
enterDirectory(panelId, file.path);
|
||||
}
|
||||
});
|
||||
|
||||
// 右键菜单
|
||||
item.on('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
showContextMenu(e, panelId, file.path, file.isDirectory);
|
||||
});
|
||||
|
||||
fileList.append(item);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取文件图标
|
||||
function getFileIcon(fileName, isDirectory) {
|
||||
if (isDirectory) {
|
||||
return '📁';
|
||||
}
|
||||
|
||||
const ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
const iconMap = {
|
||||
'txt': '📄',
|
||||
'doc': '📄', 'docx': '📄',
|
||||
'xls': '📊', 'xlsx': '📊',
|
||||
'ppt': '📊', 'pptx': '📊',
|
||||
'pdf': '📕',
|
||||
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️',
|
||||
'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬',
|
||||
'mp3': '🎵', 'wav': '🎵',
|
||||
'zip': '📦', 'rar': '📦', '7z': '📦',
|
||||
'java': '☕', 'js': '💻', 'py': '🐍', 'php': '🐘',
|
||||
'html': '🌐', 'css': '🎨', 'json': '📋'
|
||||
};
|
||||
|
||||
return iconMap[ext] || '📄';
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
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', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新路径输入框
|
||||
function updatePathInput(panelId, path) {
|
||||
$(`#${panelId}-path`).val(path);
|
||||
}
|
||||
|
||||
// 更新选中文件列表
|
||||
function updateSelectedFiles(panelId) {
|
||||
const selected = $(`#${panelId}-file-list .file-item.selected`);
|
||||
panelState[panelId].selectedFiles = selected.map(function() {
|
||||
return $(this).data('path');
|
||||
}).get();
|
||||
}
|
||||
|
||||
// 更新状态栏
|
||||
function updateStatus(text) {
|
||||
$('#status-text').text(text);
|
||||
}
|
||||
```
|
||||
|
||||
## 9.4 响应式设计
|
||||
|
||||
### 9.4.1 移动端适配
|
||||
|
||||
```css
|
||||
/* 小屏幕设备 */
|
||||
@media (max-width: 768px) {
|
||||
.panels-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel {
|
||||
height: 50%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.file-size,
|
||||
.file-date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 极小屏幕设备 */
|
||||
@media (max-width: 576px) {
|
||||
.toolbar button {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9.5 交互设计
|
||||
|
||||
### 9.5.1 文件选择
|
||||
|
||||
```javascript
|
||||
// 全选
|
||||
function selectAll(panelId) {
|
||||
$(`#${panelId}-file-list .file-item`).addClass('selected');
|
||||
updateSelectedFiles(panelId);
|
||||
}
|
||||
|
||||
// 取消选择
|
||||
function deselectAll(panelId) {
|
||||
$(`#${panelId}-file-list .file-item`).removeClass('selected');
|
||||
updateSelectedFiles(panelId);
|
||||
}
|
||||
|
||||
// 反选
|
||||
function invertSelection(panelId) {
|
||||
$(`#${panelId}-file-list .file-item`).toggleClass('selected');
|
||||
updateSelectedFiles(panelId);
|
||||
}
|
||||
```
|
||||
|
||||
### 9.5.2 拖拽操作
|
||||
|
||||
```javascript
|
||||
// 文件拖拽初始化
|
||||
function initDragAndDrop() {
|
||||
$('.file-item').attr('draggable', true);
|
||||
|
||||
// 拖拽开始
|
||||
$('.file-item').on('dragstart', function(e) {
|
||||
const panelId = $(this).closest('.panel').attr('id');
|
||||
const path = $(this).data('path');
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
panelId: panelId,
|
||||
path: path
|
||||
}));
|
||||
$(this).addClass('dragging');
|
||||
});
|
||||
|
||||
// 拖拽结束
|
||||
$('.file-item').on('dragend', function() {
|
||||
$(this).removeClass('dragging');
|
||||
});
|
||||
|
||||
// 拖拽悬停
|
||||
$('.file-list').on('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
const panelId = $(this).closest('.panel').attr('id');
|
||||
$(this).addClass('drag-over');
|
||||
});
|
||||
|
||||
// 拖拽离开
|
||||
$('.file-list').on('dragleave', function() {
|
||||
$(this).removeClass('drag-over');
|
||||
});
|
||||
|
||||
// 拖拽放置
|
||||
$('.file-list').on('drop', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).removeClass('drag-over');
|
||||
|
||||
try {
|
||||
const data = JSON.parse(e.originalEvent.dataTransfer.getData('text/plain'));
|
||||
const targetPanelId = $(this).closest('.panel').attr('id');
|
||||
handleFileDrop(data, targetPanelId);
|
||||
} catch (err) {
|
||||
console.error('拖拽失败:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理文件拖放
|
||||
function handleFileDrop(data, targetPanelId) {
|
||||
const sourcePanelId = data.panelId;
|
||||
const sourcePath = data.path;
|
||||
const targetPath = panelState[targetPanelId].currentPath;
|
||||
|
||||
if (sourcePanelId === targetPanelId) {
|
||||
// 同一面板,可以移动文件
|
||||
// 这里可以实现文件移动功能
|
||||
alert('文件移动功能开发中...');
|
||||
} else {
|
||||
// 跨面板传输
|
||||
$.ajax({
|
||||
url: '/api/files/transfer',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
sourceSessionId: panelState[sourcePanelId].sessionId,
|
||||
sourcePath: sourcePath,
|
||||
targetSessionId: panelState[targetPanelId].sessionId,
|
||||
targetPath: targetPath
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('传输成功');
|
||||
loadFiles(targetPanelId);
|
||||
} else {
|
||||
alert('传输失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: handleError
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.5.3 快捷键支持
|
||||
|
||||
```javascript
|
||||
// 绑定键盘事件
|
||||
function bindKeyboardEvents() {
|
||||
$(document).on('keydown', function(e) {
|
||||
// Delete: 删除选中文件
|
||||
if (e.key === 'Delete') {
|
||||
deleteSelectedFiles(getActivePanelId());
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// F2: 重命名
|
||||
if (e.key === 'F2') {
|
||||
const panelId = getActivePanelId();
|
||||
const selected = panelState[panelId].selectedFiles;
|
||||
if (selected.length === 1) {
|
||||
showRenameDialog(panelId, selected[0]);
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// F5: 刷新
|
||||
if (e.key === 'F5') {
|
||||
refreshPanels();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Backspace: 返回上级目录
|
||||
if (e.key === 'Backspace') {
|
||||
const panelId = getActivePanelId();
|
||||
goUp(panelId);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Ctrl+A: 全选
|
||||
if (e.ctrlKey && (e.key === 'a' || e.key === 'A')) {
|
||||
selectAll(getActivePanelId());
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Esc: 取消选择
|
||||
if (e.key === 'Escape') {
|
||||
const panelId = getActivePanelId();
|
||||
deselectAll(panelId);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Ctrl+Shift+N: 新建文件夹
|
||||
if (e.ctrlKey && e.shiftKey && (e.key === 'n' || e.key === 'N')) {
|
||||
showMkdirDialog(getActivePanelId());
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前活动面板
|
||||
function getActivePanelId() {
|
||||
// 简单实现:返回左侧面板
|
||||
// 可以根据鼠标位置等逻辑判断
|
||||
return 'left';
|
||||
}
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. **创建index.html**:包含完整的HTML结构
|
||||
|
||||
2. **创建style.css**:编写样式文件
|
||||
|
||||
3. **创建app.js**:编写JavaScript基础逻辑
|
||||
|
||||
4. **测试界面**:
|
||||
```
|
||||
# 将文件放置到正确位置
|
||||
cp index.html src/main/resources/templates/
|
||||
cp style.css src/main/resources/static/css/
|
||||
cp app.js src/main/resources/static/js/
|
||||
|
||||
# 启动服务
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
5. **访问测试**:http://localhost:8080/sftp-manager
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **CDN资源**:确保网络可以访问Bootstrap和jQuery的CDN
|
||||
2. **静态资源**:确保静态文件路径正确
|
||||
3. **浏览器兼容性**:测试主流浏览器兼容性
|
||||
4. **响应式**:在不同屏幕尺寸下测试界面
|
||||
5. **性能优化**:大文件列表考虑虚拟滚动
|
||||
|
||||
## 下一步
|
||||
|
||||
完成模块09后,继续模块10:模式切换功能
|
||||
Reference in New Issue
Block a user