feat(frontend): Phase 1 UX foundation — react-router v6 + responsive AppShell
Atomic foundation for the consensus-approved UX modernization (makes the platform URL-addressable, refresh-safe, and mobile-usable for hospital demos). - Migrate hand-rolled useState page switching → react-router v6 (routes.tsx, thin App.tsx auth gate, NavLink SideNav, lazy+Suspense per route) - Add responsive AppShell: persistent rail (lg:) ⇄ off-canvas drawer + hamburger (<lg); kills hardcoded ml-[200px]; usable at 375px - Add ui primitive kit (Skeleton/LoadingState/EmptyState/Card/Panel/Segmented) - Sweep all raw "加载中..." text loaders → skeleton primitives (G4) - Centralize test ids (utils/testids.ts); rewrite e2e for URL nav (17/17 pass: deep-link, refresh-preserves-page, back, 375px drawer/no-scroll) - Harden DemographicAnalysis against API shape mismatch (defensive normalize) - gitignore playwright-report/ and test-results/ Gates: tsc --noEmit 0 · pnpm build ok · e2e 17/17 · grep 加载中 zero outside ui/ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
frontend/src/components/AppShell.tsx
Normal file
56
frontend/src/components/AppShell.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user