Layout.jsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import React, { useState, useEffect } from 'react';
  2. import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom';
  3. import { Layout as AntLayout, Menu, Button, Dropdown, Avatar } from 'antd';
  4. import {
  5. HomeOutlined,
  6. CheckSquareOutlined,
  7. ScheduleOutlined,
  8. RobotOutlined,
  9. BarChartOutlined,
  10. UserOutlined,
  11. LogoutOutlined,
  12. MenuFoldOutlined,
  13. MenuUnfoldOutlined
  14. } from '@ant-design/icons';
  15. import { useAuth } from '../context/AuthContext';
  16. const { Header, Sider, Content } = AntLayout;
  17. const Layout = () => {
  18. const { user, logout } = useAuth();
  19. const location = useLocation();
  20. const navigate = useNavigate();
  21. const [collapsed, setCollapsed] = useState(false);
  22. const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
  23. // 监听窗口大小变化
  24. useEffect(() => {
  25. const handleResize = () => {
  26. setIsMobile(window.innerWidth < 768);
  27. if (window.innerWidth < 768) {
  28. setCollapsed(true);
  29. }
  30. };
  31. window.addEventListener('resize', handleResize);
  32. handleResize();
  33. return () => window.removeEventListener('resize', handleResize);
  34. }, []);
  35. const toggleCollapsed = () => {
  36. setCollapsed(!collapsed);
  37. };
  38. // 当前选中的菜单项
  39. const selectedKey = (() => {
  40. const path = location.pathname;
  41. if (path === '/') return '1';
  42. if (path === '/tasks') return '2';
  43. if (path === '/schedule') return '3';
  44. if (path === '/ai-chat') return '4';
  45. if (path === '/stats') return '5';
  46. return '1';
  47. })();
  48. // 用户菜单
  49. const userMenu = {
  50. items: [
  51. {
  52. key: '1',
  53. label: (
  54. <div className="flex items-center">
  55. <UserOutlined className="mr-2" />
  56. <span>{user?.email}</span>
  57. </div>
  58. ),
  59. },
  60. {
  61. key: '2',
  62. label: (
  63. <div className="flex items-center text-red-600" onClick={logout}>
  64. <LogoutOutlined className="mr-2" />
  65. <span>退出登录</span>
  66. </div>
  67. ),
  68. }
  69. ],
  70. };
  71. return (
  72. <AntLayout className="min-h-screen">
  73. <Sider
  74. trigger={null}
  75. collapsible
  76. collapsed={collapsed}
  77. className="bg-white shadow-md"
  78. width={220}
  79. collapsedWidth={isMobile ? 0 : 80}
  80. style={{
  81. overflow: 'auto',
  82. height: '100vh',
  83. position: 'fixed',
  84. left: 0,
  85. top: 0,
  86. bottom: 0,
  87. zIndex: 999,
  88. display: isMobile && collapsed ? 'none' : 'block'
  89. }}
  90. >
  91. <div className="flex justify-center items-center h-16 m-4">
  92. <Link to="/" className="flex items-center">
  93. <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3B82F6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-check-circle">
  94. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
  95. <polyline points="22 4 12 14.01 9 11.01"/>
  96. </svg>
  97. {!collapsed && <span className="ml-2 text-lg font-semibold text-primary-500">任务管理系统</span>}
  98. </Link>
  99. </div>
  100. <Menu
  101. theme="light"
  102. mode="inline"
  103. selectedKeys={[selectedKey]}
  104. className="border-r-0 mt-4"
  105. items={[
  106. {
  107. key: '1',
  108. icon: <HomeOutlined />,
  109. label: '仪表盘',
  110. onClick: () => navigate('/')
  111. },
  112. {
  113. key: '2',
  114. icon: <CheckSquareOutlined />,
  115. label: '任务管理',
  116. onClick: () => navigate('/tasks')
  117. },
  118. {
  119. key: '3',
  120. icon: <ScheduleOutlined />,
  121. label: '时间安排',
  122. onClick: () => navigate('/schedule')
  123. },
  124. {
  125. key: '4',
  126. icon: <RobotOutlined />,
  127. label: 'AI 助手',
  128. onClick: () => navigate('/ai-chat')
  129. },
  130. {
  131. key: '5',
  132. icon: <BarChartOutlined />,
  133. label: '数据统计',
  134. onClick: () => navigate('/stats')
  135. },
  136. ]}
  137. />
  138. </Sider>
  139. <AntLayout style={{ marginLeft: isMobile ? 0 : (collapsed ? 80 : 220), transition: 'margin-left 0.2s' }}>
  140. <Header className="bg-white p-0 px-4 flex justify-between items-center shadow-sm z-10">
  141. <Button
  142. type="text"
  143. icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
  144. onClick={toggleCollapsed}
  145. className="mr-3"
  146. />
  147. <Dropdown menu={userMenu} placement="bottomRight">
  148. <div className="flex items-center cursor-pointer">
  149. <Avatar icon={<UserOutlined />} className="bg-primary-500" />
  150. {!isMobile && <span className="ml-2">{user?.email}</span>}
  151. </div>
  152. </Dropdown>
  153. </Header>
  154. <Content className="m-6 p-6 bg-white rounded-lg shadow-sm">
  155. <Outlet />
  156. </Content>
  157. </AntLayout>
  158. {/* 移动端侧边栏蒙层 */}
  159. {isMobile && !collapsed && (
  160. <div
  161. className="fixed inset-0 bg-black bg-opacity-50 z-40"
  162. onClick={toggleCollapsed}
  163. />
  164. )}
  165. {/* 移动端收起时显示展开按钮 */}
  166. {isMobile && collapsed && (
  167. <Button
  168. type="primary"
  169. shape="circle"
  170. icon={<MenuUnfoldOutlined />}
  171. onClick={toggleCollapsed}
  172. style={{
  173. position: 'fixed',
  174. top: 16,
  175. left: 16,
  176. zIndex: 2000,
  177. boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
  178. }}
  179. />
  180. )}
  181. </AntLayout>
  182. );
  183. };
  184. export default Layout;