menu组件的开发 | 青训营笔记

71 阅读3分钟

这是我参与「 第五届青训营 」伴学笔记创作活动的第 5 天

组件库:menu组件的开发

一、需求分析

1、渲染嵌套树形结构

2、节点连接线

3、节点展开/收起

4、节点勾选

5、点击选择

6、自定义图标

7、默认状态

8、节点禁用

9、增删改操作

10、虚拟滚动

二、结构表示

interface IMenuNode {
	label: string
  id: string
  children: IMenuNode[]
  
  selected?: boolean // 选中
  checked?: boolean // 勾选
  expanded?: boolean // 展开
  
  disableSelect?: boolean
  disableCheck?: boolean
  disableToggle?: boolean
}
interface IInnerMenuNode extends IMenuNode {
  parentId?: string // 父节点ID
  level: number // 父节点层级
  isLeaf?: boolean // 是否叶子节点
}

parentId:解决数据嵌套的问题;

level:判断当前层级的缩进;

isLeaf:判断是否为叶子节点也就是没有子节点;

三、嵌套数据扁平化处理

1、通常我们接收到的数据都是嵌套结构的,但是对于menu组件来说必须处理嵌套的数据,使其扁平化才能更好的展示

2、我们将通过一个函数来解决,那么这个函数的参数应该包含:menu(嵌套结构的数据)、level(当前对象的层级)、parentNode(作为children的父节点是谁)

export function generateInnerMenu(
  menu: IMenuNode[],
  level = 0, // 节点层级
  parentNode = {} as IInnerMenuNode
): IInnerMenuNode[] {
  level++
  return menu.reduce((prev, cur) => {
    // 创建一个新节点
    const o = { ...cur } as IInnerMenuNode
    // 设置层级
    o.level = level
    // 如果层级比父节点层级高则是子级,设置父级parentId
    if (level > 1 && parentNode.level && level > parentNode.level) {
      o.parentId = parentNode.id
    }
    if (o.children) {
      // 如果存在children,则递归处理这些子节点
      const children = generateInnerMenu(o.children, level, o)
      // 处理完删除多余children属性
      delete o.children
      // 将新构造的节点o和已拍平数据拼接起来
      return prev.concat(o, children)
    } else {
      // 叶子节点的情况:
      // 如果是懒加载,isLeaf会被设置为false,则不需要设置
      // 如果没有初始化,则默认设置为true
      if (o.isLeaf === undefined) {
        o.isLeaf = true
      }
      // 将新构造的节点o和已拍平数据拼接起来
      return prev.concat(o)
    }
  }, [] as IInnerMenuNode[])
}

3、函数gennerateInnerMenu的函数体首先是level++,在将来每递归一次就代表这个菜单的层级多一;返回menu通过reduce聚合的新对象

4、reduce方法首先拿到此次循环的cur所有属性,将递增后的level赋值给此次循环的level,增加新的属性parentNode,告知父节点是谁

5、如果有children,递归生成新对象且要删除当前的children,通过concat拼接当前循环的对象和新对象(children);如果没有children直接拼接且增加isLeaf属性告知是叶子节点

四、节点缩进和折叠功能

1、缩进功能的实现依赖于level

const NODE_INDENT = 24
...
style={{
	paddingLeft: `${NODE_INDENT * (menuNode.value.level - 1)}px`
}}

2、展开和expended有关

3、折叠功能思路:点击某个节点,找到他的子节点加入到列表中,对拍平的数据进一步处理,一开始只显示level=1的数据,将其他数据隐藏

    const { data } = toRefs(props)
    const innerData = ref(generateInnerMenu(data.value)) // 用ref包着 不改变之前的结构
    const toggleNode = (node: IInnerMenuNode) => {
      // 在原始的列表中获取该节点
      const cur = innerData.value.find(item => item.id === node.id)
      if (cur) {
        cur.expanded = !cur.expanded
      }
    }
    // 获取那些展开的节点列表
    const expandedMenu = computed(() => {
      let excludeNodes: IInnerMenuNode[] = []
      const result = []
      // 循环列表,找出那些!expanded
      for (const item of innerData.value) {
        // 如果遍历节点,在排除列表中,直接跳出本次循环
        if (excludeNodes.includes(item)) {
          continue
        }
        // 当前节点处于折叠状态,他的子节点应该被排除
        if (item.expanded !== true) {
          excludeNodes = getChildren(item)
        }
        result.push(item)
      }
      return result
    })
    const getChildren = (node: IInnerMenuNode) => {
      let result = []
      // 找到node在列表中的索引
      const startIndex = innerData.value.findIndex(item => item.id === node.id)
      // 找到他后面所有的子节点(level比当前节点大)
      for (
        let i = startIndex + 1;
        i < innerData.value.length && node.level < innerData.value[i].level;
        i++
      ) {
        result.push(innerData.value[i])
      }
      return result
    }
    ...