AppLayout.tsx 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import { useEffect } from 'react';
  2. import type { ReactNode } from 'react';
  3. import { useLocation } from 'react-router-dom';
  4. import { NavTree } from './NavTree';
  5. import { Breadcrumb } from './Breadcrumb';
  6. import { ThemeToggle } from './ThemeToggle';
  7. import { findTrail } from './nav-utils';
  8. interface Props {
  9. children: ReactNode;
  10. }
  11. /** 品牌 mark:漏斗三阶 glyph(与 favicon 一致),mint 方块内白色。 */
  12. function BrandGlyph() {
  13. return (
  14. <svg viewBox="0 0 32 32" className="size-4" fill="currentColor" aria-hidden>
  15. <rect x="7" y="9" width="18" height="3.6" rx="1.8" />
  16. <rect x="9.5" y="14.6" width="13" height="3.6" rx="1.8" />
  17. <rect x="12" y="20.2" width="8" height="3.6" rx="1.8" />
  18. </svg>
  19. );
  20. }
  21. /** Shell: top bar + left nav (L1/L2/L3 tree) + content area (docs/02 §9). */
  22. export function AppLayout({ children }: Props) {
  23. const { pathname } = useLocation();
  24. // 页面标题随路由变:「<当前页> · HS-Data」。
  25. useEffect(() => {
  26. const trail = findTrail(pathname);
  27. const leaf = trail[trail.length - 1];
  28. document.title = leaf
  29. ? `${leaf.label} · HS Data`
  30. : 'HS Data · 数据平台';
  31. }, [pathname]);
  32. return (
  33. <div className="h-screen overflow-hidden flex flex-col bg-background text-foreground">
  34. {/* 顶栏白、侧栏浅 slate —— 低反差,顶 ≠ 左 */}
  35. <header className="h-14 flex items-center gap-2.5 px-5 border-b border-border bg-card">
  36. <span className="grid size-7 shrink-0 place-items-center rounded-md bg-primary text-primary-foreground shadow-sm">
  37. <BrandGlyph />
  38. </span>
  39. <span className="text-[15px] font-bold tracking-tight">
  40. HS <span className="text-primary">Data</span>
  41. </span>
  42. <span className="h-4 w-px bg-border" />
  43. <span className="text-xs text-muted-foreground">数据平台</span>
  44. <div className="ml-auto flex items-center gap-2">
  45. <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
  46. <span className="size-1.5 rounded-full bg-primary" />
  47. <span>MVP</span>
  48. </span>
  49. <ThemeToggle />
  50. </div>
  51. </header>
  52. <div className="flex flex-1 min-h-0">
  53. <aside className="w-56 shrink-0 overflow-y-auto border-r border-border bg-sidebar text-sidebar-foreground">
  54. <NavTree />
  55. </aside>
  56. {/* scrollbar-gutter: stable —— 始终预留滚动条宽度,内容增高出现滚动条时不再横向抖动 */}
  57. <main className="flex-1 min-w-0 overflow-auto [scrollbar-gutter:stable]">
  58. <div className="p-6 flex flex-col gap-4">
  59. <Breadcrumb />
  60. {children}
  61. </div>
  62. </main>
  63. </div>
  64. </div>
  65. );
  66. }
  67. export default AppLayout;