Files
opsxshell/src/app/AppShell.tsx
T
Logan.LiandCursor fea6d8db04 feat: Pro 买断门禁、SFTP 精简与批量多机改造
新增 entitlement/license IPC 与 Waffo 一次性结账服务骨架,挂载 AI/转发/代理/串口/广播/批量导入导出门禁;批量执行改为结果矩阵并复用连接池,SFTP 工具栏图标化并去掉已连接条,同时完善跨平台本地终端与窗口配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 11:38:30 +08:00

167 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AppShell — 应用壳
*
* 窗口拖拽约定(Tauri overlay title bar):
* - 仅使用 data-tauri-drag-region 处理拖拽和双击缩放
* - 不叠加 CSS app-region 或 React 双击处理,避免一次双击切换两次
* - 标签栏空白区也由 Tauri 原生拖拽区处理
*/
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'
import StatusBar from '../components/layout/StatusBar'
import type { Session } from '../stores'
import { getDesktopPlatform } from '../platform/runtime'
export interface AppShellProps {
activeTabId: string | null
isMobile: boolean
sidebarCollapsed: boolean
mobileSidebarOpen: boolean
onToggleSidebar: () => void
onToggleMobileSidebar: () => void
onCloseMobileSidebar: () => void
onNewSession: () => void
onQuickConnect: () => void
onOpenLauncher: () => void
onEditSession: (session: Session) => void
children?: ReactNode
}
function TitleBar() {
const platform = getDesktopPlatform()
// Overlay title bar is macOS-only; Win/Linux keep the native chrome.
if (platform !== 'macos') return null
return (
<header
data-tauri-drag-region
className="bg-dark-900/95 flex h-10 flex-shrink-0 select-none items-center border-b border-dark-800"
>
<div data-tauri-drag-region className="h-full w-[78px] flex-shrink-0" aria-hidden />
<div data-tauri-drag-region className="flex h-full min-w-0 flex-1 items-center justify-center px-2">
<span className="pointer-events-none truncate text-[11px] tracking-wide text-txt-faint">OpsXShell</span>
</div>
<div data-tauri-drag-region className="h-full w-[78px] flex-shrink-0" aria-hidden />
</header>
)
}
function MobileMenuButton({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
className="titlebar-no-drag flex h-10 w-10 items-center justify-center text-txt-muted transition-colors hover:bg-dark-800 hover:text-content-primary"
aria-label="切换侧边栏"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
)
}
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({
activeTabId,
isMobile,
sidebarCollapsed,
mobileSidebarOpen,
onToggleSidebar,
onToggleMobileSidebar,
onCloseMobileSidebar,
onNewSession,
onQuickConnect,
onOpenLauncher,
onEditSession,
children,
}: AppShellProps) {
const handleSidebarNewSession = () => {
onNewSession()
if (isMobile) onCloseMobileSidebar()
}
const handleSidebarQuickConnect = () => {
onQuickConnect()
if (isMobile) onCloseMobileSidebar()
}
const handleSidebarEditSession = (session: Session) => {
onEditSession(session)
if (isMobile) onCloseMobileSidebar()
}
const desktopSidebarHidden = !isMobile && sidebarCollapsed
return (
<div className="flex h-screen flex-col overflow-hidden bg-dark-900 text-content-primary">
<TitleBar />
<div className="flex min-h-0 flex-1 overflow-hidden">
{isMobile && mobileSidebarOpen && (
<div
className="titlebar-no-drag fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
onClick={onCloseMobileSidebar}
aria-hidden
/>
)}
<aside
className={`titlebar-no-drag h-full flex-shrink-0 transition-[width,transform] duration-200 ease-in-out ${
isMobile
? `fixed inset-y-0 left-0 top-10 z-50 ${mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`
: desktopSidebarHidden
? 'w-0 overflow-hidden border-0'
: ''
} `}
aria-hidden={desktopSidebarHidden}
>
<Sidebar
onNewSession={handleSidebarNewSession}
onQuickConnect={handleSidebarQuickConnect}
onEditSession={handleSidebarEditSession}
onCollapse={isMobile ? undefined : onToggleSidebar}
/>
</aside>
<main className="flex min-w-0 flex-1 flex-col bg-dark-800">
<div className="flex items-stretch">
{isMobile && <MobileMenuButton onClick={onToggleMobileSidebar} />}
{desktopSidebarHidden && <ExpandSidebarButton onClick={onToggleSidebar} />}
<div className="min-w-0 flex-1">
<TabBar onOpenLauncher={onOpenLauncher} />
</div>
</div>
<div className="titlebar-no-drag min-h-0 flex-1 overflow-hidden">
<TabContent activeTabId={activeTabId} onNewSession={onNewSession} onQuickConnect={onQuickConnect} />
</div>
</main>
</div>
<div className="titlebar-no-drag">
<StatusBar />
</div>
<div className="titlebar-no-drag">{children}</div>
</div>
)
}