57 lines
1.9 KiB
TypeScript
57 lines
1.9 KiB
TypeScript
|
|
import { useState, useCallback } from 'react';
|
||
|
|
import { Outlet } from 'react-router-dom';
|
||
|
|
import { TopNav } from '@/components/TopNav';
|
||
|
|
import { SideNav } from '@/components/SideNav';
|
||
|
|
import { useRiskStore } from '@/stores';
|
||
|
|
import { TESTIDS } from '@/utils/testids';
|
||
|
|
|
||
|
|
interface AppShellProps {
|
||
|
|
onLogout?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 仅负责布局骨架(顶栏 / 侧栏 / 内容区),不涉及路由匹配与鉴权。
|
||
|
|
export function AppShell({ onLogout }: AppShellProps) {
|
||
|
|
const alerts = useRiskStore((s) => s.alerts);
|
||
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||
|
|
|
||
|
|
const openDrawer = useCallback(() => setDrawerOpen(true), []);
|
||
|
|
const closeDrawer = useCallback(() => setDrawerOpen(false), []);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div data-testid={TESTIDS.appShell} className="h-screen bg-bg-page flex flex-col overflow-hidden">
|
||
|
|
<TopNav onLogout={onLogout} onToggleMenu={openDrawer} />
|
||
|
|
|
||
|
|
<div className="flex flex-1 min-h-0">
|
||
|
|
{/* lg 及以上:持久侧栏导轨 */}
|
||
|
|
<aside
|
||
|
|
data-testid={TESTIDS.sidebarRail}
|
||
|
|
className="hidden lg:block w-[200px] shrink-0 bg-bg-card border-r border-border"
|
||
|
|
>
|
||
|
|
<SideNav alertCount={alerts.length} />
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
{/* lg 以下:离屏抽屉 + 遮罩 */}
|
||
|
|
{drawerOpen && (
|
||
|
|
<div
|
||
|
|
className="fixed inset-0 z-40 bg-black/40 lg:hidden"
|
||
|
|
onClick={closeDrawer}
|
||
|
|
aria-hidden="true"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
<aside
|
||
|
|
data-testid={TESTIDS.appDrawer}
|
||
|
|
className={`fixed top-0 left-0 bottom-0 z-50 w-[260px] max-w-[80vw] bg-bg-card border-r border-border shadow-xl transition-transform duration-200 lg:hidden ${
|
||
|
|
drawerOpen ? 'translate-x-0' : '-translate-x-full'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<SideNav alertCount={alerts.length} onNavigate={closeDrawer} />
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
<main className="flex-1 min-w-0 overflow-auto p-5">
|
||
|
|
<Outlet />
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|