Nest.js从0到1搭建博客系统---React18后台主框架以及左侧菜单搭建(14)

310 阅读2分钟

世界上最难解的闭包,是你在外面我在里面,似乎永远触及又无法触及

image.png

效果图

image.png

目录结构

image.png

主体布局

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;