世界上最难解的闭包,是你在外面我在里面,似乎永远触及又无法触及
效果图
目录结构
主体布局
import { Layout } from 'antd';
import { useAtom } from 'jotai';
import { Outlet } from 'react-router-dom';
import LayoutFooter from './components/Footer';
import LayoutHeader from './components/Header';
import LayoutMenu from "./components/Menu";
import { collapseAtom } from '@/store/app';
const LayoutIndex: React.FC = () => {
const { Sider, Content } = Layout;
const [collapse] = useAtom(collapseAtom);
return (
// 主体布局
<Layout w-full style={{ height: '100vh' }}>
{/* 侧边栏 */}
<Sider trigger={null} width={220} collapsed={collapse} theme="dark">
<LayoutMenu />
</Sider>
<Layout style={{ display: 'flex', flexDirection: 'column' }}>
{/* 顶部导航栏 */}
<LayoutHeader />
{/* 内容区域 */}
<Content m-2 p-2 style={{ flex: 1, overflowY: 'auto', background: '#fff' }}>
<Outlet />
</Content>
{/* 底部导航栏 */}
<LayoutFooter />
</Layout>
</Layout>
);
};
export default LayoutIndex;
左侧菜单
import { Menu } from "antd";
import { useAtom } from "jotai";
import React, { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import type { MenuProps } from "antd";
import LayoutLogo from './components/Logo';
import { menuListAtom } from "@/services/MenuService";
import { collapseAtom } from '@/store/app';
import { getOpenKeys, searchRoute } from '@/utils';
import './index.scss';
const LayoutMenu: React.FC = () => {
const { pathname } = useLocation();
const [openKeys, setOpenKeys] = useState<string[]>([]);
const [selectedKeys, setSelectedKeys] = useState<string[]>([pathname]);
const [collapse] = useAtom(collapseAtom);
const [menuList] = useAtom(menuListAtom)
console.log(menuList);
// 刷新页面菜单保持高亮
useEffect(() => {
setSelectedKeys([pathname]);
collapse ? null : setOpenKeys(getOpenKeys(pathname));
}, [pathname, collapse]);
// eslint-disable-next-line consistent-return
const onOpenChange = (keys: string[]) => {
if (keys.length === 0 || keys.length === 1) return setOpenKeys(keys);
const latestKeys = keys[keys.length - 1];
if (keys.includes(keys[0])) return setOpenKeys(keys);
setOpenKeys([latestKeys]);
}
// 点击当前菜单跳转页面
const navigate = useNavigate();
const clickMenu: MenuProps["onClick"] = ({ key }: { key: string }) => {
// eslint-disable-next-line react/destructuring-assignment
const route = searchRoute(key, menuList);
if (route.isLink) window.open(route.isLink, "_blank");
navigate(key);
};
return (
<div className="layout-menu">
<LayoutLogo />
<Menu
theme="dark"
mode="inline"
style={{ height: "calc(100vh - 60px)" }}
triggerSubMenuAction="click"
defaultSelectedKeys={[pathname]}
openKeys={openKeys}
selectedKeys={selectedKeys}
items={menuList}
onClick={clickMenu}
onOpenChange={onOpenChange}
/>
</div >
)
}
export default LayoutMenu;
- 样式
.layout-menu{
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
.ant-menu {
&::-webkit-scrollbar {
width: 4px;
background-color: #001529 !important;
}
&::-webkit-scrollbar-thumb {
width: 4px;
background-color: #41444b !important;
}
}
.ant-menu-root{
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
}
- 使用mock模拟菜单数据,后期对接口
import * as Icons from "@ant-design/icons";
import { atom } from 'jotai';
import React from "react";
import type { MenuProps } from "antd";
import { menuListApi } from '@/apis/menuApi';
type MenuItem = Required<MenuProps>["items"][number];
const getItem = (
label: React.ReactNode,
key?: React.Key | null,
icon?: React.ReactNode,
children?: MenuItem[],
type?: "group"
): MenuItem => {
return {
key,
icon,
children,
label,
type
} as MenuItem;
};
// 动态渲染 Icon 图标
const customIcons: { [key: string]: any } = Icons;
const addIcon = (name: string) => {
return React.createElement(customIcons[name]);
};
// 处理后台返回菜单 key 值为 antd 菜单需要的 key 值
const transformMenu = (menuList, newMenuList: MenuItem[] = []) => {
// eslint-disable-next-line consistent-return
menuList.forEach((item) => {
if (!item?.children?.length) return newMenuList.push(getItem(item.title, item.path, addIcon(item.icon!)));
newMenuList.push(getItem(item.title, item.path, addIcon(item.icon!), transformMenu(item.children)));
});
return newMenuList;
};
const menuListAtom = atom(
async () => {
const res = await menuListApi();
return transformMenu(res.data);
}
)
export { menuListAtom };
顶部导航栏
import { Col, Row } from 'antd';
import AssemblySize from "./components/AssemblySize";
import CollapseIcon from "./components/CollapseIcon";
import Fullscreen from "./components/Fullscreen";
import Language from "./components/Language";
import Notify from "./components/Notify";
import Theme from "./components/Theme";
import UserAvatar from "./components/UserAvatar";
const LayoutHeader: React.FC = () => {
return (
<Row h-12 px-2 bg="white" justify="space-between" align="middle">
<Col>
<CollapseIcon />
</Col>
<Col style={{ display: "flex", alignItems: "center" }}>
<Notify />
<AssemblySize />
<Language />
<Theme />
<Fullscreen />
<UserAvatar />
</Col>
</Row>
);
};
export default LayoutHeader;
底部导航栏
const LayoutFooter: React.FC = () => {
return <footer flex justify-center items-center h-8 bg="white">2024 © react-vhen-blog-admin by vhen</footer>;
};
export default LayoutFooter;