react + Material UI递归实现侧边导航(多层级)

项目使用MUI 5实现,过程中需要使用到侧边导航,翻看了整个文档,最终决定使用accordion来实现多层级的侧边导航
先看效果图

在这里插入图片描述
在这里插入图片描述

先看完整代码
import React from 'react'
import { styled } from '@mui/material/styles'
import type { AccordionProps } from '@mui/material/Accordion'
import type { AccordionSummaryProps } from '@mui/material/AccordionSummary'
import MuiAccordionSummary from '@mui/material/AccordionSummary'
import MuiAccordionDetails from '@mui/material/AccordionDetails'
import { menuList } from './config'
import IconArrow from '~icons/common/arrow.svg'

const Accordion = styled((props: AccordionProps) => <Mui.Accordion disableGutters elevation={0} square {...props} />)(
  ({ theme }) => ({
    '&:not(:last-child)': {
      borderBottom: 0
    },
    '&:before': {
      display: 'none'
    }
  })
)
const AccordionSummary = styled((props: AccordionSummaryProps) => <MuiAccordionSummary {...props} />)(({ theme }) => ({
  backgroundColor: 'rgba(255, 255, 255, .05)',
  '& .MuiAccordionSummary-expandIconWrapper': {
    transform: 'rotate(90deg)'
  },
  '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
    transform: 'rotate(180deg)'
  }
}))

const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
  padding: theme.spacing(2),
  cursor: 'pointer',
  display: 'flex',
  alignItems: 'center'
}))

const activeStyles = {
  backgroundColor: 'rgba(38, 91, 50, 0.1)',
  transition: 'background-color .6s',
  color: '#265B32'
}
interface NavData {
  key: string
  level: number
  name: string
  icon: string
  path?: string
  children?: AnyObject[]
}

function Menu() {
  const navigate = useNavigate()
  const { pathname, state } = useLocation()
  const [expanded, setExpanded] = React.useState<string | false>('panel1')
  const [currentNav, setCurrentNav] = useState('')
  const [selected, setSelected] = useState<string[]>([])

  const handleExpanded = (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => {
    setExpanded(newExpanded ? panel : false)
  }

  const handleClick = (item: AnyObject) => () => {
    if (!item.children) {
      if (item.path !== pathname) {
        navigate(item.path, { state: item.key })
        setCurrentNav(item.path)
      }
    }
  }

  function getAllParentArr(data: AnyObject[], key: string) {
    for (const i in data) {
      if (data[i].key === key) {
        return [data[i]]
      }
      if (data[i]?.children) {
        const node = getAllParentArr(data[i].children, key) as AnyObject[]
        if (node) {
          return node.concat(data[i])
        }
      }
    }
  }

  useEffect(() => {
    setCurrentNav(pathname)
    const result = getAllParentArr(menuList, state) as AnyObject[]
    if (result && result.length) {
      setSelected(result.map((item: AnyObject) => item.key))
    }
  }, [pathname])

  function renderMenu(data: AnyObject[], level: number) {
    if (data) {
      return data.map((value: any) => {
        value.level = level + 1
        return renderSubMenu(value, value.level)
      })
    }
  }
  function renderSubMenu(data: NavData, level: number) {
    if (!data.children) {
      return (
        <AccordionDetails
          className={`hover:text-green-700 `}
          style={currentNav === data.path ? activeStyles : {}}
          key={data.key}
          onClick={handleClick(data)}
          sx={{ pl: data.level }}
        >
          {data.level === 1 && data.icon}
          <span className="text-size-14px ml-8px">{data.name}</span>
        </AccordionDetails>
      )
    } else {
      return (
        <Accordion defaultExpanded={true} onChange={handleExpanded(data.key)} key={data.key}>
          <AccordionSummary
            expandIcon={data.children && <IconArrow />}
            className={`hover:text-green-700`}
            id={data.key}
            onClick={handleClick(data)}
            sx={{ pl: data.level }}
          >
            <div className="flex items-center">
              <div className={` ${selected.includes(data.key) ? 'text-green-700' : ''}`}>
                {data.level === 1 && data.icon}
              </div>
              <span
                className={`font-rm text-size-14px ${data.level === 1 ? 'ml-4px' : 'ml-10px'} ${
                  selected.includes(data.key) ? 'text-green-700' : ''
                }`}
              >
                {data.name}
              </span>
            </div>
          </AccordionSummary>
          {data.children && renderMenu(data.children, level + 1)}
        </Accordion>
      )
    }
  }

  return (
    <div className="relative bg-white w-246px h-100vh border-r-grey-2 b-r b-r-dashed flex-shrink-0">
      <div className="py-24px ml-16px font-rb text-size-20px">STC</div>
      {renderMenu(menuList, 0)}
    </div>
  )
}
export default Menu

代码分析
1.实现accordion组件自定义样式
// 通过styled设置Accordion\AccordionSummary\AccordionDetails样式
const Accordion = styled((props: AccordionProps) => <Mui.Accordion disableGutters elevation={0} square {...props} />)(
  ({ theme }) => ({
    '&:not(:last-child)': {
      borderBottom: 0
    },
    '&:before': {
      display: 'none'
    }
  })
)
const AccordionSummary = styled((props: AccordionSummaryProps) => <MuiAccordionSummary {...props} />)(({ theme }) => ({
  backgroundColor: 'rgba(255, 255, 255, .05)',
  '& .MuiAccordionSummary-expandIconWrapper': {
    transform: 'rotate(90deg)'
  },
  '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
    transform: 'rotate(180deg)'
  }
}))

const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
  padding: theme.spacing(2),
  cursor: 'pointer',
  display: 'flex',
  alignItems: 'center'
}))
2. 通过数据创建需要递归生成的accordion
// level 用来进行下级菜单的位置缩进
  function renderMenu(data: AnyObject[], level: number) {
    if (data) {
      return data.map((value: any) => {
        value.level = level + 1
        return renderSubMenu(value, value.level)
      })
    }
  }
  function renderSubMenu(data: NavData, level: number) {
  // 没有children的直接通过AccordionDetails渲染
    if (!data.children) {
      return (
        <AccordionDetails
          className={`hover:text-green-700 `}
          style={currentNav === data.path ? activeStyles : {}}
          key={data.key}
          onClick={handleClick(data)}
          sx={{ pl: data.level }}
        >
          {data.level === 1 && data.icon}
          <span className="text-size-14px ml-8px">{data.name}</span>
        </AccordionDetails>
      )
    } else {
      return (
      // 有children的通过Accordion渲染,可进行展开收缩
        <Accordion defaultExpanded={true} onChange={handleExpanded(data.key)} key={data.key}>
          <AccordionSummary
            expandIcon={data.children && <IconArrow />}
            className={`hover:text-green-700`}
            id={data.key}
            onClick={handleClick(data)}
            sx={{ pl: data.level }}
          >
            <div className="flex items-center">
              <div className={` ${selected.includes(data.key) ? 'text-green-700' : ''}`}>
                {data.level === 1 && data.icon}
              </div>
              <span
                className={`font-rm text-size-14px ${data.level === 1 ? 'ml-4px' : 'ml-10px'} ${
                  selected.includes(data.key) ? 'text-green-700' : ''
                }`}
              >
                {data.name}
              </span>
            </div>
          </AccordionSummary>
          // 递归继续渲染子级
          {data.children && renderMenu(data.children, level + 1)}
        </Accordion>
      )
    }
  }
3. 逻辑部分
// 控制导航展开或关闭
const handleExpanded = (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => {
    setExpanded(newExpanded ? panel : false)
  }
// 点击当前导航,修改路由,并通过state存储key, 用于每次刷新页面时高亮已选中的导航所有父级
  const handleClick = (item: AnyObject) => () => {
    if (!item.children) {
      if (item.path !== pathname) {
        navigate(item.path, { state: item.key })
        setCurrentNav(item.path)
      }
    }
  }
// 通过子级key获取所有父元素key,用于高亮展示当前导航的所有父级文案
  function getAllParentArr(data: AnyObject[], key: string) {
    for (const i in data) {
      if (data[i].key === key) {
        return [data[i]]
      }
      if (data[i]?.children) {
        const node = getAllParentArr(data[i].children, key) as AnyObject[]
        if (node) {
          return node.concat(data[i])
        }
      }
    }
  }
// 路由改变时重置当前导航相关的所有父级
  useEffect(() => {
    setCurrentNav(pathname)
    const result = getAllParentArr(menuList, state) as AnyObject[]
    if (result && result.length) {
      setSelected(result.map((item: AnyObject) => item.key))
    }
  }, [pathname])
4. menuList部分结构
import Icon from '~icons/icon.svg'

export const menuList = [
  {
    key: 'k-1',
    name: 'Key1',
    icon: <Icon />,
    children: [
      {
        key: 'k-11',
        name: 'Key11',
        children: [
          {
            key: 'k-111',
            name: 'Key111',
            children: [
              {
                key: 'k-1111',
                name: 'Key111',
                path: '/key111'
              }
            ]
          }
        ]
      }
    ]
  },
  {
    key: 'demo',
    name: 'Demo',
    icon: <Icon />,
    path: '/demo'
  }
]
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用React和Ant Design来实现一个用户管理功能非常简单。首先,我们需要创建一个React应用程序。可以使用脚手架工具(如Create React App)来快速搭建起一个React应用的基本结构。 1. 创建React应用程序 使用以下命令来创建一个新的React应用程序: ``` npx create-react-app user-management cd user-management ``` 2. 安装Ant Design 在项目根目录下运行以下命令来安装Ant Design依赖项: ``` npm install antd ``` 3. 集成Ant Design组件 编辑src/App.js文件,导入所需的Ant Design组件并编写用户管理功能的代码。以下是一个简单的示例: ```jsx import React, { useState } from 'react'; import { Table, Button, Modal, Form, Input } from 'antd'; const App = () => { const [users, setUsers] = useState([]); const [isModalVisible, setIsModalVisible] = useState(false); const columns = [ { title: '姓名', dataIndex: 'name', key: 'name' }, { title: '年龄', dataIndex: 'age', key: 'age' }, { title: '操作', key: 'action', render: (text, record) => <Button onClick={() => deleteUser(record.key)}>删除</Button> } ]; const [form] = Form.useForm(); const addUser = () => { form.validateFields().then(values => { const user = { name: values.name, age: values.age, key: users.length + 1 }; setUsers([...users, user]); form.resetFields(); setIsModalVisible(false); }); }; const deleteUser = (key) => { setUsers(users.filter(user => user.key !== key)); }; return ( <div> <Button onClick={() => setIsModalVisible(true)}>添加用户</Button> <Table dataSource={users} columns={columns} /> <Modal title="添加用户" visible={isModalVisible} onCancel={() => setIsModalVisible(false)} onOk={addUser}> <Form form={form}> <Form.Item name="name" label="姓名" rules={[{ required: true }]}> <Input /> </Form.Item> <Form.Item name="age" label="年龄" rules={[{ required: true }]}> <Input /> </Form.Item> </Form> </Modal> </div> ); }; export default App; ``` 4. 运行应用 使用以下命令来运行应用程序: ``` npm start ``` 应用程序将在浏览器中自动打开。您现在就可以通过点击“添加用户”按钮来添加用户,点击“删除”按钮来删除用户,并且可以通过Ant Design的Table组件来显示用户列表。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值