feat(ui): add collapsible persistent sidebar
This commit is contained in:
1 parent
9ce71795b8
commit
101d2164b4
13 files changed
+153
-29
No files matched your search
@@ -87,6 +87,7 @@ scripts/ 构建和验收脚本
|
|||||||
| -------------------------- | -------------------- |
|
| -------------------------- | -------------------- |
|
||||||
| `Cmd/Ctrl + N` | 新建会话 |
|
| `Cmd/Ctrl + N` | 新建会话 |
|
||||||
| `Cmd/Ctrl + K` | 快速连接 |
|
| `Cmd/Ctrl + K` | 快速连接 |
|
||||||
|
| `Cmd/Ctrl + B` | 折叠/展开侧栏 |
|
||||||
| `Cmd/Ctrl + D` | 终端分屏 |
|
| `Cmd/Ctrl + D` | 终端分屏 |
|
||||||
| `Cmd/Ctrl + W` | 关闭标签 |
|
| `Cmd/Ctrl + W` | 关闭标签 |
|
||||||
| `Cmd + Option + I` / `F12` | 开发构建打开调试工具 |
|
| `Cmd + Option + I` / `F12` | 开发构建打开调试工具 |
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ SFTP 使用双栏文件管理器。上传和下载先写入 `.opsx-part` 临时
|
|||||||
|
|
||||||
## 终端与批量命令
|
## 终端与批量命令
|
||||||
|
|
||||||
|
- `Cmd/Ctrl + B`:折叠/展开会话侧栏,便于专心操作终端
|
||||||
- `Cmd/Ctrl + D`:分屏当前终端
|
- `Cmd/Ctrl + D`:分屏当前终端
|
||||||
- `Cmd/Ctrl + W`:关闭当前标签
|
- `Cmd/Ctrl + W`:关闭当前标签
|
||||||
- 终端右键菜单提供复制、粘贴、全选、清屏、向右/向下分屏、最大化/恢复和关闭当前分屏;没有选区时“复制”保持禁用,不会一次右键就直接粘贴。
|
- 终端右键菜单提供复制、粘贴、全选、清屏、向右/向下分屏、最大化/恢复和关闭当前分屏;没有选区时“复制”保持禁用,不会一次右键就直接粘贴。
|
||||||
|
|||||||
@@ -52,11 +52,13 @@ OpsXShell 的默认界面只围绕七件事:连接主机、管理会话、操
|
|||||||
| 普通输入框复制/粘贴 | `Cmd+C/V` | `Ctrl+C/V` |
|
| 普通输入框复制/粘贴 | `Cmd+C/V` | `Ctrl+C/V` |
|
||||||
| 新建会话 | `Cmd+N` | `Ctrl+N` |
|
| 新建会话 | `Cmd+N` | `Ctrl+N` |
|
||||||
| 快速连接 | `Cmd+K` | `Ctrl+K` |
|
| 快速连接 | `Cmd+K` | `Ctrl+K` |
|
||||||
|
| 折叠/展开侧栏 | `Cmd+B` | `Ctrl+B` |
|
||||||
| 关闭标签 | `Cmd+W` | `Ctrl+W` |
|
| 关闭标签 | `Cmd+W` | `Ctrl+W` |
|
||||||
| 分屏 | `Cmd+D` | `Ctrl+D`,但必须避免与远端 EOF 语义冲突并允许改键 |
|
| 分屏 | `Cmd+D` | `Ctrl+D`,但必须避免与远端 EOF 语义冲突并允许改键 |
|
||||||
| 终端搜索 | `Cmd+F` | `Ctrl+F` |
|
| 终端搜索 | `Cmd+F` | `Ctrl+F` |
|
||||||
| 开发工具(开发构建) | `Cmd+Option+I` | `F12` |
|
| 开发工具(开发构建) | `Cmd+Option+I` | `F12` |
|
||||||
|
|
||||||
|
- 侧栏折叠采用**完全隐藏**(专心终端):折叠后不保留图标窄轨。会话树是主内容而非 IDE 工具切换条;折叠后靠 `Cmd/Ctrl+B`、标签栏左侧展开按钮恢复。快速连接继续走 `Cmd/Ctrl+K`,不依赖侧栏可见。
|
||||||
- 快捷键不能抢占输入框的复制、粘贴、全选、撤销和输入法组合状态。
|
- 快捷键不能抢占输入框的复制、粘贴、全选、撤销和输入法组合状态。
|
||||||
- 菜单和提示必须显示当前平台实际快捷键,不能同时展示两套组合键让用户猜。
|
- 菜单和提示必须显示当前平台实际快捷键,不能同时展示两套组合键让用户猜。
|
||||||
- `Escape` 关闭当前最上层弹框/菜单;`Enter` 触发主动作,文本域需要明确换行组合键。
|
- `Escape` 关闭当前最上层弹框/菜单;`Enter` 触发主动作,文本域需要明确换行组合键。
|
||||||
|
|||||||
+35
-13
@@ -78,17 +78,46 @@ function App() {
|
|||||||
}, [settings, settingsLoaded])
|
}, [settings, settingsLoaded])
|
||||||
|
|
||||||
const isMobile = useIsMobile()
|
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)
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setSidebarCollapsed(true)
|
setSidebarCollapsed(true)
|
||||||
} else {
|
return
|
||||||
setSidebarCollapsed(false)
|
|
||||||
setMobileSidebarOpen(false)
|
|
||||||
}
|
}
|
||||||
}, [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
|
// Keep renderer state authoritative when the native SSH service closes a
|
||||||
// connection (network loss, keepalive failure, or remote shell failure).
|
// 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(() => {
|
const handleOpenSettings = useCallback(() => {
|
||||||
useTabStore.getState().addTab({ title: '设置', type: 'settings' })
|
useTabStore.getState().addTab({ title: '设置', type: 'settings' })
|
||||||
}, [])
|
}, [])
|
||||||
@@ -361,6 +382,7 @@ function App() {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
mobileSidebarOpen={mobileSidebarOpen}
|
mobileSidebarOpen={mobileSidebarOpen}
|
||||||
|
onToggleSidebar={toggleSidebar}
|
||||||
onToggleMobileSidebar={toggleMobileSidebar}
|
onToggleMobileSidebar={toggleMobileSidebar}
|
||||||
onCloseMobileSidebar={closeMobileSidebar}
|
onCloseMobileSidebar={closeMobileSidebar}
|
||||||
onNewSession={openNewSession}
|
onNewSession={openNewSession}
|
||||||
|
|||||||
+25
-3
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
import { PanelLeft } from 'lucide-react'
|
||||||
import Sidebar from '../components/layout/Sidebar'
|
import Sidebar from '../components/layout/Sidebar'
|
||||||
import TabBar from '../components/layout/TabBar'
|
import TabBar from '../components/layout/TabBar'
|
||||||
import TabContent from '../components/layout/TabContent'
|
import TabContent from '../components/layout/TabContent'
|
||||||
@@ -20,6 +21,7 @@ export interface AppShellProps {
|
|||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
sidebarCollapsed: boolean
|
sidebarCollapsed: boolean
|
||||||
mobileSidebarOpen: boolean
|
mobileSidebarOpen: boolean
|
||||||
|
onToggleSidebar: () => void
|
||||||
onToggleMobileSidebar: () => void
|
onToggleMobileSidebar: () => void
|
||||||
onCloseMobileSidebar: () => void
|
onCloseMobileSidebar: () => void
|
||||||
onNewSession: () => void
|
onNewSession: () => void
|
||||||
@@ -63,11 +65,26 @@ function MobileMenuButton({ onClick }: { onClick: () => void }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExpandSidebarButton({ onClick }: { onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className="titlebar-no-drag flex h-10 w-10 flex-shrink-0 items-center justify-center border-r border-dark-700/40 text-txt-muted transition-colors hover:bg-dark-700/60 hover:text-content-primary"
|
||||||
|
title="展开侧栏 (Ctrl/Cmd+B)"
|
||||||
|
aria-label="展开侧栏"
|
||||||
|
>
|
||||||
|
<PanelLeft className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function AppShell({
|
export default function AppShell({
|
||||||
activeTabId,
|
activeTabId,
|
||||||
isMobile,
|
isMobile,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
mobileSidebarOpen,
|
mobileSidebarOpen,
|
||||||
|
onToggleSidebar,
|
||||||
onToggleMobileSidebar,
|
onToggleMobileSidebar,
|
||||||
onCloseMobileSidebar,
|
onCloseMobileSidebar,
|
||||||
onNewSession,
|
onNewSession,
|
||||||
@@ -91,6 +108,8 @@ export default function AppShell({
|
|||||||
if (isMobile) onCloseMobileSidebar()
|
if (isMobile) onCloseMobileSidebar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const desktopSidebarHidden = !isMobile && sidebarCollapsed
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col overflow-hidden bg-dark-900 text-content-primary">
|
<div className="flex h-screen flex-col overflow-hidden bg-dark-900 text-content-primary">
|
||||||
<TitleBar />
|
<TitleBar />
|
||||||
@@ -105,24 +124,27 @@ export default function AppShell({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
className={`titlebar-no-drag h-full flex-shrink-0 transition-all duration-300 ease-in-out ${
|
className={`titlebar-no-drag h-full flex-shrink-0 transition-[width,transform] duration-200 ease-in-out ${
|
||||||
isMobile
|
isMobile
|
||||||
? `fixed inset-y-0 left-0 top-10 z-50 ${mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`
|
? `fixed inset-y-0 left-0 top-10 z-50 ${mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`
|
||||||
: sidebarCollapsed
|
: desktopSidebarHidden
|
||||||
? 'w-0 overflow-hidden'
|
? 'w-0 overflow-hidden border-0'
|
||||||
: ''
|
: ''
|
||||||
} `}
|
} `}
|
||||||
|
aria-hidden={desktopSidebarHidden}
|
||||||
>
|
>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
onNewSession={handleSidebarNewSession}
|
onNewSession={handleSidebarNewSession}
|
||||||
onQuickConnect={handleSidebarQuickConnect}
|
onQuickConnect={handleSidebarQuickConnect}
|
||||||
onEditSession={handleSidebarEditSession}
|
onEditSession={handleSidebarEditSession}
|
||||||
|
onCollapse={isMobile ? undefined : onToggleSidebar}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="flex min-w-0 flex-1 flex-col bg-dark-800">
|
<main className="flex min-w-0 flex-1 flex-col bg-dark-800">
|
||||||
<div className="flex items-stretch">
|
<div className="flex items-stretch">
|
||||||
{isMobile && <MobileMenuButton onClick={onToggleMobileSidebar} />}
|
{isMobile && <MobileMenuButton onClick={onToggleMobileSidebar} />}
|
||||||
|
{desktopSidebarHidden && <ExpandSidebarButton onClick={onToggleSidebar} />}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<TabBar onOpenLauncher={onOpenLauncher} />
|
<TabBar onOpenLauncher={onOpenLauncher} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -17,6 +17,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Settings,
|
Settings,
|
||||||
Laptop,
|
Laptop,
|
||||||
|
PanelLeft,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTabStore } from '../../stores/tabStore'
|
import { useTabStore } from '../../stores/tabStore'
|
||||||
import { useSessionStore, useSnippetStore, useCommandPaletteStore, type Session } from '../../stores'
|
import { useSessionStore, useSnippetStore, useCommandPaletteStore, type Session } from '../../stores'
|
||||||
@@ -108,6 +109,15 @@ export function CommandPalette({
|
|||||||
icon: <Settings size={16} />,
|
icon: <Settings size={16} />,
|
||||||
action: () => onOpenSettings(),
|
action: () => onOpenSettings(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'cmd:toggle-sidebar',
|
||||||
|
type: 'command',
|
||||||
|
label: '折叠/展开侧栏',
|
||||||
|
description: '隐藏侧栏以专心操作终端',
|
||||||
|
shortcut: 'Ctrl+B',
|
||||||
|
icon: <PanelLeft size={16} />,
|
||||||
|
action: () => window.dispatchEvent(new CustomEvent('opsxshell:toggle-sidebar')),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'cmd:new-tab',
|
id: 'cmd:new-tab',
|
||||||
type: 'command',
|
type: 'command',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
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 { useSessionStore, Session } from '../../stores'
|
||||||
import { useSettingsStore } from '../../stores/settingsStore'
|
import { useSettingsStore } from '../../stores/settingsStore'
|
||||||
import { useTabStore } from '../../stores/tabStore'
|
import { useTabStore } from '../../stores/tabStore'
|
||||||
@@ -12,6 +12,7 @@ import { SessionTree } from './sidebar/SessionTree'
|
|||||||
import { SearchBox } from './sidebar/SearchBox'
|
import { SearchBox } from './sidebar/SearchBox'
|
||||||
import { useConfirmDialog } from '../common/ConfirmDialog'
|
import { useConfirmDialog } from '../common/ConfirmDialog'
|
||||||
import BatchCommandDialog from '../sessions/BatchCommandDialog'
|
import BatchCommandDialog from '../sessions/BatchCommandDialog'
|
||||||
|
import appIconUrl from '../../assets/opsxshell-app-icon.png'
|
||||||
|
|
||||||
const MIN_SIDEBAR_WIDTH = 180
|
const MIN_SIDEBAR_WIDTH = 180
|
||||||
const MAX_SIDEBAR_WIDTH = 400
|
const MAX_SIDEBAR_WIDTH = 400
|
||||||
@@ -20,9 +21,10 @@ interface SidebarProps {
|
|||||||
onNewSession: () => void
|
onNewSession: () => void
|
||||||
onQuickConnect: () => void
|
onQuickConnect: () => void
|
||||||
onEditSession: (session: Session) => 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 { sessions, setSessions, addSession, removeSession } = useSessionStore()
|
||||||
const { settings, setSetting } = useSettingsStore()
|
const { settings, setSetting } = useSettingsStore()
|
||||||
const { addTab } = useTabStore()
|
const { addTab } = useTabStore()
|
||||||
@@ -259,15 +261,31 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-3.5 border-b border-dark-700/30">
|
<div className="p-3.5 border-b border-dark-700/30">
|
||||||
<div className="flex items-center gap-2.5 mb-3.5 px-0.5">
|
<div className="flex items-center gap-2.5 mb-3.5 px-0.5">
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-accent-blue to-accent-blue/70 rounded-lg flex items-center justify-center shadow-sm shadow-accent-blue/25">
|
<img
|
||||||
<Server className="w-4 h-4 text-white" />
|
src={appIconUrl}
|
||||||
</div>
|
alt="OpsXShell"
|
||||||
<div className="min-w-0">
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="h-8 w-8 flex-shrink-0 rounded-[9px] object-cover"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
<div className="font-semibold text-sm text-content-primary tracking-tight leading-none">
|
<div className="font-semibold text-sm text-content-primary tracking-tight leading-none">
|
||||||
OpsXShell
|
OpsXShell
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-txt-faint mt-1">远程运维工作台</div>
|
<div className="text-[10px] text-txt-faint mt-1">SSH / SFTP 终端</div>
|
||||||
</div>
|
</div>
|
||||||
|
{onCollapse && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCollapse}
|
||||||
|
title="折叠侧栏 (Ctrl/Cmd+B)"
|
||||||
|
aria-label="折叠侧栏"
|
||||||
|
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg text-txt-muted transition-colors hover:bg-dark-700 hover:text-content-primary"
|
||||||
|
>
|
||||||
|
<PanelLeftClose className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
@@ -414,9 +432,14 @@ export default function Sidebar({ onNewSession, onQuickConnect, onEditSession }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Resize Handle */}
|
{/* Resize Handle — 双击也可折叠 */}
|
||||||
<div
|
<div
|
||||||
onMouseDown={handleResizeStart}
|
onMouseDown={handleResizeStart}
|
||||||
|
onDoubleClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onCollapse?.()
|
||||||
|
}}
|
||||||
|
title="拖拽调整宽度,双击折叠"
|
||||||
className={`
|
className={`
|
||||||
absolute top-0 right-0 bottom-0 w-1 cursor-col-resize
|
absolute top-0 right-0 bottom-0 w-1 cursor-col-resize
|
||||||
hover:bg-accent-blue/50 active:bg-accent-blue/70
|
hover:bg-accent-blue/50 active:bg-accent-blue/70
|
||||||
|
|||||||
@@ -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 { useSessionStore } from '../../stores'
|
||||||
import { useTabStore } from '../../stores/tabStore'
|
import { useTabStore } from '../../stores/tabStore'
|
||||||
import EmptyState from '../common/EmptyState'
|
import EmptyState from '../common/EmptyState'
|
||||||
import LocalShellMenu from '../layout/sidebar/LocalShellMenu'
|
import LocalShellMenu from '../layout/sidebar/LocalShellMenu'
|
||||||
|
import appIconUrl from '../../assets/opsxshell-app-icon.png'
|
||||||
|
|
||||||
interface WelcomeViewProps {
|
interface WelcomeViewProps {
|
||||||
onNewSession?: () => void
|
onNewSession?: () => void
|
||||||
@@ -27,12 +28,17 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
|||||||
<div className="w-full max-w-3xl px-6 py-10 sm:py-12">
|
<div className="w-full max-w-3xl px-6 py-10 sm:py-12">
|
||||||
{/* Hero — 短文案 */}
|
{/* Hero — 短文案 */}
|
||||||
<div className="flex items-center gap-4 mb-8">
|
<div className="flex items-center gap-4 mb-8">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-accent-blue/30 to-accent-purple/20 border border-dark-600/60 flex items-center justify-center shadow-lg shadow-black/20 flex-shrink-0">
|
<img
|
||||||
<Terminal className="w-7 h-7 text-content-primary" />
|
src={appIconUrl}
|
||||||
</div>
|
alt="OpsXShell"
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
className="h-14 w-14 flex-shrink-0 rounded-2xl object-cover shadow-lg shadow-black/20"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h1 className="text-2xl font-semibold text-content-primary tracking-tight">OpsXShell</h1>
|
<h1 className="text-2xl font-semibold text-content-primary tracking-tight">OpsXShell</h1>
|
||||||
<p className="text-sm text-txt-muted mt-0.5">SSH / SFTP 跨平台运维工作台</p>
|
<p className="text-sm text-txt-muted mt-0.5">SSH / SFTP 终端</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,23 @@ export default function SettingsView() {
|
|||||||
<span className="w-12 text-right text-xs tabular-nums text-txt-muted">{settings.sidebarWidth}px</span>
|
<span className="w-12 text-right text-xs tabular-nums text-txt-muted">{settings.sidebarWidth}px</span>
|
||||||
</div>
|
</div>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
<SettingRow label="折叠侧栏" description="隐藏会话树以专心操作终端,可用 Ctrl/Cmd+B 切换">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={settings.sidebarCollapsed}
|
||||||
|
onClick={() => setSetting('sidebarCollapsed', !settings.sidebarCollapsed)}
|
||||||
|
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||||
|
settings.sidebarCollapsed ? 'bg-accent-blue' : 'bg-dark-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform ${
|
||||||
|
settings.sidebarCollapsed ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</SettingRow>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const KeybindingAction = {
|
|||||||
|
|
||||||
// 视图
|
// 视图
|
||||||
VIEW_FULLSCREEN: 'view.fullscreen',
|
VIEW_FULLSCREEN: 'view.fullscreen',
|
||||||
|
VIEW_TOGGLE_SIDEBAR: 'view.toggleSidebar',
|
||||||
|
|
||||||
// 会话
|
// 会话
|
||||||
SESSION_NEW: 'session.new',
|
SESSION_NEW: 'session.new',
|
||||||
@@ -95,6 +96,13 @@ export const KEYBINDING_DEFINITIONS: KeybindingDefinition[] = [
|
|||||||
defaultKeys: 'F11',
|
defaultKeys: 'F11',
|
||||||
fireInInput: true,
|
fireInInput: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: KeybindingAction.VIEW_TOGGLE_SIDEBAR,
|
||||||
|
label: '折叠/展开侧栏',
|
||||||
|
category: KeybindingCategory.VIEW,
|
||||||
|
defaultKeys: 'Ctrl+B',
|
||||||
|
fireInInput: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: KeybindingAction.SESSION_NEW,
|
id: KeybindingAction.SESSION_NEW,
|
||||||
label: '新建会话',
|
label: '新建会话',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
* - Ctrl+1~9: 跳转到指定索引标签
|
* - Ctrl+1~9: 跳转到指定索引标签
|
||||||
* - Ctrl+W: 关闭当前标签
|
* - Ctrl+W: 关闭当前标签
|
||||||
* - F11: 全屏切换
|
* - F11: 全屏切换
|
||||||
|
* - Ctrl/Cmd+B: 折叠/展开侧栏
|
||||||
* - Ctrl+Shift+N: 新建标签
|
* - Ctrl+Shift+N: 新建标签
|
||||||
* - Ctrl+Shift+P: 打开命令面板
|
* - Ctrl+Shift+P: 打开命令面板
|
||||||
* - Ctrl+N: 新建会话
|
* - Ctrl+N: 新建会话
|
||||||
@@ -125,6 +126,7 @@ function shouldFireInInput(e: KeyboardEvent): boolean {
|
|||||||
KeybindingAction.TAB_PREV,
|
KeybindingAction.TAB_PREV,
|
||||||
KeybindingAction.TAB_GOTO_N,
|
KeybindingAction.TAB_GOTO_N,
|
||||||
KeybindingAction.VIEW_FULLSCREEN,
|
KeybindingAction.VIEW_FULLSCREEN,
|
||||||
|
KeybindingAction.VIEW_TOGGLE_SIDEBAR,
|
||||||
KeybindingAction.TOOLS_COMMAND_PALETTE,
|
KeybindingAction.TOOLS_COMMAND_PALETTE,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -202,6 +204,13 @@ export function useKeyboardShortcuts(): void {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Ctrl/Cmd+B: 折叠侧栏 ----
|
||||||
|
if (matchesKeySequence(e, keys[KeybindingAction.VIEW_TOGGLE_SIDEBAR])) {
|
||||||
|
e.preventDefault()
|
||||||
|
window.dispatchEvent(new CustomEvent('opsxshell:toggle-sidebar'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 以下快捷键在输入框中不触发 (除非 fireInInput=true) ----
|
// ---- 以下快捷键在输入框中不触发 (除非 fireInInput=true) ----
|
||||||
if (isInputElement(e) && !shouldFireInInput(e)) {
|
if (isInputElement(e) && !shouldFireInInput(e)) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export interface AppSettings {
|
|||||||
// 外观
|
// 外观
|
||||||
theme: 'dark' | 'light' | 'system'
|
theme: 'dark' | 'light' | 'system'
|
||||||
sidebarWidth: number
|
sidebarWidth: number
|
||||||
|
/** 桌面侧栏是否折叠(专心终端) */
|
||||||
|
sidebarCollapsed: boolean
|
||||||
|
|
||||||
// 终端
|
// 终端
|
||||||
fontFamily: string
|
fontFamily: string
|
||||||
@@ -41,6 +43,7 @@ export interface AppSettings {
|
|||||||
const defaultSettings: AppSettings = {
|
const defaultSettings: AppSettings = {
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
sidebarWidth: 256,
|
sidebarWidth: 256,
|
||||||
|
sidebarCollapsed: false,
|
||||||
// Prefer Nerd Font so local zsh powerline prompts render correctly
|
// Prefer Nerd Font so local zsh powerline prompts render correctly
|
||||||
fontFamily: 'MesloLGS NF',
|
fontFamily: 'MesloLGS NF',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|||||||
Reference in new issue
Block a user