功能介绍
基于 Vue.js 的递归组件知识,来开发一个不常见的树形table控件。
本节要实现的 Tree 组件具有以下功能:
- 节点可以无限延伸(递归)
- 节点可以选中,选中父节点,它的所有子节点也全部被选中,同样,反选父节点,其所有子节点也取消选择;
- 节点的半选状态
- 同一级所有子节点选中时,它的父级也自动选中,一直递归判断到根节点。
- 父级节点支持展开收缩。
API
table-Tree 是典型的数据驱动型组件,所以节点的配置就是一个 data,里面描述了所有节点的信息,数据如下:
{
"name": "权限管理",
"id": 19,
"children": [
{
"id": 58,
"name": "新增角色",
}
]
}
每个节点的配置(props:data)描述如下:
- name:节点标题
- checked:是否选中该节点。开启后,该节点的 Checkbox 将选中;
- children:子节点属性数组
- expand:节点展开收缩
- indeterminate:节点半选
入口 treeTable.vue
在 src/components
中新建目录 tree
,并在 tree 下创建两个组件 treeTable.vue
和 subTable.vue
。treeTable.vue 是组件的入口,用于接收和处理数据,并将数据传递给 subTable.vue;subTable.vue 就是一个递归组件,它构成了每一个节点,一个多选框、节点标题以及递归的下一级节点。
tree.vue 主要做的事情:
- 克隆数据 (JSON.parse(JSON.stringify()))
- 加入showRow属性终止递归
传统的tree组件递归终止是根据children
,如果一个节点没有 children 字段,那它就是最后一个节点
这里面情况不同,我们递归的最后一级始终是这个
这就要去给元数据加
shouRow
属性
// 递归的终止条件 用于判断当前项的子级的子级是否存在,存在为true,继续递归,不存在就终止
methods: {
// 加入showRow属性,用于递归组件
showRow (list) {
list.forEach(item => {
item.showRow = this.hasThreeChild(item.children)
if (item.children && item.children.length !== 0) {
this.showRow(item.children);
}
})
},
hasThreeChild (list) {
return list.some(item => item.children.length !== 0)
},
}
递归组件 subTable.vue
subTable.vue 是组件 treeTable 的核心,而一个 subTable 节点包含 4 个部分:
- 半选
- 全选
- 节点标题
- 递归子节点
先看subTable结构
<template>
<div class="permission-subTable">
<div v-if="!menu.showRow" class="content-item">
<div class="cell">
<span v-if="menu.children && menu.children.length" class="table-expand" @click="handleExpand">
<i v-if="menu.expand" class="el-icon-minus table-expand" />
<i v-else class="el-icon-plus table-expand" />
</span>
<el-tooltip effect="dark" :content="menu.name " placement="right">
<el-checkbox v-model="menu.checked" class="hasEllipsis" :indeterminate="menu.indeterminate" @change="(checked) => handleCheck(checked, menu)">
{{ menu.name }}
</el-checkbox>
</el-tooltip>
</div>
<div v-show="menu.expand" class="last-content">
<template v-if="menu.children && menu.children.length">
<div v-for="child in menu.children" :key="child.id" class="last-child-item">
<el-checkbox v-model="child.checked" @change="(checked) => handleCheck(checked, child)">
{{ child.name }}
</el-checkbox>
</div>
</template>
</div>
<div v-show="!menu.expand" style="flex:1;border: 0.5px solid #dcdfe6;" />
</div>
<div v-else class="table">
<div class="title">
<el-tooltip effect="dark" :content="menu.name " placement="right">
<el-checkbox v-model="menu.checked" class="hasEllipsis" :indeterminate="menu.indeterminate" @change="(checked) => handleCheck(checked, menu)">
{{ menu.name }}
</el-checkbox>
</el-tooltip>
</div>
<div class="content">
<SubTable v-for="item in menu.children" :key="item.id" :menu="item" />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SubTable',
props: {
menu: {
type: Object,
default: () => ({}),
},
},
data () {
return {
};
},
watch: {
'menu.children': {
handler (data) {
if (data) {
if (!data.length) return;
const checkedAll = !data.some(item => !item.checked);
const menu = this.menu;
this.$set(menu, 'checked', checkedAll);
const isIndeterminate = data.filter(item => (item.indeterminate));
if (isIndeterminate.length) {
this.$set(menu, 'indeterminate', true);
} else {
const checkChild = data.filter(item => (item.checked));
const indeterminate = checkChild.length < data.length && checkChild.length > 0;
this.$set(menu, 'indeterminate', indeterminate);
}
}
},
deep: true,
immediate: true,
},
},
mounted () {
},
methods: {
handleExpand () {
this.$set(this.menu, 'expand', !this.menu.expand);
},
handleCheck (checked, item) {
this.checkChild(checked, item);
},
checkChild (checked, data) {
if (data.children && data.children.length) {
data.children.forEach(item => {
this.checkChild(checked, item);
});
} else {
data.checked = checked;
}
},
}
};
</script>
<style scoped lang="scss">
.table {
display: flex;
overflow: auto;
}
.title {
width: 158px;
display: flex;
align-items: center;
border: 0.5px solid #dcdfe6;
padding: 0 4px;
}
.content {
flex: 1;
}
.item-content {
display: flex;
}
.cell {
width: 158px;
border: 0.5px solid #dcdfe6;
display: flex;
align-items: center;
justify-content: left;
padding: 0 4px;
.table-expand {
cursor: pointer;
margin-right: 2px;
}
}
.last-content {
display: flex;
flex-wrap: wrap;
flex: 1;
border: 0.5px solid #dcdfe6;
max-height: 200px;
overflow: auto;
padding: 0 12px;
}
.last-child-item {
flex: 40%;
}
.content-item {
display: flex;
}
</style>
<style lang="scss">
.permission-subTable {
.hasEllipsis {
.el-checkbox__label {
width: 120px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: middle;
}
}
}
</style>
updateTreeDown 只是向下修改了所有的数据,因为当前节点的数据里,是包含其所有子节点数据的,通过递归遍历可以轻松修改
第 2 个逻辑相对复杂,
一个节点,除了手动选中(或反选),还有就是第 2 种逻辑的被动选中(或反选),也就是说,如果这个节点的所有直属子节点(就是它的第一级子节点)都选中(或反选)时,这个节点就自动被选中(或反选),递归地,可以一级一级响应上去。有了这个思路,我们就可以通过 watch
来监听当前节点的子节点是否都选中,进而修改当前的 checked
字段:
watch: {
'menu.children': {
handler (data) {
if (data) {
if (!data.length) return;
const checkedAll = !data.some(item => !item.checked);
const menu = this.menu;
this.$set(menu, 'checked', checkedAll);
const isIndeterminate = data.filter(item => (item.indeterminate));
if (isIndeterminate.length) {
this.$set(menu, 'indeterminate', true);
} else {
const checkChild = data.filter(item => (item.checked));
const indeterminate = checkChild.length < data.length && checkChild.length > 0;
this.$set(menu, 'indeterminate', indeterminate);
}
}
},
deep: true,
immediate: true,
},
},
在 watch 中,监听了 data.children 的改变,并且是深度监听的。这段代码的意思是,当 data.children
中的数据的某个字段发生变化时(这里当然是指 checked 字段),也就是说它的某个子节点被选中(或反选)了,这时执行绑定的句柄 handler 中的逻辑。const checkedAll = !data.some(item => !item.checked);
也是一个巧妙的缩写,checkedAll 最终返回结果就是当前子节点是否都被选中了。
这里非常巧妙地利用了递归的特性,因为 SubTable.vue 是一个递归组件,那每一个组件里都会有 watch 监听 menu.children
,要知道,当前的节点有两个”身份“,它既是下属节点的父节点,同时也是上级节点的子节点,它作为下属节点的父节点被修改的同时,也会触发上级节点中的 watch 监听函数。这就是递归。
注意
文章参考于树形组件-tree 只知道他的github地址 所以贴一下
功能是如何去写传统的树形结构