/** * Layout主布局组件 * 作者: 阿瑞 * 功能: 应用主布局,包含导航菜单、用户信息、主题切换等功能 * 版本: v3.0 - 性能优化版本,移除realDark,添加React.memo优化 */ import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { useRouter } from 'next/router'; import { ConfigProvider, Divider, Dropdown, message, Modal, Button } from 'antd'; import { LogoutOutlined, TeamOutlined, UserOutlined } from '@ant-design/icons'; import { PageContainer, ProConfigProvider, ProLayout, type MenuDataItem, type ProSettings } from '@ant-design/pro-components'; import { Icon } from '@iconify/react'; import { MdDarkMode, MdLightMode } from "react-icons/md"; import { useUserActions, useUserInfo } from '@/store/userStore'; import PersonalInfo from './PersonalInfo'; import { IPermission } from '@/models/types'; import { useTheme } from '@/utils/theme'; import ThemeSwitcher from './ThemeSwitcher'; import ScriptLibrary from '../ScriptLibrary/index'; import TodoManager from '../TodoManager/index'; import { useSocket } from '@/hooks/useSocket'; // Layout组件的Props类型定义 interface LayoutProps { children: React.ReactNode; } // 生成动态路由的函数 - 优化版本,添加缓存机制 const routeCache = new Map(); const generateDynamicRoutes = (permissions: IPermission[]): MenuDataItem[] => { if (!permissions?.length) return []; // 关键代码行注释:生成缓存键 const cacheKey = permissions.map(p => `${p.路径 || ''}-${p.排序 ?? 0}`).join('|'); // 关键代码行注释:尝试从缓存获取 if (routeCache.has(cacheKey)) { return routeCache.get(cacheKey)!; } // 对权限进行排序 const sortedPermissions = permissions.sort((a, b) => (a.排序 ?? 0) - (b.排序 ?? 0)); // 映射权限数据到菜单项 const routes = sortedPermissions.map(permission => ({ path: permission.路径 || '/', name: permission.名称 || '', icon: , component: './DynamicComponent', routes: permission.子级 ? generateDynamicRoutes(permission.子级) : undefined }) as Partial); // 关键代码行注释:存储到缓存 routeCache.set(cacheKey, routes); // 关键代码行注释:限制缓存大小 if (routeCache.size > 50) { const firstKey = routeCache.keys().next().value; if (firstKey) { routeCache.delete(firstKey); } } return routes; }; // 默认props配置 const defaultProps = { route: { path: '/', routes: [], }, location: { pathname: '/', }, }; // 头部标题渲染组件 - 使用React.memo优化性能 const HeaderTitle: React.FC = React.memo(() => ( logo 私域管理系统 )); HeaderTitle.displayName = 'HeaderTitle'; // 用户信息显示组件 - 新增独立组件,使用React.memo优化 const UserInfoDisplay: React.FC<{ userInfo: any; collapsed?: boolean; }> = React.memo(({ userInfo, collapsed }) => { if (collapsed) return null; return ( <> {/* 团队信息部分 */}
团队:{userInfo?.团队?.名称 || 'N/A'}
{/* 角色信息部分 */}
角色:{userInfo?.角色?.名称 || 'N/A'}
); }); UserInfoDisplay.displayName = 'UserInfoDisplay'; // 菜单底部渲染组件 - 优化版本,使用React.memo const MenuFooter: React.FC<{ collapsed?: boolean; userInfo: any; onShowScriptLibrary: () => void; onShowTodoManager: () => void; }> = React.memo(({ collapsed, userInfo, onShowScriptLibrary, onShowTodoManager }) => { if (collapsed) return undefined; return (
{/* 用户信息部分 */} {/* 话术库模态框按钮 */}
{/* 代办事项抽屉按钮 */}
{/* 分割线 */} {/* 底部版权信息 */}
© 2024 AOUN
); }); MenuFooter.displayName = 'MenuFooter'; // 主题切换按钮组件 - 新增独立组件,使用React.memo优化 const ThemeToggleButton: React.FC<{ toggleTheme: () => void; navTheme: 'light' | 'dark'; }> = React.memo(({ toggleTheme, navTheme }) => ( )); ThemeToggleButton.displayName = 'ThemeToggleButton'; // Layout组件主体 const Layout: React.FC = ({ children }) => { // 状态管理 const router = useRouter(); const userInfo = useUserInfo(); const userActions = useUserActions(); // 初始化 Socket.IO 连接 useSocket(); const [isClient, setIsClient] = useState(false); const [dynamicRoutes, setDynamicRoutes] = useState([]); const [showPersonalInfo, setShowPersonalInfo] = useState(false); const { navTheme, toggleTheme, changePrimaryColor, themeToken } = useTheme(() => { }); const [showScriptLibrary, setShowScriptLibrary] = useState(false); const [showTodoManager, setShowTodoManager] = useState(false); // 使用 useMemo 优化 settings 计算 - 优化依赖项 const settings = useMemo>(() => ({ fixSiderbar: true, layout: "mix", splitMenus: false, navTheme: navTheme === 'dark' ? 'realDark' : 'light', contentWidth: "Fluid", colorPrimary: themeToken.colorPrimary, title: "私域管理系统V3", siderMenuType: "sub", fixedHeader: true, menuHeaderRender: false, }), [navTheme, themeToken.colorPrimary]); // 使用 useCallback 优化事件处理函数 - 优化依赖项 const handleLogout = useCallback(() => { router.push('/'); userActions.clearUserInfoAndToken(); }, [router, userActions]); const handleShowPersonalInfo = useCallback(() => { setShowPersonalInfo(true); }, []); const handleClosePersonalInfo = useCallback(() => { setShowPersonalInfo(false); }, []); const handleShowScriptLibrary = useCallback(() => { setShowScriptLibrary(true); }, []); const handleCloseScriptLibrary = useCallback(() => { setShowScriptLibrary(false); }, []); const handleShowTodoManager = useCallback(() => { setShowTodoManager(true); }, []); const handleCloseTodoManager = useCallback(() => { setShowTodoManager(false); }, []); const handleMenuItemClick = useCallback((path: string) => { router.push(path || '/'); }, [router]); // 用户信息更新逻辑 - 优化依赖项 const { fetchAndSetUserInfo } = useUserActions(); useEffect(() => { const updateUserInfo = async () => { try { await fetchAndSetUserInfo(); } catch (error) { message.error('无法更新用户信息'); console.error('用户信息更新失败:', error); } }; updateUserInfo(); }, [fetchAndSetUserInfo]); // 客户端渲染检测 useEffect(() => { setIsClient(true); }, []); // 动态路由生成 - 使用 useMemo 优化,添加更精确的依赖项 const memoizedRoutes = useMemo(() => { if (userInfo?.角色?.权限) { return generateDynamicRoutes(userInfo.角色.权限); } return []; }, [userInfo?.角色?.权限]); // 更新动态路由 - 优化依赖项 useEffect(() => { setDynamicRoutes(memoizedRoutes); }, [memoizedRoutes]); // 下拉菜单配置 - 使用 useMemo 优化,稳定化依赖项 const dropdownMenuItems = useMemo(() => [ { key: 'profile', icon: , label: '个人资料', onClick: handleShowPersonalInfo, }, { key: 'logout', icon: , label: '退出登录', onClick: handleLogout }, ], [handleShowPersonalInfo, handleLogout]); // 菜单项渲染函数 - 使用 useCallback 优化 const renderMenuItem = useCallback((item: any, dom: React.ReactNode) => (
handleMenuItemClick(item.path)} style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} > {item.icon && ( {item.icon} )} {dom}
), [handleMenuItemClick]); // 头像渲染函数 - 使用 useCallback 优化,稳定化依赖项 const renderAvatar = useCallback((_props: any, dom: React.ReactNode) => ( <> {/* 主题切换按钮 */} {/* 主题色选择器 */} {/* 下拉菜单按钮 */} {dom} ), [themeToken.colorPrimary, changePrimaryColor, toggleTheme, navTheme, dropdownMenuItems]); // 菜单底部渲染函数 - 使用useCallback优化 const renderMenuFooter = useCallback((props: any) => ( ), [userInfo, handleShowScriptLibrary, handleShowTodoManager]); // 如果不在客户端,不渲染任何内容 if (!isClient) { return null; } return (
document.getElementById('test-pro-layout') || document.body} > dynamicRoutes, collapsedShowGroupTitle: true }} avatarProps={{ src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD$IOql2/weixintupian_20170331104822.jpg', size: 'small', title: userInfo?.姓名 || 'N/A', render: renderAvatar, }} headerTitleRender={() => } menuFooterRender={renderMenuFooter} menuItemRender={renderMenuItem} {...settings} style={{ height: '100vh', background: 'transparent', }} contentStyle={{ background: 'var(--glass-bg)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)', display: 'flex', flexDirection: 'column', padding: 0, margin: 0, }} contentWidth="Fluid" locale="zh-CN" > {/* 内容区域边距移除方案说明: 为了完全移除页面内容的左右空白,采用了三层防护措施: 1. ProLayout 层级:通过 token.pageContainer.paddingInlinePageContainerContent = 0 移除 ProLayout 组件默认的左右内边距 2. PageContainer 层级: - pageHeaderRender={false} 禁用头部渲染避免额外空间 - token.paddingInlinePageContainerContent = 0 再次确保移除左右内边距 - style.padding = 0 移除组件自身样式内边距 3. 最内层 div:padding = '0px' 作为最后一道防线 这样的多层配置确保在不同版本的 Ant Design Pro 中都能正常工作 */} {/* 最内层容器 - 实际承载页面内容的容器 */}
{children}
{/* 个人资料模态框 */} {/* 话术库模态框 */} {/* 待办事项管理器 */}
); }; // 使用React.memo包装整个Layout组件 - 性能优化最后一步 export default React.memo(Layout);