| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- import React, { useState, useEffect, useMemo } from 'react';
- import {
- Outlet,
- useLocation,
- } from 'react-router';
- import {
- Layout, Button, Space, Badge, Avatar, Dropdown, Typography, Input, Menu,
- } from 'antd';
- import {
- MenuFoldOutlined,
- MenuUnfoldOutlined,
- BellOutlined,
- VerticalAlignTopOutlined,
- UserOutlined
- } from '@ant-design/icons';
- import { useAuth, useTheme } from '../hooks_sys.tsx';
- import { useMenu, useMenuSearch, type MenuItem } from '../menu.tsx';
- const { Header, Sider, Content } = Layout;
- /**
- * 主布局组件
- * 包含侧边栏、顶部导航和内容区域
- */
- export const MainLayout = () => {
- const { user } = useAuth();
- const { isDark } = useTheme();
- const [showBackTop, setShowBackTop] = useState(false);
- const location = useLocation();
-
- // 使用菜单hook
- const {
- menuItems,
- userMenuItems,
- openKeys,
- collapsed,
- setCollapsed,
- handleMenuClick: handleRawMenuClick,
- onOpenChange
- } = useMenu();
-
- // 处理菜单点击
- const handleMenuClick = (key: string) => {
- const item = findMenuItem(menuItems, key);
- if (item && 'label' in item) {
- handleRawMenuClick(item);
- }
- };
-
- // 查找菜单项
- const findMenuItem = (items: MenuItem[], key: string): MenuItem | null => {
- for (const item of items) {
- if (!item) continue;
- if (item.key === key) return item;
- if (item.children) {
- const found = findMenuItem(item.children, key);
- if (found) return found;
- }
- }
- return null;
- };
-
- // 使用菜单搜索hook
- const {
- searchText,
- setSearchText,
- filteredMenuItems
- } = useMenuSearch(menuItems);
-
- // 获取当前选中的菜单项
- const selectedKey = useMemo(() => {
- const findSelectedKey = (items: MenuItem[]): string | null => {
- for (const item of items) {
- if (!item) continue;
- if (item.path === location.pathname) return item.key || null;
- if (item.children) {
- const childKey = findSelectedKey(item.children);
- if (childKey) return childKey;
- }
- }
- return null;
- };
-
- return findSelectedKey(menuItems) || '';
- }, [location.pathname, menuItems]);
-
- // 检测滚动位置,控制回到顶部按钮显示
- useEffect(() => {
- const handleScroll = () => {
- setShowBackTop(window.pageYOffset > 300);
- };
-
- window.addEventListener('scroll', handleScroll);
- return () => window.removeEventListener('scroll', handleScroll);
- }, []);
-
- // 回到顶部
- const scrollToTop = () => {
- window.scrollTo({
- top: 0,
- behavior: 'smooth'
- });
- };
-
- // 应用名称 - 从CONFIG中获取或使用默认值
- const appName = window.CONFIG?.APP_NAME || '应用Starter';
-
- return (
- <Layout style={{ minHeight: '100vh' }}>
- <Sider
- trigger={null}
- collapsible
- collapsed={collapsed}
- width={240}
- className="custom-sider"
- style={{
- overflow: 'auto',
- height: '100vh',
- position: 'fixed',
- left: 0,
- top: 0,
- bottom: 0,
- zIndex: 100,
- }}
- >
- <div className="p-4">
- <Typography.Title level={2} className="text-xl font-bold truncate">
- {collapsed ? '应用' : appName}
- </Typography.Title>
-
- {/* 菜单搜索框 */}
- {!collapsed && (
- <div className="mb-4">
- <Input.Search
- placeholder="搜索菜单..."
- allowClear
- value={searchText}
- onChange={(e) => setSearchText(e.target.value)}
- />
- </div>
- )}
- </div>
-
- {/* 菜单列表 */}
- <Menu
- theme={isDark ? 'dark' : 'light'}
- mode="inline"
- items={filteredMenuItems}
- openKeys={openKeys}
- selectedKeys={[selectedKey]}
- onOpenChange={onOpenChange}
- onClick={({ key }) => handleMenuClick(key)}
- inlineCollapsed={collapsed}
- />
- </Sider>
-
- <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
- <Header className="p-0 flex justify-between items-center"
- style={{
- position: 'sticky',
- top: 0,
- zIndex: 99,
- boxShadow: '0 1px 4px rgba(0,21,41,0.08)',
- }}
- >
- <Button
- type="text"
- icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
- onClick={() => setCollapsed(!collapsed)}
- className="w-16 h-16"
- />
-
- <Space size="middle" className="mr-4">
- <Badge count={5} offset={[0, 5]}>
- <Button
- type="text"
- icon={<BellOutlined />}
- />
- </Badge>
-
- <Dropdown menu={{ items: userMenuItems }}>
- <Space className="cursor-pointer">
- <Avatar
- src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
- icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
- />
- <span>
- {user?.nickname || user?.username}
- </span>
- </Space>
- </Dropdown>
- </Space>
- </Header>
-
- <Content className="m-6" style={{ overflow: 'initial' }}>
- <div className="site-layout-content p-6 rounded-lg">
- <Outlet />
- </div>
-
- {/* 回到顶部按钮 */}
- {showBackTop && (
- <Button
- type="primary"
- shape="circle"
- icon={<VerticalAlignTopOutlined />}
- size="large"
- onClick={scrollToTop}
- style={{
- position: 'fixed',
- right: 30,
- bottom: 30,
- zIndex: 1000,
- boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
- }}
- />
- )}
- </Content>
- </Layout>
- </Layout>
- );
- };
|