OpsXShell
-SSH / SFTP 跨平台运维工作台
+SSH / SFTP 终端
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 (
SSH / SFTP 跨平台运维工作台
+SSH / SFTP 终端