24 KiB
24 KiB
模块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结构
<!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)
/* 全局样式 */
* {
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)
// 面板状态
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 文件列表加载
// 加载文件列表
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 移动端适配
/* 小屏幕设备 */
@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 文件选择
// 全选
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 拖拽操作
// 文件拖拽初始化
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 快捷键支持
// 绑定键盘事件
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';
}
实施步骤
-
创建index.html:包含完整的HTML结构
-
创建style.css:编写样式文件
-
创建app.js:编写JavaScript基础逻辑
-
测试界面:
# 将文件放置到正确位置 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
注意事项
- CDN资源:确保网络可以访问Bootstrap和jQuery的CDN
- 静态资源:确保静态文件路径正确
- 浏览器兼容性:测试主流浏览器兼容性
- 响应式:在不同屏幕尺寸下测试界面
- 性能优化:大文件列表考虑虚拟滚动
下一步
完成模块09后,继续模块10:模式切换功能