MainLayout.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import React, { useState } from 'react';
  2. import { Outlet, useNavigate, useLocation } from 'react-router';
  3. import { useAuth } from '@/client/mobile/hooks/AuthProvider';
  4. import { UserType } from '@/server/modules/users/user.enum';
  5. import { getGlobalConfig } from '@/client/utils/utils';
  6. import { cn } from '@/client/lib/utils';
  7. import {
  8. MessageCircle,
  9. Users,
  10. FileText,
  11. Heart,
  12. Video,
  13. User,
  14. X,
  15. Menu
  16. } from 'lucide-react';
  17. /**
  18. * 主布局组件 - shadcn风格
  19. * 包含侧边栏、顶部导航和内容区域
  20. */
  21. export const MainLayout = () => {
  22. const [sidebarOpen, setSidebarOpen] = useState(false);
  23. const { user } = useAuth();
  24. const navigate = useNavigate();
  25. const location = useLocation();
  26. const handleClassroomClick = (classId = '') => {
  27. if (user?.userType === UserType.TEACHER) {
  28. navigate(`/mobile/classroom/${classId}/${UserType.TEACHER}`);
  29. } else {
  30. navigate(`/mobile/classroom/${classId}/${UserType.STUDENT}`);
  31. }
  32. setSidebarOpen(false);
  33. };
  34. const menuItems = [
  35. {
  36. id: 'public-chatroom',
  37. label: '公开解盘',
  38. icon: <MessageCircle className="w-5 h-5" />,
  39. onClick: () => handleClassroomClick(getGlobalConfig('PUBLIC_CHATROOM_ID'))
  40. },
  41. {
  42. id: 'private-chatroom',
  43. label: '学员解盘',
  44. icon: <Users className="w-5 h-5" />,
  45. onClick: () => handleClassroomClick(getGlobalConfig('PRIVATE_CHATROOM_ID'))
  46. },
  47. // {
  48. // id: 'exam',
  49. // label: '考试模式',
  50. // icon: <FileText className="w-5 h-5" />,
  51. // onClick: () => navigate('/mobile/exam')
  52. // },
  53. {
  54. id: 'training',
  55. label: '训练模式',
  56. icon: <Heart className="w-5 h-5" />,
  57. onClick: () => navigate('/mobile/xunlian')
  58. },
  59. {
  60. id: 'video-replay',
  61. label: '视频回放',
  62. icon: <Video className="w-5 h-5" />,
  63. onClick: () => navigate('/mobile/video-replay')
  64. }
  65. ];
  66. return (
  67. <div className="min-h-screen bg-background flex">
  68. {/* 侧边栏 - 桌面端收起状态 */}
  69. <aside className={cn(
  70. "fixed lg:static inset-y-0 left-0 z-50 w-16 lg:w-16 bg-card border-r border-border transition-all duration-300",
  71. sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
  72. )}>
  73. <div className="flex flex-col h-full">
  74. {/* 侧边栏头部 - 包含用户头像和用户名 */}
  75. <div className="flex flex-col items-center justify-center h-20 border-b border-border py-2">
  76. {user && (
  77. <>
  78. <button
  79. onClick={() => navigate('/mobile/member')}
  80. className="w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center mb-1"
  81. >
  82. <User className="w-4 h-4" />
  83. </button>
  84. <span className="text-xs text-muted-foreground truncate max-w-[60px]">
  85. {user.username}
  86. </span>
  87. </>
  88. )}
  89. <button
  90. onClick={() => setSidebarOpen(false)}
  91. className="lg:hidden p-1 rounded-md hover:bg-muted mt-2"
  92. >
  93. <X className="w-4 h-4" />
  94. </button>
  95. </div>
  96. {/* 菜单项 */}
  97. <nav className="flex-1 px-2 py-4 space-y-2">
  98. {menuItems.map((item) => (
  99. <button
  100. key={item.id}
  101. onClick={item.onClick}
  102. className={cn(
  103. "w-full flex flex-col items-center p-2 rounded-md text-sm font-medium transition-colors",
  104. "hover:bg-muted hover:text-foreground",
  105. "text-muted-foreground hover:text-foreground"
  106. )}
  107. >
  108. <div className="mb-1">
  109. {item.icon}
  110. </div>
  111. <span className="text-xs leading-tight whitespace-nowrap text-center">
  112. {item.label}
  113. </span>
  114. </button>
  115. ))}
  116. </nav>
  117. </div>
  118. </aside>
  119. {/* 遮罩层 - 移动端 */}
  120. {sidebarOpen && (
  121. <div
  122. className="fixed inset-0 bg-background/80 backdrop-blur-sm z-40 lg:hidden"
  123. onClick={() => setSidebarOpen(false)}
  124. />
  125. )}
  126. {/* 主内容区域 */}
  127. <div className="flex-1 flex flex-col min-w-0">
  128. {/* 顶部导航 */}
  129. <header className="bg-card border-b border-border sticky top-0 z-40">
  130. <div className="flex items-center justify-between h-16 px-4">
  131. <div className="flex items-center">
  132. <button
  133. onClick={() => setSidebarOpen(true)}
  134. className="lg:hidden p-2 rounded-md hover:bg-muted mr-2"
  135. >
  136. <Menu className="w-5 h-5" />
  137. </button>
  138. <h1 className="text-xl font-semibold text-foreground">股票训练系统</h1>
  139. </div>
  140. <div className="flex items-center space-x-4">
  141. {user ? null : (
  142. <div className="flex space-x-2">
  143. <button
  144. onClick={() => navigate('/mobile/login')}
  145. className="px-3 py-1.5 rounded-md text-sm bg-primary text-primary-foreground hover:bg-primary/90"
  146. >
  147. 登录
  148. </button>
  149. <button
  150. onClick={() => navigate('/mobile/register')}
  151. className="px-3 py-1.5 rounded-md text-sm bg-secondary text-secondary-foreground hover:bg-secondary/80"
  152. >
  153. 注册
  154. </button>
  155. </div>
  156. )}
  157. </div>
  158. </div>
  159. </header>
  160. {/* 内容区域 */}
  161. <main className="flex-1 overflow-auto p-6">
  162. <Outlet />
  163. </main>
  164. </div>
  165. </div>
  166. );
  167. };