diff --git a/README.md b/README.md index c88b321..0e1012f 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ scripts/ 构建和验收脚本 | -------------------------- | -------------------- | | `Cmd/Ctrl + N` | 新建会话 | | `Cmd/Ctrl + K` | 快速连接 | +| `Cmd/Ctrl + B` | 折叠/展开侧栏 | | `Cmd/Ctrl + D` | 终端分屏 | | `Cmd/Ctrl + W` | 关闭标签 | | `Cmd + Option + I` / `F12` | 开发构建打开调试工具 | diff --git a/docs/user-guide.md b/docs/user-guide.md index ce29fc4..5318bec 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -72,6 +72,7 @@ SFTP 使用双栏文件管理器。上传和下载先写入 `.opsx-part` 临时 ## 终端与批量命令 +- `Cmd/Ctrl + B`:折叠/展开会话侧栏,便于专心操作终端 - `Cmd/Ctrl + D`:分屏当前终端 - `Cmd/Ctrl + W`:关闭当前标签 - 终端右键菜单提供复制、粘贴、全选、清屏、向右/向下分屏、最大化/恢复和关闭当前分屏;没有选区时“复制”保持禁用,不会一次右键就直接粘贴。 diff --git a/docs/ux-foundation-audit.md b/docs/ux-foundation-audit.md index 7143a67..63d2ec8 100644 --- a/docs/ux-foundation-audit.md +++ b/docs/ux-foundation-audit.md @@ -52,11 +52,13 @@ OpsXShell 的默认界面只围绕七件事:连接主机、管理会话、操 | 普通输入框复制/粘贴 | `Cmd+C/V` | `Ctrl+C/V` | | 新建会话 | `Cmd+N` | `Ctrl+N` | | 快速连接 | `Cmd+K` | `Ctrl+K` | +| 折叠/展开侧栏 | `Cmd+B` | `Ctrl+B` | | 关闭标签 | `Cmd+W` | `Ctrl+W` | | 分屏 | `Cmd+D` | `Ctrl+D`,但必须避免与远端 EOF 语义冲突并允许改键 | | 终端搜索 | `Cmd+F` | `Ctrl+F` | | 开发工具(开发构建) | `Cmd+Option+I` | `F12` | +- 侧栏折叠采用**完全隐藏**(专心终端):折叠后不保留图标窄轨。会话树是主内容而非 IDE 工具切换条;折叠后靠 `Cmd/Ctrl+B`、标签栏左侧展开按钮恢复。快速连接继续走 `Cmd/Ctrl+K`,不依赖侧栏可见。 - 快捷键不能抢占输入框的复制、粘贴、全选、撤销和输入法组合状态。 - 菜单和提示必须显示当前平台实际快捷键,不能同时展示两套组合键让用户猜。 - `Escape` 关闭当前最上层弹框/菜单;`Enter` 触发主动作,文本域需要明确换行组合键。 diff --git a/src/app/App.tsx b/src/app/App.tsx index 93f9296..da5afb2 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -78,17 +78,46 @@ function App() { }, [settings, settingsLoaded]) const isMobile = useIsMobile() - const [sidebarCollapsed, setSidebarCollapsed] = useState(false) + const setSetting = useSettingsStore((state) => state.setSetting) + const [sidebarCollapsed, setSidebarCollapsed] = useState(settings.sidebarCollapsed) const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) useEffect(() => { if (isMobile) { setSidebarCollapsed(true) - } else { - setSidebarCollapsed(false) - setMobileSidebarOpen(false) + return } - }, [isMobile]) + setMobileSidebarOpen(false) + if (settingsLoaded) { + setSidebarCollapsed(settings.sidebarCollapsed) + } + }, [isMobile, settingsLoaded, settings.sidebarCollapsed]) + + const toggleSidebar = useCallback(() => { + if (isMobile) { + setMobileSidebarOpen((prev) => !prev) + return + } + setSidebarCollapsed((prev) => { + const next = !prev + void setSetting('sidebarCollapsed', next) + return next + }) + }, [isMobile, setSetting]) + + const toggleMobileSidebar = useCallback(() => { + setMobileSidebarOpen((prev) => !prev) + }, []) + + const closeMobileSidebar = useCallback(() => { + setMobileSidebarOpen(false) + }, []) + + useEffect(() => { + const handleToggleSidebar = () => toggleSidebar() + window.addEventListener('opsxshell:toggle-sidebar', handleToggleSidebar) + return () => window.removeEventListener('opsxshell:toggle-sidebar', handleToggleSidebar) + }, [toggleSidebar]) // Keep renderer state authoritative when the native SSH service closes a // connection (network loss, keepalive failure, or remote shell failure). @@ -112,14 +141,6 @@ function App() { } }, []) - const toggleMobileSidebar = useCallback(() => { - setMobileSidebarOpen((prev) => !prev) - }, []) - - const closeMobileSidebar = useCallback(() => { - setMobileSidebarOpen(false) - }, []) - const handleOpenSettings = useCallback(() => { useTabStore.getState().addTab({ title: '设置', type: 'settings' }) }, []) @@ -361,6 +382,7 @@ function App() { isMobile={isMobile} sidebarCollapsed={sidebarCollapsed} mobileSidebarOpen={mobileSidebarOpen} + onToggleSidebar={toggleSidebar} onToggleMobileSidebar={toggleMobileSidebar} onCloseMobileSidebar={closeMobileSidebar} onNewSession={openNewSession} diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx index 767d577..cec2587 100644 --- a/src/app/AppShell.tsx +++ b/src/app/AppShell.tsx @@ -8,6 +8,7 @@ */ import type { ReactNode } from 'react' +import { PanelLeft } from 'lucide-react' import Sidebar from '../components/layout/Sidebar' import TabBar from '../components/layout/TabBar' import TabContent from '../components/layout/TabContent' @@ -20,6 +21,7 @@ export interface AppShellProps { isMobile: boolean sidebarCollapsed: boolean mobileSidebarOpen: boolean + onToggleSidebar: () => void onToggleMobileSidebar: () => void onCloseMobileSidebar: () => void onNewSession: () => void @@ -63,11 +65,26 @@ function MobileMenuButton({ onClick }: { onClick: () => void }) { ) } +function ExpandSidebarButton({ onClick }: { onClick: () => void }) { + return ( + + ) +} + export default function AppShell({ activeTabId, isMobile, sidebarCollapsed, mobileSidebarOpen, + onToggleSidebar, onToggleMobileSidebar, onCloseMobileSidebar, onNewSession, @@ -91,6 +108,8 @@ export default function AppShell({ if (isMobile) onCloseMobileSidebar() } + const desktopSidebarHidden = !isMobile && sidebarCollapsed + return (
@@ -105,24 +124,27 @@ export default function AppShell({ )}
{isMobile && } + {desktopSidebarHidden && }
diff --git a/src/assets/opsxshell-app-icon.png b/src/assets/opsxshell-app-icon.png new file mode 100644 index 0000000..9d86172 Binary files /dev/null and b/src/assets/opsxshell-app-icon.png differ diff --git a/src/components/common/CommandPalette.tsx b/src/components/common/CommandPalette.tsx index d00deb0..252322c 100644 --- a/src/components/common/CommandPalette.tsx +++ b/src/components/common/CommandPalette.tsx @@ -17,6 +17,7 @@ import { ChevronRight, Settings, Laptop, + PanelLeft, } from 'lucide-react' import { useTabStore } from '../../stores/tabStore' import { useSessionStore, useSnippetStore, useCommandPaletteStore, type Session } from '../../stores' @@ -108,6 +109,15 @@ export function CommandPalette({ icon: , action: () => onOpenSettings(), }, + { + id: 'cmd:toggle-sidebar', + type: 'command', + label: '折叠/展开侧栏', + description: '隐藏侧栏以专心操作终端', + shortcut: 'Ctrl+B', + icon: , + action: () => window.dispatchEvent(new CustomEvent('opsxshell:toggle-sidebar')), + }, { id: 'cmd:new-tab', type: 'command', diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 9f27715..77ee38f 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -4,7 +4,7 @@ */ import { useState, useEffect, useRef, useCallback } from 'react' -import { Plus, Zap, Server, Settings, ListChecks, Terminal, FolderSync, X, TerminalSquare } from 'lucide-react' +import { Plus, Zap, Settings, ListChecks, Terminal, FolderSync, X, TerminalSquare, PanelLeftClose } from 'lucide-react' import { useSessionStore, Session } from '../../stores' import { useSettingsStore } from '../../stores/settingsStore' import { useTabStore } from '../../stores/tabStore' @@ -12,6 +12,7 @@ import { SessionTree } from './sidebar/SessionTree' import { SearchBox } from './sidebar/SearchBox' import { useConfirmDialog } from '../common/ConfirmDialog' import BatchCommandDialog from '../sessions/BatchCommandDialog' +import appIconUrl from '../../assets/opsxshell-app-icon.png' const MIN_SIDEBAR_WIDTH = 180 const MAX_SIDEBAR_WIDTH = 400 @@ -20,9 +21,10 @@ interface SidebarProps { onNewSession: () => void onQuickConnect: () => void onEditSession: (session: Session) => void + onCollapse?: () => void } -export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }: SidebarProps) { +export default function Sidebar({ onNewSession, onQuickConnect, onEditSession, onCollapse }: SidebarProps) { const { sessions, setSessions, addSession, removeSession } = useSessionStore() const { settings, setSetting } = useSettingsStore() const { addTab } = useTabStore() @@ -259,15 +261,31 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }: {/* Header */}
-
- -
-
+ OpsXShell +
OpsXShell
-
远程运维工作台
+
SSH / SFTP 终端
+ {onCollapse && ( + + )}
@@ -414,9 +432,14 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }: )}
- {/* Resize Handle */} + {/* Resize Handle — 双击也可折叠 */}
{ + e.preventDefault() + onCollapse?.() + }} + title="拖拽调整宽度,双击折叠" className={` absolute top-0 right-0 bottom-0 w-1 cursor-col-resize hover:bg-accent-blue/50 active:bg-accent-blue/70 diff --git a/src/components/sessions/WelcomeView.tsx b/src/components/sessions/WelcomeView.tsx index ed0ab26..e9559fb 100644 --- a/src/components/sessions/WelcomeView.tsx +++ b/src/components/sessions/WelcomeView.tsx @@ -1,8 +1,9 @@ -import { Terminal, Plus, Zap, Clock, Server, ChevronRight } from 'lucide-react' +import { Plus, Zap, Clock, Server, ChevronRight } from 'lucide-react' import { useSessionStore } from '../../stores' import { useTabStore } from '../../stores/tabStore' import EmptyState from '../common/EmptyState' import LocalShellMenu from '../layout/sidebar/LocalShellMenu' +import appIconUrl from '../../assets/opsxshell-app-icon.png' interface WelcomeViewProps { onNewSession?: () => void @@ -27,12 +28,17 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
{/* Hero — 短文案 */}
-
- -
+ OpsXShell

OpsXShell

-

SSH / SFTP 跨平台运维工作台

+

SSH / SFTP 终端

diff --git a/src/components/settings/SettingsView.tsx b/src/components/settings/SettingsView.tsx index c3beec6..09517d1 100644 --- a/src/components/settings/SettingsView.tsx +++ b/src/components/settings/SettingsView.tsx @@ -216,6 +216,23 @@ export default function SettingsView() { {settings.sidebarWidth}px
+ + +
) diff --git a/src/constants/keybindings.ts b/src/constants/keybindings.ts index 06381b9..98252a2 100644 --- a/src/constants/keybindings.ts +++ b/src/constants/keybindings.ts @@ -16,6 +16,7 @@ export const KeybindingAction = { // 视图 VIEW_FULLSCREEN: 'view.fullscreen', + VIEW_TOGGLE_SIDEBAR: 'view.toggleSidebar', // 会话 SESSION_NEW: 'session.new', @@ -95,6 +96,13 @@ export const KEYBINDING_DEFINITIONS: KeybindingDefinition[] = [ defaultKeys: 'F11', fireInInput: true, }, + { + id: KeybindingAction.VIEW_TOGGLE_SIDEBAR, + label: '折叠/展开侧栏', + category: KeybindingCategory.VIEW, + defaultKeys: 'Ctrl+B', + fireInInput: true, + }, { id: KeybindingAction.SESSION_NEW, label: '新建会话', diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts index d6b6750..3774e82 100644 --- a/src/hooks/useKeyboardShortcuts.ts +++ b/src/hooks/useKeyboardShortcuts.ts @@ -9,6 +9,7 @@ * - Ctrl+1~9: 跳转到指定索引标签 * - Ctrl+W: 关闭当前标签 * - F11: 全屏切换 + * - Ctrl/Cmd+B: 折叠/展开侧栏 * - Ctrl+Shift+N: 新建标签 * - Ctrl+Shift+P: 打开命令面板 * - Ctrl+N: 新建会话 @@ -125,6 +126,7 @@ function shouldFireInInput(e: KeyboardEvent): boolean { KeybindingAction.TAB_PREV, KeybindingAction.TAB_GOTO_N, KeybindingAction.VIEW_FULLSCREEN, + KeybindingAction.VIEW_TOGGLE_SIDEBAR, KeybindingAction.TOOLS_COMMAND_PALETTE, ] @@ -202,6 +204,13 @@ export function useKeyboardShortcuts(): void { return } + // ---- Ctrl/Cmd+B: 折叠侧栏 ---- + if (matchesKeySequence(e, keys[KeybindingAction.VIEW_TOGGLE_SIDEBAR])) { + e.preventDefault() + window.dispatchEvent(new CustomEvent('opsxshell:toggle-sidebar')) + return + } + // ---- 以下快捷键在输入框中不触发 (除非 fireInInput=true) ---- if (isInputElement(e) && !shouldFireInInput(e)) { return diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index 84df956..274eed7 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -12,6 +12,8 @@ export interface AppSettings { // 外观 theme: 'dark' | 'light' | 'system' sidebarWidth: number + /** 桌面侧栏是否折叠(专心终端) */ + sidebarCollapsed: boolean // 终端 fontFamily: string @@ -41,6 +43,7 @@ export interface AppSettings { const defaultSettings: AppSettings = { theme: 'dark', sidebarWidth: 256, + sidebarCollapsed: false, // Prefer Nerd Font so local zsh powerline prompts render correctly fontFamily: 'MesloLGS NF', fontSize: 14,