antd 实现 sidebar 左侧菜单·记

本文所用到的知识:react 相关和 antd 等。

关于 antd 之 Layout.Sider 使用说明请移步这里:https://ant.design/components/layout-cn/

本文实现 sidebar 采取的是 “路由与菜单分开管理” 的理念。这样的好处是:

  • 不是所有的路由都需要展示在 sidebar 中;
  • 避免给路由增加控制菜单的属性,降低路由与菜单的耦合度,简化路由的维护。
import { Menu, Layout } from 'antd';
import {
  DatabaseOutlined,
  UnorderedListOutlined,
  HistoryOutlined,
  TeamOutlined,
  DeleteOutlined,
} from '@ant-design/icons';
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import Cookies from "js-cookie";
import "./index.less"

const { SubMenu } = Menu;
const { Sider } = Layout

const Sidebar = (props) => {

  const {
    globalCurrentZoo,
    location,
  } = props;

  const superAdmin = () => {
    return ["marry", "lily"]
  }

  const userName = Cookies.get("username");
  const isSuperAdmin = superAdmin().includes(userName);
  const { pathname } = location;

  const [collapsed, setCollapsed] = useState(false);
  const [zoo, setZoo] = useState("");
  let selectedKeys = [], openKeys = [];

  useEffect(()=>{
    globalCurrentZoo && setZoo(globalCurrentZoo.name)
  }, [globalCurrentZoo]);

  const menuList = [{
    title: "鱼类",
    key: "/animal/:zoo/fish",
    icon: <DatabaseOutlined />,
    path: `/animal/${zoo}/fish`,
    rule: /\/animal\/[\s\S]+\/fish\/?$/,
  },{
    title: "鸟类",
    key: "/animal/:zoo/birds/:type/list",
    icon: <UnorderedListOutlined />,
    subMenus: [{
      title: "鸡",
      key: "/animal/:zoo/birds/chicken/list",
      path: `/animal/${zoo}/birds/chicken/list`,
      rule: /\/animal\/[\s\S]+\/birds\/chicken\/+/,
    },{
      title: "鸭",
      key: "/animal/:zoo/birds/duck/list",
      path: `/animal/${zoo}/birds/duck/list`,
      rule: /\/animal\/[\s\S]+\/birds\/duck\/+/,
    }]
  },{
    title: "两栖类",
    key: "/animal/:zoo/amphibians",
    icon: <HistoryOutlined />,
    path: `/animal/${zoo}/amphibians`,
    rule: /\/animal\/[\s\S]+\/amphibians\/?$/,
  },{
    title: "爬行类",
    key: "/animal/:zoo/reptiles",
    icon: <TeamOutlined />,
    path: `/animal/${zoo}/reptiles`,
    rule: /\/animal\/[\s\S]+\/reptiles\/?$/,
  }];

  const managerMenus = () => {
    {isSuperAdmin ? (
      menuList.push(
        {
          title: "哺乳类",
          key: "/animal/:zoo/mammals",
          icon: <DeleteOutlined />,
          subMenus: [{
            title: "猫",
            key: "/animal/:zoo/mammals/cat",
            path: `/animal/${zoo}/mammals/cat`,
            rule: /\/animal\/[\s\S]+\/mammals\/cat\/?$/,
          },{
            title: "狗",
            key: "/animal/:zoo/mammals/dog",
            path: `/animal/${zoo}/mammals/dog`,
            rule: /\/animal\/[\s\S]+\/mammals\/dog\/?$/,
          }]
        }
      )
    ) : ("")}
  }

  let flag = false;
  const matchPath = (arr, path, defaultRes={}) => {
    let res = {
      key: null,
      parentKey: null,
      ...defaultRes,
    }
    for (let i = 0; i < arr.length; i++) {
      const item = arr[i];
      if(flag) break;
      if(item.rule){
        const rule = item.rule
        const matched = rule.test(path)
        if (!matched) {
          // 当遍历到子菜单的最后一项时将parentKey置为null
          if(i >= arr.length - 1){
            res = { ...res, parentKey: null }
          }
          continue;
        }else{
          let key = item.key
          if(res.parentKey){
            // 多级菜单
            res = { ...res, key }
          }else{
            // 一级菜单
            res = { ...res, key, parentKey: null }
          }
          flag = true;
          break;
        }
      }
      if (item.subMenus && Array.isArray(item.subMenus)) {
        const parentKey = item.key;
        // 递归
        const subMenusResult =  matchPath(item.subMenus, path, { parentKey })
        res = { ...res, ...subMenusResult }
      } 
    }
    return res;
  }
  
  managerMenus();

  const { key, parentKey } = matchPath(menuList, pathname);
  key && (selectedKeys = [key]);
  parentKey && (openKeys = [parentKey]);

  const onCollapse = collapsed => {
    setCollapsed(collapsed);
  };

  return (
    <div className="sidebar-wrap" style={
   
   {width: collapsed ? "80px" : "200px"}}>
      <Sider 
        className="sidebar"
        collapsible
        collapsed={collapsed}
        theme="light"
        onCollapse={onCollapse}
        style={
   
   {position: 'absolute'}}
      >
        <Menu
          className="sidebar-menu"
          mode="inline"
          defaultSelectedKeys={selectedKeys}
          defaultOpenKeys={openKeys}
        >
          {menuList.map((item, idx) => {
            return (
              <React.Fragment key={idx}>
                {item.subMenus ? (
                  <SubMenu 
                    key={item.key}
                    icon={item.icon}
                    title={item.title}
                  >
                    {item.subMenus.map((subItem, subIdx) => {
                      return (
                        <Menu.Item key={subItem.key}>
                          <Link to={subItem.path}>{subItem.title}</Link>
                        </Menu.Item>
                      )
                    })}
                  </SubMenu>
                ) : (
                  <Menu.Item key={item.key} icon={item.icon}>
                    <Link to={item.path}>{item.title}</Link>
                  </Menu.Item>
                )}
              </React.Fragment>
            )
          })}
        </Menu>
      </Sider>
    </div>
  );
}

export default Sidebar;

上述代码完全实现了 sidebar,美中不足是:在封装 matchPath 方法时,需要全局声明一个 flag 变量来控制菜单。这会造成该方法与该组件的强耦合,不方便共用。

于是我对 matchPath 方法做了优化:

const matchPath = (arr, path, defaultRes={}) => {
    let res = {
      key: null,
      parentKey: null,
      ...defaultRes,
    }
    for (let i = 0; i < arr.length; i++) {
      const item = arr[i];
      if(res.parentKey) break;
      if(item.rule){
        const rule = item.rule
        const matched = rule.test(path)
        if (!matched) {
          // 当遍历到子菜单的最后一项时将p置为null
          if(i >= arr.length - 1){
            res = { ...res, p: null }
          }
          continue;
        }else{
          let key = item.key
          if(res.p){
            // 多级菜单
            res = { ...res, key, parentKey:res.p }
          }else{
            // 一级菜单
            res = { ...res, key, parentKey:key }
          }
          break;
        }
      }
      if (item.subMenus && Array.isArray(item.subMenus)) {
        const p = item.key;
        const subMenusResult =  matchPath(item.subMenus, path, {p})
        res = { ...res, ...subMenusResult }
      } 
    }
    return res;
  }

优化后,实现了无需全局声明一个 flag 变量来控制菜单,可是这样会带来一个负面效果:虽然最终的 selectedKeys 和 openKeys 的值是对的,但是 Menu 拿到正确的 openKeys 后,defaultOpenKeys 却是失效的,不知为何,若有幸被大佬看到,求支点迷津(o^^o)。

还有一种优化思路和大家交流一下:就是在写一个函数,在该函数中调用 matchPath 函数,调用之前声明一个变量 flag,初始值是 false。

猜你喜欢

转载自blog.csdn.net/mChales_Liu/article/details/114014199