Layout.jsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { useState, useEffect } from 'react'
  2. import { Outlet, useNavigate, useLocation } from 'react-router-dom'
  3. import { Layout as AntLayout, Menu, Button, Drawer, Popconfirm } from 'antd'
  4. import {
  5. DashboardOutlined,
  6. UserOutlined,
  7. AppstoreOutlined,
  8. FileTextOutlined,
  9. ExportOutlined,
  10. ImportOutlined,
  11. MenuFoldOutlined,
  12. MenuUnfoldOutlined,
  13. TeamOutlined,
  14. LogoutOutlined,
  15. MenuOutlined
  16. } from '@ant-design/icons'
  17. import { useAuth } from '../contexts/AuthContext'
  18. const { Header, Sider, Content } = AntLayout
  19. const MOBILE_BREAKPOINT = 768
  20. const menuItems = [
  21. {
  22. key: '/dashboard',
  23. icon: <DashboardOutlined />,
  24. label: '仪表盘'
  25. },
  26. {
  27. key: '/persons',
  28. icon: <UserOutlined />,
  29. label: '人员管理'
  30. },
  31. {
  32. key: '/items',
  33. icon: <AppstoreOutlined />,
  34. label: '物品管理'
  35. },
  36. {
  37. key: '/work-records',
  38. icon: <FileTextOutlined />,
  39. label: '工作记录'
  40. },
  41. {
  42. key: '/export',
  43. icon: <ExportOutlined />,
  44. label: '导出报表'
  45. },
  46. {
  47. key: '/import',
  48. icon: <ImportOutlined />,
  49. label: '导入数据'
  50. },
  51. {
  52. key: '/admins',
  53. icon: <TeamOutlined />,
  54. label: '管理员管理'
  55. }
  56. ]
  57. function Layout() {
  58. const [collapsed, setCollapsed] = useState(false)
  59. const [isMobile, setIsMobile] = useState(window.innerWidth < MOBILE_BREAKPOINT)
  60. const [drawerVisible, setDrawerVisible] = useState(false)
  61. const navigate = useNavigate()
  62. const location = useLocation()
  63. const { logout, admin } = useAuth()
  64. // Mobile detection with resize listener
  65. useEffect(() => {
  66. const handleResize = () => {
  67. const mobile = window.innerWidth < MOBILE_BREAKPOINT
  68. setIsMobile(mobile)
  69. if (!mobile) {
  70. setDrawerVisible(false)
  71. }
  72. }
  73. window.addEventListener('resize', handleResize)
  74. return () => window.removeEventListener('resize', handleResize)
  75. }, [])
  76. const handleMenuClick = ({ key }) => {
  77. navigate(key)
  78. if (isMobile) {
  79. setDrawerVisible(false)
  80. }
  81. }
  82. const handleLogout = () => {
  83. logout()
  84. navigate('/login')
  85. }
  86. const toggleDrawer = () => {
  87. setDrawerVisible(!drawerVisible)
  88. }
  89. const menuContent = (
  90. <Menu
  91. theme="dark"
  92. mode="inline"
  93. selectedKeys={[location.pathname]}
  94. items={menuItems}
  95. onClick={handleMenuClick}
  96. />
  97. )
  98. const logoContent = (
  99. <div style={{
  100. height: 32,
  101. margin: 16,
  102. background: 'rgba(255, 255, 255, 0.2)',
  103. borderRadius: 6,
  104. display: 'flex',
  105. alignItems: 'center',
  106. justifyContent: 'center',
  107. color: '#fff',
  108. fontWeight: 'bold',
  109. fontSize: collapsed && !isMobile ? 14 : 16
  110. }}>
  111. {collapsed && !isMobile ? '统计' : '工作统计系统'}
  112. </div>
  113. )
  114. return (
  115. <AntLayout style={{ minHeight: '100vh' }}>
  116. {/* Mobile Drawer */}
  117. {isMobile ? (
  118. <Drawer
  119. placement="left"
  120. onClose={() => setDrawerVisible(false)}
  121. open={drawerVisible}
  122. width={250}
  123. bodyStyle={{ padding: 0, background: '#001529' }}
  124. headerStyle={{ display: 'none' }}
  125. >
  126. {logoContent}
  127. {menuContent}
  128. </Drawer>
  129. ) : (
  130. <Sider
  131. trigger={null}
  132. collapsible
  133. collapsed={collapsed}
  134. style={{
  135. overflow: 'auto',
  136. height: '100vh',
  137. position: 'fixed',
  138. left: 0,
  139. top: 0,
  140. bottom: 0
  141. }}
  142. >
  143. {logoContent}
  144. {menuContent}
  145. </Sider>
  146. )}
  147. <AntLayout style={{ marginLeft: isMobile ? 0 : (collapsed ? 80 : 200), transition: 'margin-left 0.2s' }}>
  148. <Header style={{
  149. padding: '0 16px',
  150. background: '#fff',
  151. display: 'flex',
  152. alignItems: 'center',
  153. justifyContent: 'space-between',
  154. position: 'sticky',
  155. top: 0,
  156. zIndex: 1
  157. }}>
  158. {isMobile ? (
  159. <Button
  160. type="text"
  161. icon={<MenuOutlined />}
  162. onClick={toggleDrawer}
  163. style={{ fontSize: 18, minWidth: 44, minHeight: 44 }}
  164. className="mobile-menu-btn"
  165. />
  166. ) : (
  167. collapsed ? (
  168. <MenuUnfoldOutlined
  169. style={{ fontSize: 18, cursor: 'pointer' }}
  170. onClick={() => setCollapsed(false)}
  171. />
  172. ) : (
  173. <MenuFoldOutlined
  174. style={{ fontSize: 18, cursor: 'pointer' }}
  175. onClick={() => setCollapsed(true)}
  176. />
  177. )
  178. )}
  179. <div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? 8 : 16 }}>
  180. {admin && !isMobile && <span style={{ color: '#666' }}>欢迎, {admin.username}</span>}
  181. <Popconfirm
  182. title="确认登出"
  183. description="确定要退出登录吗?"
  184. onConfirm={handleLogout}
  185. okText="确定"
  186. cancelText="取消"
  187. >
  188. <Button
  189. type="text"
  190. icon={<LogoutOutlined />}
  191. style={{ minWidth: 44, minHeight: 44 }}
  192. className="mobile-touch-target"
  193. >
  194. {!isMobile && '登出'}
  195. </Button>
  196. </Popconfirm>
  197. </div>
  198. </Header>
  199. <Content style={{
  200. margin: isMobile ? 12 : 24,
  201. padding: isMobile ? 12 : 24,
  202. background: '#fff',
  203. borderRadius: 8,
  204. minHeight: 280
  205. }}>
  206. <Outlet />
  207. </Content>
  208. </AntLayout>
  209. </AntLayout>
  210. )
  211. }
  212. export default Layout