MainLayout.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import React, { useState, useEffect, useMemo } from 'react';
  2. import {
  3. Outlet,
  4. useLocation,
  5. } from 'react-router';
  6. import {
  7. Layout, Button, Space, Badge, Avatar, Dropdown, Typography, Input, Menu,
  8. } from 'antd';
  9. import {
  10. MenuFoldOutlined,
  11. MenuUnfoldOutlined,
  12. BellOutlined,
  13. VerticalAlignTopOutlined,
  14. UserOutlined
  15. } from '@ant-design/icons';
  16. import { useAuth } from '../hooks/AuthProvider';
  17. import { useMenu, useMenuSearch, type MenuItem } from '../menu';
  18. import { getGlobalConfig } from '@/client/utils/utils';
  19. const { Header, Sider, Content } = Layout;
  20. /**
  21. * 主布局组件
  22. * 包含侧边栏、顶部导航和内容区域
  23. */
  24. export const MainLayout = () => {
  25. const { user } = useAuth();
  26. const [showBackTop, setShowBackTop] = useState(false);
  27. const location = useLocation();
  28. // 使用菜单hook
  29. const {
  30. menuItems,
  31. userMenuItems,
  32. openKeys,
  33. collapsed,
  34. setCollapsed,
  35. handleMenuClick: handleRawMenuClick,
  36. onOpenChange
  37. } = useMenu();
  38. // 处理菜单点击
  39. const handleMenuClick = (key: string) => {
  40. const item = findMenuItem(menuItems, key);
  41. if (item && 'label' in item) {
  42. handleRawMenuClick(item);
  43. }
  44. };
  45. // 查找菜单项
  46. const findMenuItem = (items: MenuItem[], key: string): MenuItem | null => {
  47. for (const item of items) {
  48. if (!item) continue;
  49. if (item.key === key) return item;
  50. if (item.children) {
  51. const found = findMenuItem(item.children, key);
  52. if (found) return found;
  53. }
  54. }
  55. return null;
  56. };
  57. // 使用菜单搜索hook
  58. const {
  59. searchText,
  60. setSearchText,
  61. filteredMenuItems
  62. } = useMenuSearch(menuItems);
  63. // 获取当前选中的菜单项
  64. const selectedKey = useMemo(() => {
  65. const findSelectedKey = (items: MenuItem[]): string | null => {
  66. for (const item of items) {
  67. if (!item) continue;
  68. if (item.path === location.pathname) return item.key || null;
  69. if (item.children) {
  70. const childKey = findSelectedKey(item.children);
  71. if (childKey) return childKey;
  72. }
  73. }
  74. return null;
  75. };
  76. return findSelectedKey(menuItems) || '';
  77. }, [location.pathname, menuItems]);
  78. // 检测滚动位置,控制回到顶部按钮显示
  79. useEffect(() => {
  80. const handleScroll = () => {
  81. setShowBackTop(window.pageYOffset > 300);
  82. };
  83. window.addEventListener('scroll', handleScroll);
  84. return () => window.removeEventListener('scroll', handleScroll);
  85. }, []);
  86. // 回到顶部
  87. const scrollToTop = () => {
  88. window.scrollTo({
  89. top: 0,
  90. behavior: 'smooth'
  91. });
  92. };
  93. // 应用名称 - 从CONFIG中获取或使用默认值
  94. const appName = getGlobalConfig('APP_NAME') || '应用Starter';
  95. return (
  96. <Layout style={{ minHeight: '100vh' }}>
  97. <Sider
  98. trigger={null}
  99. collapsible
  100. collapsed={collapsed}
  101. width={240}
  102. className="custom-sider"
  103. theme='light'
  104. style={{
  105. overflow: 'auto',
  106. height: '100vh',
  107. position: 'fixed',
  108. left: 0,
  109. top: 0,
  110. bottom: 0,
  111. zIndex: 100,
  112. transition: 'all 0.2s ease',
  113. boxShadow: '2px 0 8px 0 rgba(29, 35, 41, 0.05)',
  114. }}
  115. >
  116. <div className="p-4">
  117. <Typography.Title level={2} className="text-xl font-bold truncate">
  118. {collapsed ? '应用' : appName}
  119. </Typography.Title>
  120. {/* 菜单搜索框 */}
  121. {!collapsed && (
  122. <div className="mb-4">
  123. <Input.Search
  124. placeholder="搜索菜单..."
  125. allowClear
  126. value={searchText}
  127. onChange={(e) => setSearchText(e.target.value)}
  128. />
  129. </div>
  130. )}
  131. </div>
  132. {/* 菜单列表 */}
  133. <Menu
  134. theme='light'
  135. mode="inline"
  136. items={filteredMenuItems}
  137. openKeys={openKeys}
  138. selectedKeys={[selectedKey]}
  139. onOpenChange={onOpenChange}
  140. onClick={({ key }) => handleMenuClick(key)}
  141. inlineCollapsed={collapsed}
  142. />
  143. </Sider>
  144. <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
  145. <div className="sticky top-0 z-50 bg-white shadow-sm transition-all duration-200 h-16 flex items-center justify-between pl-2"
  146. style={{
  147. boxShadow: '0 1px 4px rgba(0,21,41,0.08)'
  148. }}
  149. >
  150. <Button
  151. type="text"
  152. icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
  153. onClick={() => setCollapsed(!collapsed)}
  154. className="w-16 h-16"
  155. />
  156. <Space size="middle" className="mr-4">
  157. <Badge count={5} offset={[0, 5]}>
  158. <Button
  159. type="text"
  160. icon={<BellOutlined />}
  161. />
  162. </Badge>
  163. <Dropdown menu={{ items: userMenuItems }}>
  164. <Space className="cursor-pointer">
  165. <Avatar
  166. src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
  167. icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
  168. />
  169. <span>
  170. {user?.nickname || user?.username}
  171. </span>
  172. </Space>
  173. </Dropdown>
  174. </Space>
  175. </div>
  176. <Content className="m-6" style={{ overflow: 'initial', transition: 'all 0.2s ease' }}>
  177. <div className="site-layout-content p-6 rounded-lg bg-white shadow-sm">
  178. <Outlet />
  179. </div>
  180. {/* 回到顶部按钮 */}
  181. {showBackTop && (
  182. <Button
  183. type="primary"
  184. shape="circle"
  185. icon={<VerticalAlignTopOutlined />}
  186. size="large"
  187. onClick={scrollToTop}
  188. style={{
  189. position: 'fixed',
  190. right: 30,
  191. bottom: 30,
  192. zIndex: 1000,
  193. boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
  194. }}
  195. />
  196. )}
  197. </Content>
  198. </Layout>
  199. </Layout>
  200. );
  201. };