MainLayout.tsx 6.0 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. style={{
  104. overflow: 'auto',
  105. height: '100vh',
  106. position: 'fixed',
  107. left: 0,
  108. top: 0,
  109. bottom: 0,
  110. zIndex: 100,
  111. }}
  112. >
  113. <div className="p-4">
  114. <Typography.Title level={2} className="text-xl font-bold truncate">
  115. {collapsed ? '应用' : appName}
  116. </Typography.Title>
  117. {/* 菜单搜索框 */}
  118. {!collapsed && (
  119. <div className="mb-4">
  120. <Input.Search
  121. placeholder="搜索菜单..."
  122. allowClear
  123. value={searchText}
  124. onChange={(e) => setSearchText(e.target.value)}
  125. />
  126. </div>
  127. )}
  128. </div>
  129. {/* 菜单列表 */}
  130. <Menu
  131. theme='light'
  132. mode="inline"
  133. items={filteredMenuItems}
  134. openKeys={openKeys}
  135. selectedKeys={[selectedKey]}
  136. onOpenChange={onOpenChange}
  137. onClick={({ key }) => handleMenuClick(key)}
  138. inlineCollapsed={collapsed}
  139. />
  140. </Sider>
  141. <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
  142. <Header className="p-0 flex justify-between items-center"
  143. style={{
  144. position: 'sticky',
  145. top: 0,
  146. zIndex: 99,
  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. </Header>
  176. <Content className="m-6" style={{ overflow: 'initial' }}>
  177. <div className="site-layout-content p-6 rounded-lg">
  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. };