diff --git a/src/main/java/com/sftp/manager/config/WebConfig.java b/src/main/java/com/sftp/manager/config/WebConfig.java index 1503732..ef92f23 100644 --- a/src/main/java/com/sftp/manager/config/WebConfig.java +++ b/src/main/java/com/sftp/manager/config/WebConfig.java @@ -3,6 +3,7 @@ package com.sftp.manager.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @@ -23,4 +24,10 @@ public class WebConfig implements WebMvcConfigurer { // registry.addResourceHandler("/**") // .addResourceLocations("classpath:/static/"); } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + // 访问根路径时转发到静态首页 /static/index.html + registry.addViewController("/").setViewName("forward:/index.html"); + } } diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index ac16ec0..fd232d1 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -6,11 +6,32 @@ box-sizing: border-box; } +:root { + /* 设计令牌(简化版) */ + --color-primary: #0d6efd; + --color-success: #198754; + --color-warning: #ffc107; + --color-danger: #dc3545; + --color-dark: #0f172a; + --color-gray-700: #334155; + --color-gray-600: #6b7280; + --color-gray-200: #e5e7eb; + --color-gray-100: #f3f4f6; + + --radius-sm: 4px; + --radius-md: 12px; + --shadow-card: 0 22px 55px rgba(15, 23, 42, 0.18); +} + body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; line-height: 1.5; - background-color: #f5f5f5; + color: var(--color-dark); + background: + radial-gradient(circle at top left, #e0f2fe 0, transparent 55%), + radial-gradient(circle at bottom right, #e9d5ff 0, transparent 55%), + #f3f4f6; overflow: hidden; } @@ -18,8 +39,13 @@ body { .app-container { display: flex; flex-direction: column; - height: 100vh; - background-color: #fff; + height: calc(100vh - 32px); + max-width: 1220px; + margin: 16px auto; + background-color: #ffffff; + border-radius: var(--radius-md); + box-shadow: var(--shadow-card); + overflow: hidden; } /* 导航栏 */ @@ -42,6 +68,8 @@ body { align-items: center; gap: 8px; min-height: 44px; + background-color: #f9fafb; + border-bottom: 1px solid #e5e7eb; } /* 传输进度(上传/下载/跨面板传输) */ @@ -89,6 +117,7 @@ body { flex-direction: column; border-right: 1px solid #dee2e6; overflow: hidden; + background-color: #ffffff; min-width: 0; } @@ -96,6 +125,11 @@ body { border-right: none; } +/* 当前活动面板高亮 */ +.panel.active-panel { + box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.4); +} + /* 拖拽上传效果 */ .panel.drag-over { background-color: #e7f3ff; @@ -233,14 +267,45 @@ body { color: white; } +.file-item.selected .file-name { + color: #ffffff; +} + .file-icon { margin-right: 10px; - width: 24px; + width: 40px; text-align: center; font-size: 16px; flex-shrink: 0; } +/* 文件类型徽标(替代 emoji 图标) */ +.file-type { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 34px; + padding: 2px 8px; + border-radius: 999px; + background-color: rgba(15, 23, 42, 0.06); + color: #0f172a; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.file-type-dir { + background-color: rgba(37, 99, 235, 0.15); + color: #1d4ed8; +} + +.file-item.selected .file-type, +.file-item.selected .file-type-dir { + background-color: rgba(15, 23, 42, 0.32); + color: #ffffff; +} + .file-name { flex: 1; white-space: nowrap; @@ -278,6 +343,8 @@ body { padding: 4px 16px; font-size: 12px; color: #666; + background-color: #f9fafb; + border-top: 1px solid #e5e7eb; } /* 上下文菜单 */ @@ -346,6 +413,57 @@ body { font-size: 14px; } +/* 页面头部信息区域 */ +.app-header { + padding: 12px 20px 4px; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + background: linear-gradient(120deg, rgba(37, 99, 235, 0.07), rgba(56, 189, 248, 0.04)); + border-bottom: 1px solid rgba(148, 163, 184, 0.35); +} + +.app-header-main { + max-width: 70%; +} + +.app-title { + font-size: 18px; + font-weight: 600; + color: #0f172a; + margin-bottom: 4px; +} + +.app-subtitle { + font-size: 13px; + color: #6b7280; + margin-bottom: 0; +} + +.app-header-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + justify-content: flex-end; +} + +.header-tag { + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + color: #1e40af; + background-color: rgba(59, 130, 246, 0.08); + border: 1px solid rgba(59, 130, 246, 0.18); + white-space: nowrap; +} + +/* 全局聚焦可见样式(键盘导航) */ +*:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + /* 响应式设计 */ @media (max-width: 768px) { .panels-container { @@ -382,3 +500,200 @@ body { padding: 6px 8px; } } + +/* ============================ + ui-ux-pro-max 主题覆盖样式 + ============================ */ + +/* 设计令牌(Minimal + Gold) */ +:root { + --color-primary: #171717; + --color-secondary: #404040; + --color-cta: #D4AF37; + + --color-text: #171717; + --color-bg: #ffffff; + + --radius-sm: 4px; + --radius-md: 14px; + --shadow-card: 0 24px 60px rgba(15, 23, 42, 0.18); + + --transition-base: 150ms ease; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + color: var(--color-text, #171717); + background-color: var(--color-bg, #ffffff); + background-image: + radial-gradient(circle at top left, rgba(23, 23, 23, 0.04) 0, transparent 55%), + radial-gradient(circle at bottom right, rgba(64, 64, 64, 0.04) 0, transparent 55%); + -webkit-font-smoothing: antialiased; +} + +.app-container { + border-radius: var(--radius-md); + box-shadow: var(--shadow-card); + border: 1px solid rgba(15, 23, 42, 0.06); +} + +.navbar { + background-color: #171717 !important; + border-bottom: 1px solid #27272a; +} + +.navbar-brand { + letter-spacing: 0.02em; +} + +.navbar .btn-primary.btn-sm { + background-color: var(--color-cta, #D4AF37); + border-color: var(--color-cta, #D4AF37); + color: #171717; + font-weight: 500; + padding-inline: 14px; + border-radius: 999px; + transition: background-color var(--transition-base), border-color var(--transition-base), transform var(--transition-base), box-shadow var(--transition-base); +} + +.navbar .btn-primary.btn-sm:hover { + background-color: #b48b1f; + border-color: #b48b1f; + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25); +} + +.navbar .btn-primary.btn-sm:active { + transform: translateY(0); + box-shadow: none; +} + +.toolbar { + background-color: #f9fafb; + border-bottom: 1px solid #e5e7eb; +} + +.toolbar .btn-group { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.toolbar .btn { + border-radius: 999px; + padding: 4px 14px; + font-size: 13px; + font-weight: 500; + border-width: 1px; + border-style: solid; + border-color: rgba(148, 163, 184, 0.55); + background-color: #ffffff; + color: var(--color-secondary, #404040); + transition: background-color var(--transition-base), color var(--transition-base), border-color var(--transition-base), box-shadow var(--transition-base), transform var(--transition-base); +} + +.toolbar .btn-outline-primary { + border-color: rgba(148, 163, 184, 0.7); +} + +.toolbar .btn-outline-primary:hover { + background-color: var(--color-primary); + color: #ffffff; + border-color: var(--color-primary); + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.18); +} + +.toolbar .btn-outline-secondary { + border-color: rgba(148, 163, 184, 0.55); + color: var(--color-secondary); + background-color: #f9fafb; +} + +.toolbar .btn-outline-secondary:hover { + background-color: #e5e7eb; + border-color: #9ca3af; +} + +.toolbar .btn-outline-danger { + border-color: rgba(220, 38, 38, 0.35); + color: #b91c1c; + background-color: #fef2f2; +} + +.toolbar .btn-outline-danger:hover { + background-color: #dc2626; + border-color: #b91c1c; + color: #ffffff; +} + +.toolbar .btn:active { + transform: scale(0.97); + box-shadow: none; +} + +.panel { + border-right: 1px solid #e5e7eb; +} + +.panel.active-panel { + box-shadow: inset 0 0 0 1px rgba(212, 175, 55, 0.65); +} + +.panel-header { + background-color: #f3f4f6; + border-bottom: 1px solid #e5e7eb; +} + +.path-bar { + background-color: #ffffff; + border-bottom: 1px solid #e5e7eb; +} + +.file-item { + transition: background-color var(--transition-base), color var(--transition-base); +} + +.app-header { + background: linear-gradient(120deg, rgba(23, 23, 23, 0.03), rgba(64, 64, 64, 0.02)); +} + +.app-title { + font-size: 20px; +} + +.header-tag { + color: #404040; + background-color: #f4f4f5; + border: 1px solid #e4e4e7; +} + +/* 主按钮:统一金色 CTA 风格 */ +.btn-primary { + background-color: var(--color-cta, #D4AF37); + border-color: var(--color-cta, #D4AF37); + color: #171717; + font-weight: 500; + transition: background-color var(--transition-base), border-color var(--transition-base), box-shadow var(--transition-base), transform var(--transition-base); +} + +.btn-primary:hover { + background-color: #b48b1f; + border-color: #b48b1f; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.25); + transform: translateY(-1px); +} + +.btn-primary:active { + transform: translateY(0); + box-shadow: none; +} + +/* 尊重用户的「减少动态效果」系统设置 */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 71354b7..facef69 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -19,6 +19,19 @@ + +
+
+

文件工作台

+

双面板本地 / SFTP 文件管理器,支持拖拽、键盘快捷键与批量操作。

+
+
+ 双面板布局 + SFTP / 本地 + 拖拽上传 +
+
+
@@ -96,7 +109,7 @@
- 就绪 + 就绪
diff --git a/src/main/resources/static/js/app.js b/src/main/resources/static/js/app.js index 07c6b30..2d276bf 100644 --- a/src/main/resources/static/js/app.js +++ b/src/main/resources/static/js/app.js @@ -45,12 +45,22 @@ function initApp() { // 绑定键盘事件 bindKeyboardEvents(); + // 默认高亮左侧面板 + setActivePanel('left'); + // 文件项拖拽(跨面板传输) initDragAndDrop(); // 面板点击切换活动面板 - $('#left-panel').on('click', function() { activePanelId = 'left'; }); - $('#right-panel').on('click', function() { activePanelId = 'right'; }); + $('#left-panel').on('click', function() { setActivePanel('left'); }); + $('#right-panel').on('click', function() { setActivePanel('right'); }); +} + +// 设置当前活动面板并应用高亮样式 +function setActivePanel(panelId) { + activePanelId = panelId; + $('#left-panel, #right-panel').removeClass('active-panel'); + $('#' + panelId + '-panel').addClass('active-panel'); } // 初始化面板路径 @@ -188,11 +198,12 @@ function renderFileList(panelId, files) { } }); - // 右键菜单:删除 + // 右键菜单 item.on('contextmenu', function(e) { e.preventDefault(); const itemPath = $(this).data('path'); - if (itemPath) showContextMenu(e, panelId, itemPath); + const itemIsDir = $(this).data('is-dir'); + if (itemPath) showContextMenu(e, panelId, itemPath, itemIsDir); }); item.attr('draggable', true); @@ -202,21 +213,27 @@ function renderFileList(panelId, files) { // 获取文件图标 function getFileIcon(fileName, isDirectory) { - if (isDirectory) return '📁'; + if (isDirectory) { + return 'DIR'; + } 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': '📋' + const typeMap = { + 'txt': 'TXT', 'md': 'TXT', + 'doc': 'DOC', 'docx': 'DOC', + 'xls': 'XLS', 'xlsx': 'XLS', + 'ppt': 'PPT', 'pptx': 'PPT', + 'pdf': 'PDF', + 'jpg': 'IMG', 'jpeg': 'IMG', 'png': 'IMG', 'gif': 'IMG', 'webp': 'IMG', + 'mp4': 'VID', 'avi': 'VID', 'mkv': 'VID', 'mov': 'VID', + 'mp3': 'AUD', 'wav': 'AUD', 'flac': 'AUD', + 'zip': 'ZIP', 'rar': 'ZIP', '7z': 'ZIP', + 'java': 'CODE', 'js': 'CODE', 'ts': 'CODE', 'py': 'CODE', 'php': 'CODE', 'go': 'CODE', + 'html': 'WEB', 'css': 'WEB', + 'json': 'JSON', 'yml': 'YML', 'yaml': 'YML' }; - return iconMap[ext] || '📄'; + const label = typeMap[ext] || (ext ? ext.toUpperCase().slice(0, 4) : 'FILE'); + return '' + label + ''; } // 格式化文件大小 @@ -515,7 +532,22 @@ function updateConnectionSelect(panelId) { select.empty().append(''); + // 为避免同一主机/端口/用户名的连接在下拉框中重复显示, + // 这里按「username@host:port」进行去重,只保留第一条会话记录 + const seenKeys = new Set(); + $.each(activeConnections, function(sessionId, conn) { + if (!conn) { + return; + } + + const dedupKey = (conn.username || '') + '@' + + (conn.host || '') + ':' + (conn.port || 22); + if (seenKeys.has(dedupKey)) { + return; // 已有同一连接信息,跳过重复项 + } + seenKeys.add(dedupKey); + const name = (conn && (conn.name || conn.host)) ? (conn.name || conn.host) : ('会话 ' + (sessionId || '').substring(0, 13)); select.append('