这是我参与「 第五届青训营 」伴学笔记创作活动的第 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
}
...