ResponsiveLayout.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. "use client";
  2. import * as React from "react";
  3. import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
  4. import { Button } from "@/components/ui/button";
  5. import { Menu, PanelRight } from "lucide-react";
  6. import { LAYOUT } from "@/lib/constants/breakpoints";
  7. import { useI18n } from "@/lib/i18n/provider";
  8. /**
  9. * ResponsiveLayout - 响应式布局组件
  10. *
  11. * 提供统一的响应式布局,支持桌面端和移动端自适应。
  12. *
  13. * @param sidebar - 侧边栏内容(桌面端显示,移动端可折叠)
  14. * @param main - 主内容区(所有设备都显示)
  15. * @param rightPanel - 右侧面板(大屏幕显示,小屏幕隐藏或折叠)
  16. * @param header - 顶部栏(可选)
  17. * @param className - 额外的 CSS 类名
  18. */
  19. export interface ResponsiveLayoutProps {
  20. sidebar?: React.ReactNode;
  21. main: React.ReactNode;
  22. rightPanel?: React.ReactNode;
  23. header?: React.ReactNode;
  24. className?: string;
  25. sidebarWidth?: string;
  26. }
  27. export function ResponsiveLayout({
  28. sidebar,
  29. main,
  30. rightPanel,
  31. header,
  32. className,
  33. sidebarWidth,
  34. }: ResponsiveLayoutProps) {
  35. const actualSidebarWidth = sidebarWidth || LAYOUT.sidebarWidth;
  36. const { t } = useI18n();
  37. /** 与顶部安全区对齐;与 ChatHeader(h-16)上圆钮视觉对齐 */
  38. const mobileFabTop =
  39. "top-[max(0.75rem,env(safe-area-inset-top,0px)+0.25rem)]";
  40. return (
  41. <div
  42. className={`flex h-[100dvh] max-h-[100dvh] bg-background overflow-hidden ${className || ""}`}
  43. >
  44. {sidebar && (
  45. <aside
  46. className="hidden md:block border-r bg-background flex-shrink-0"
  47. style={{ width: actualSidebarWidth }}
  48. >
  49. {sidebar}
  50. </aside>
  51. )}
  52. {sidebar && (
  53. <Sheet>
  54. <SheetTrigger asChild>
  55. <Button
  56. variant="outline"
  57. size="icon"
  58. className={`fixed left-3 z-50 md:hidden h-10 w-10 rounded-full border-border/80 bg-background/90 shadow-sm backdrop-blur-sm ${mobileFabTop}`}
  59. aria-label={t("agent.layout.openNavMenu")}
  60. >
  61. <Menu className="h-5 w-5" />
  62. </Button>
  63. </SheetTrigger>
  64. <SheetContent
  65. side="left"
  66. className="w-[min(100vw-2rem,24rem)] max-w-[24rem] p-0"
  67. style={{ width: actualSidebarWidth }}
  68. >
  69. {sidebar}
  70. </SheetContent>
  71. </Sheet>
  72. )}
  73. <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
  74. {header && (
  75. <header className="flex-shrink-0 border-b bg-background">{header}</header>
  76. )}
  77. <div className="flex flex-1 min-h-0 overflow-hidden">
  78. <main className="flex-1 flex flex-col min-h-0 overflow-hidden">{main}</main>
  79. {rightPanel && (
  80. <>
  81. <aside
  82. className="hidden lg:block border-l bg-background flex-shrink-0"
  83. style={{ width: LAYOUT.rightPanelWidth }}
  84. >
  85. {rightPanel}
  86. </aside>
  87. <Sheet>
  88. <SheetTrigger asChild>
  89. <Button
  90. variant="outline"
  91. size="icon"
  92. className={`fixed right-3 z-50 lg:hidden h-10 w-10 rounded-full border-border/80 bg-background/90 shadow-sm backdrop-blur-sm ${mobileFabTop}`}
  93. aria-label={t("agent.layout.openVisitorPanel")}
  94. >
  95. <PanelRight className="h-5 w-5" />
  96. </Button>
  97. </SheetTrigger>
  98. <SheetContent
  99. side="right"
  100. className="w-[min(100vw-2rem,20rem)] max-w-[20rem] p-0"
  101. style={{ width: LAYOUT.rightPanelWidth }}
  102. >
  103. {rightPanel}
  104. </SheetContent>
  105. </Sheet>
  106. </>
  107. )}
  108. </div>
  109. </div>
  110. </div>
  111. );
  112. }