写在前面
近期在做移动端项目,用的 UI 框架是 Vant,然后有个需求是只需要我这边渲染出一个部门列表,看了下 vant 好像没有可用的 Tree 组件,于是就自己封装了个
常见的 Tree 组件
确定基本功能
首先确定下 Tree 组件具有的基本功能:
- [ ] 递归组件显示子节点
- [ ] 支持 expand 展开功能
- [ ] 支持 select 点击当前节点返回当前节点信息功能
- [ ] 点击勾选时联动勾选功能
- [ ] 获取当前所有勾选的节点
- [ ] 支持异步拉取数据渲染
构建文件目录
新建一个 components 目录放我们的组件,Node.vue
,Tree.vue
,组件函数方法库util.js
|-- app.vue
|-- components
|-- tree.vue
|-- node.vue
|-- util.js
数据格式转换
通常来讲,后端传给前端的部门信息数据一般都不会是树形,都是要前端转换,所以我们要先把这些数据进行转换
假设我们拿到手的数据,有个parentId
和当前Id
,我们可以根据这两个字段来进行递归
当然啦,每个项目他的字段名都不一样,根据自己的项目来,如果叫babaId
和erziId
也没人管你,只要有对应关系就行。
1. 定义一个 formatTree 方法用来格式化数据,把它变成树形结构
function formatTree(arr){ //主函数
function findParents(arr){
// 查找根节点父级元素方法,有可能存在多个最外层的父级节点,先把他们找出来
}
function findChildren(parents){
// 递归查找每个parents父级节点对应的子孙节点
}
}
2. 编写 findParents 方法,找到所有最外层父级节点数据
我们先写下第一个函数方法,findParents
,找爸爸,爸爸的爸爸叫什么?
function findParents(arr){
// arr为原数组
//通过reduce方法把数组转换成对象,作为一个哈希表(说白了就是个对象)存储他们的parentId
const map = arr.reduce((obj,cur)=>{
let parentId = cur['parentId'] // 获取每一项的parentId
obj[parentId] = parentId // 把他的parentId作为key值
return obj
},{})
// 最后做一次筛选,找出最外层的父级节点数据
return arr.filter(item=>!map[item.id])
}
filter
那一步可能有点花里胡哨,map
存储的是每一个节点的id
所以我们只要用原数组arr
进行一次 filter
,把每一个项的parentId
都放进去 map
去匹配,如果找不到,说明他是最外层根节点的数据
3. 编写 findChildren 方法,找出父级节点的所有子节点
function findChildren(parents){
if(!parents.length) return
parents.forEach(p=>{
arr.forEach(item=>{
// 如果原数组arr里面的每一项中的parentId等于父级的某一个节点的id,则把它推进父级的children数组里面
if(p.id === item.parentId){
if(!p.children){
p.children = []
}
p.children.push(item)
}
})
// 最后进行一次递归,找儿子们的儿子们
findChildren(p.children)
})
}
值得注意的是,这里我是通过修改传进来的原数据 parents
,给他添加 children
属性
4.完整的代码
function formatTree(arr) {
// 有可能存在多个最外层的父级节点,先把他们找出来
function findParents(arr) {
// arr为原数组
//通过reduce方法把数组转换成对象,作为一个哈希表(说白了就是个对象)存储他们的id
const map = arr.reduce((obj, cur) => {
let id = cur['id'] // 获取每一项的id
obj[id] = id
return obj
}, {})
// 最后做一次筛选,找出最外层的父级节点数据
return arr.filter(item => !map[item.parentId])
}
let parents = findParents(arr) // 获取最外层父级节点
// 查找每个parents 对应的子孙节点,此处开始递归
function findChildren(parents) {
if (!parents) return
parents.forEach(p => {
arr.forEach(item => {
// 如果原数组arr里面的每一项中的parentId恒等于父级的某一个节点的id,则把它推进父级的children数组里面
if (p.id === item.parentId) {
if (!p.children) {
p.children = []
}
p.children.push(item)
}
})
// 最后进行一次递归,找儿子们的儿子们
findChildren(p.children)
})
}
findChildren(parents)
return parents
}
在app.vue
页面,虽然还没有写Tree
组件,但是可以先把他引进来,根据上述的功能清单,Tree
组件有几个属性:
属性名 | 作用 | 类型 | 默认值 |
---|---|---|---|
data | 可嵌套的节点属性的数组,生成 tree 的数据 | Array(详细数据格式参考 data 格式表) | [] |
showCheckBox | 是否显示多选框 | Boolean | false |
事件:
事件名 | 作用 | 返回值 |
---|---|---|
onChecked | 勾选时触发 tree 的数据 | 当前已选中的节点数据 |
onExpand | 点击展开时触发 | 当前已选中的节点数据 |
onSelect | 点击节点时触发 | 当前已选中的节点数据 |
另外需要注意的是,传进去
data
数组元素格式表:
属性 | 作用 | 类型 |
---|---|---|
title | 节点显示的文字内容 | String |
expand | 是否展开状态 | Boolean |
checked | 是否勾选状态 | Boolean |
children | 子节点内容 | Array |
我们定义一个假数据叫list
的数组,为了防止数据污染,先把他深拷贝一次,赋值给depData
,Tree
组件通过:data
进行接收
然后使用上面的formatTree
把它转换成树形结构数据,传入到Tree
组件里面
<!--app.vue-->
<template>
<div id="app">
<Tree
:data="depData"
:showCheckBox="true"
@onChecked="handleChecked"
@onExpand="handleExpand"
@onSelect="hanldeSelect"
/>
</div>
</template>
<script>
import Tree from './components/Tree'
export default {
components: {
Tree,
},
data() {
return {
depData: [],
list: [ // 定义初始数据
{
id: 1,
parentId: 0,
title: '公司',
},
{
id: 4,
parentId: 1,
title: '开发一部',
},
{
id: 2,
parentId: 1,
title: '开发二部',
},
{
id: 5,
parentId: 3,
title: '前端组',
},
{
id: 3,
parentId: 1,
title: '开发三部',
},
{
id: 6,
parentId: 3,
title: '后端组',
},
{
id: 7,
parentId: 4,
title: '爆破组',
},
{
id: 8,
parentId: 2,
title: '测试组',
},
{
id: 9,
parentId: 2,
title: '运维组',
},
{
id: 10,
parentId: 9,
title: '西岚',
},
{
id: 11,
parentId: 9,
title: '东岚',
},
{
id: 12,
parentId: 5,
title: '南岚',
},
],
}
},
methods: {
formatTree(arr) {
// 有可能存在多个最外层的父级节点,先把他们找出来
function findParents(arr) {
// arr为原数组
//通过reduce方法把数组转换成对象,作为一个哈希表(说白了就是个对象)存储他们的id
const map = arr.reduce((obj, cur) => {
let id = cur['id'] // 获取每一项的id
obj[id] = id
return obj
}, {})
// 最后做一次筛选,找出最外层的父级节点数据
return arr.filter((item) => !map[item.parentId])
}
let parents = findParents(arr) // 获取最外层父级节点
// 查找每个parents 对应的子孙节点,此处开始递归
function findChildren(parents) {
if (!parents) return
parents.forEach((p) => {
arr.forEach((item) => {
// 如果原数组arr里面的每一项中的parentId恒等于父级的某一个节点的id,则把它推进父级的children数组里面
if (p.id === item.parentId) {
if (!p.children) {
p.children = []
}
p.children.push(item)
}
})
// 最后进行一次递归,找儿子们的儿子们
findChildren(p.children)
})
}
findChildren(parents)
return parents
},
handleChecked(v) {
console.log(v)
// 定义handleChecked方法,当点击勾选的时候,接收Tree组件 $emit出来的值
},
handleExpand(v) {
console.log(v)
//当点击展开的时候,接收Tree组件 $emit出来的值
},
hanldeSelect(v) {
console.log(v)
//,当点击节点的时候,接收Tree组件 $emit出来的值
},
deepCopy(data) {
//深拷贝原数据
if (data == null) return // 如果为空则返回
let typeOf = (d) => {
return Object.prototype.toString.call(d)
}
let o = null
if (typeOf(data) === '[object Object]') {
o = {}
for (let k in data) {
o[k] = this.deepCopy(data[k])
}
} else if (typeOf(data) === '[object Array]') {
o = []
for (let i = 0; i < data.length; i++) {
o.push(this.deepCopy(data[i]))
}
} else {
return data
}
return o
},
},
computed: {
data() {
return this.formatTree(this.depData)
},
},
created() {
// 把每个数组都添加两个属性,一个展开expand一个checked勾选
this.depData = this.deepCopy(this.list).map((item) => {
item.checked = false
item.expand = true // 为了方便展示,这里先让他展开
return item
})
},
}
</script>
开发 Tree 组件
我这里用的 Vue2.6 版本, webpack4 搭建项目
1. 组件拆分
把这个 Tree 组件拆分两部分:
- 一部分是
Tree.vue
,负责获取数据, - 另一部分是
Node.vue
, 节点组件,负责递归渲染树形结构
首先开发Tree
组件,Tree
组件拿到的 props
是个格式化后的树形数组
所以我们先进行一次循环,把 每一个数组元素 传进去子组件Node
里面,这时候 Node
拿到的是个对象
Tree.vue
<!--Tree.vue-->
<template>
<div class="tree">
<div class="tree-node" v-for="item in data" :key="item.id">
<Node :data="item" :showCheckBox="showCheckBox" :loadData="loadData"/>
</div>
</div>
</template>
<script>
import Node from './Node';
export default {
name:"Tree",
components: {
Node
},
props: {
data: {
// 外部传入的数据
type: Array,
default() {
return [];
}
},
showCheckBox: {
// 配置项,是否显示checkbox
type: Boolean,
default: false
},
loadData: {
// 配置项,异步加载数据的回调方法,
type: Function
}
},
// 先定义几个方法,这些方法最终要执行的是把Node组件传过来的数据抛到调用Tree的组件里面
methods: {
handleCheck() {
// 定义勾选方法
},
handleSelect() {
// 定义点击节点方法
},
handleExpand() {
// 定义点击展开方法
},
getCheckedNodes() {
// 获取勾选的节点
},
getCheckedChildrenNodes() {
// 仅获取勾选的子节点
}
}
};
</script>
<style>
.tree-node {
font-size: 30px;
width: 90%;
margin: 0 auto;
}
</style>
Node.vue
下面就是重头戏也就是需要递 🐢 的 Node 节点,
<!--Node.vue-->
<template>
<ul class="tree-ul">
<!--tree的每一行-->
<li class="tree-li">
<van-checkbox class="checkbox" icon-size="18px" v-if="showCheckBox" :value="data.checked" @input="handleCheck"></van-checkbox>
<span>{{ data.title }}</span>
<span class="tree-expand" @click="handleExpand">
<!--展开箭头组件-->
<van-icon v-if="showArrow" :name="arrowType" />
</span>
<!--node组件递归-->
<Node v-show="data.expand" :showCheckBox="showCheckBox" :loadData="loadData" v-for="(item, index) in data.children" :key="index" :data="item" />
</li>
</ul>
</template>
<script>
export default {
name: 'Node', // 这个很关键,递归组件必须有name
props: {
data: {
type: Object,
default () {
return {}
}
},
showCheckBox: {
type: Boolean,
default: false,
},
loadData:{
type:Function
}
},
computed: {
showArrow() {
return this.data.children&&this.data.children.length
},
arrowType() {
//箭头方向,van组件提供的属性
return this.showArrow && this.data.expand ? 'arrow-down' : 'arrow';
}
},
methods: {
handleCheck() {}, //勾选方法
handleExpand() {},//展开的方法
handleSelect() {}, //点击列表
},
mounted(){
}
}
</script>
<style>
.tree-ul,
.tree-li {
font-size: 20px;
list-style: none;
margin-left: 10px;
position: relative;
height: auto;
}
.tree-ul {
margin: 0 auto;
box-sizing: border-box;
}
.tree-li {
position: relative;
width: 100%;
box-sizing: border-box;
margin: 6px 3px;
padding-right: 3px;
padding-left: 10px;
}
.tree-expand {
height: 20px;
cursor: pointer;
position: absolute;
top: 4px;
right: 0;
margin: auto;
}
.checkbox {
display: inline-block!important;
vertical-align: middle;
margin-right: 4px;
}
.tree-loading {
width: 20px;
height: 20px;
position: absolute;
top: 0;
right: 0;
margin: auto;
}
</style>
定义三个方法用来接收Tree
传过来的值,然后跑下我们的生成树形结构数据的代码,可以看到基本的 tree 组件已经基本跑起来了。
但是现在的 tree 没有点击展开和勾选等事件,这也是最核心的一部分内容,
目前我们的目录结构如下:
我们新增一个util.js
文件,用来存放我们的工具方法
2.完成 node 节点的点击,勾选,展开方法
首先我们先考虑下,无论是点击还是勾选等操作,都是在子节点上面进行,获取到的是值最终还是得把它抛到最外层组件,也就是调用 Tree 组件的app.vue
页面上,所以我们得先来过下 vue 里面组件传值的方法
传值方式 | 优点 | 缺点 |
---|---|---|
props 和this.$emit |
常用方式支持父子组件传值 | 不支持直接的兄弟组件传值 |
props 和.sync |
常用方式支持父子组件传值 | 不支持直接的兄弟组件传值 |
bus 总线机制 |
可以实现全局通讯 | 需要再new Vue 并且要把 bus 绑定在原型链,不适合单独的组件 |
provide 和inject |
子组件可以获取父组件数据,支持跨级获取 | 数据并非响应式,子组件向父组件传值并不是很友好 |
vuex |
最佳的全局通讯解决方案 | 需要引入第三方包,不适用于单独的组件 |
this.$children ,this.$ref |
支持获取父级或子组件的值 | 不支持跨级和兄弟组件传值 |
this.$attrs 和this.$listener |
支持获取父级或子组件的值,提高组件封装性 | 不支持跨级和兄弟组件传值 |
除此之外还有 vue 1.x 版本的时候提供的 api $dispatch
和 $broadcast
在子组件调用 dispatch 方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先通过 $on 监听了这个事件;
相反,在父组件调用 broadcast 方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先通过 $on 监听了这个事件。
看起来好像很好用,但是却被废除了,官方的解释是
因为基于组件树结构的事件流方式有时让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。
嗯…好吧,其实我参考了下其他 ui 框架的源码,他们差不多是自己实现了一个$dispatch
和$broadcast
,然后在 mixins 到组件里面进行使用
3.使用发布订阅模式实现EventBus完成组件传值
我这里的话,考虑到 tree 是个递归组件,因此自己使用了发布订阅模式实现了一个类似于 bus 机制的传值方式,但是这个只在我组件内部使用,不会污染到外层业务组件,
// util.js
/**
* 发布订阅模式,
* 消息发布中心
* @class BroadCast
*/
class BroadCast {
constructor() {
this.listMap = {}; // 存储监听者
}
emit(k, v) { // 发布消息函数,k为监听者的key,v为需要传值的value
this.listMap[k] &&
this.listMap[k].map((fn) => {
fn.call(null, v);
});
}
on(k, fn) { // 添加监听者,,k为监听者的key,fn为执行的回调函数
if (!this.listMap[k]) {
this.listMap[k] = [];
}
this.listMap[k].push(fn);
}
}
const broadCast = new BroadCast();
export default { // 这部分是要Mixins到组件里面
methods: { // 定义broadcast发布消息方法
broadCast(k, v) {
broadCast.emit(k, v);
},
on(k, fn) { // 定义接收消息方法
broadCast.on(k, fn);
}
}
};
使用的话就是通过mixins
api 把它混入到需要传值和接收值的组件里面,通过this.broadCast
传值,this.on
接收值
Node.vue
// Node.vue
// ... 省略<template>代码
import { broadCastMixins } from './util'; // 引入broadCast传值的方法
export default {
name: 'Node',
props: {
// ...省略props代码
},
mixins: [broadCastMixins],
computed: {
// ...省略computed代码
},
methods: {
handleCheck() { //勾选方法
this.$set(this.data, 'checked', !this.data.checked);
this.broadCast('handleCheck', this.data);
},
handleExpand() {//展开的方法
this.$set(this.data, 'expand', !this.data.expand);
this.broadCast('handleExpand', this.data);
},
handleSelect() {//点击节点方法
this.broadCast('handleSelect', this.data);
}
}
};
我们在 Node 组件引入util.js
里面的传值方法,当对节点进行操作的时候,把this.data
值传出去,第一个参数就是this.on
接收的 key 名,第二个参数就是需要传的值
另外这里我们并没有使用this.data.checked = !this.data.checked
这种方式,而采用了this.$set
,主要是前者设置值不是响应式,可能导致数据改变视图没有更新的情况
Tree.vue
// Tree.vue
// ... 省略<template>代码
import Node from './Node';
import { broadCastMixins } from './util'; // Tree组件也要引入传值方法,用来接收值
export default {
name:"Tree",
components: {
Node
},
mixins: [broadCastMixins],
props: {
// 隐藏props代码
},
// 这些方法最终要执行的是把Node组件传过来的数据抛到调用Tree的组件里面
methods: {
handleCheck(v) {
// 定义勾选方法
this.$emit('onChecked', v);
},
handleSelect(v) {
// 定义点击节点方法
this.$emit('onSelect', v);
},
handleExpand(v) {
// 定义点击展开方法
this.$emit('onExpand', v);
},
getCheckedNodes() {
// 获取勾选的节点
},
getCheckedChildrenNodes() {
// 仅获取勾选的子节点
}
},
// 在组件created的时候,定义接收方法,this.broadCast方法传过来的值在这里进行接收
created() {
this.on('handleCheck', this.handleCheck);
this.on('handleSelect', this.handleSelect);
this.on('handleExpand', this.handleExpand);
}
};
把this.on
方法定义在created
生命周期函数里面,第一个参数就是跟 Node 组件约定好的接收名 key,第二个参数就是执行的回调函数
值得注意的是,只能在 created 里面定义,因为如果是在mounted
里面定义的话,是子组件先渲染完,才到父组件渲染完毕,就无法接收到子组件传过来的值,详情可以去搜下父子组件生命周期
到这里点击勾选事件基本已经完成了,看下效果
看得出点击和展开功能都正常,目前我们已经实现的功能如下,还剩下三个功能,
- [x] 递归组件显示子节点
- [x] 支持 expand 展开功能
- [x] 支持 select 点击当前节点返回当前节点信息功能
- [x] 支持 checkbox 勾选功能
- [ ] 点击勾选时联动勾选功能
- [ ] 获取当前所有勾选的节点
- [ ] 支持异步拉取数据渲染
,目前勾选只是单个勾选,没有实现联动勾选,联动勾选可以说是 Tree 组件最麻烦的一部分内容了…
4.开发联动勾选功能
分两部分:
- 勾选一个节点他的子孙节点全部被选中
- 如果同级所有节点被选中,则他的父级就被选中,如果父级节点也全部选中,同样层层递归直到根节点
1.勾选一个节点他的子孙节点全部被选中
第一个相对来说比较简单,我们在Tree.vue
定义一个函数,
// Tree.vue
// ...省略template代码以及script代码
methods:{
//...省略其他methods代码
handleCheck(v) {
this.updateTreeDown(v, v.checked);
// 定义勾选方法
this.$emit('onChecked', v);
},
// node为当前勾选的节点,checked为否勾选的值
updateTreeDown(node, checked) {
this.$set(node, 'checked', checked); // 设置勾选状态
if (node['children']) { // 如果有子节点
node['children'].forEach((child) => { //则进行一次递归,让子节点也设置同样的勾选值
this.updateTreeDown(child, checked);
});
}
}
}
// ...省略css代码
当我们点击勾选的时候,通过之前定义的broadCast
传值方法,把勾选的节点信息传到Tree.vue
,当我们调用updateTreeDown
方法时,即可实现子节点勾选状态和当前节点node
同步
看下效果,还可以
2.如果同级所有节点被选中,则他的父级就被选中
这一步就比较麻烦,思考一下,当前节点勾选,则需要遍历同级所有节点的状态,如果全部都是勾选,则让它的父级节点也勾选,父级节点同理,层层递归直到跟节点。
因此我们要找到节点之间的对应关系,目前我们这个数据是有parentId
和id
,因为我们是通过后端传过来的数据进行生成的树形结构。
但是要考虑到某些情况,比如某些需求不需要跟后端交互,只用前端自己写一个静态数据,生成一棵树,那样的话,前端在自定义数据的时候,他就不一定会再每个节点写上一个 id
因此为了解决这个需求场景,我们还得先定义一个方法:
- 给每个节点带上一个唯一的
key
值 - 然后在生成一个对照表用来寻找他的
parentKey
在Tree.vue
定义一个树形数组拍平方法
//Tree.vue
//...省略template代码和其他script代码
data(){
return {
flatTreeMap:{}//存放树形数组拍平后生成的对象
}
},
props: {
data: {
// 外部传入的数据
type: Array,
default () {
return [];
}
},
//...省略其他props代码
},
watch: {
data: { // 当data发生发变化时,在进行一次flatTreeMap更新
deep: true,
handler() {
this.flatTreeMap = this.transferFlatTree();
}
}
},
methods:{
//...省略其他methods代码
transferFlatTree() {
let keyCount = 0; // 定义一个key
//treeArr为树形结构数组,parent为父级
function flat(treeArr, parent = '') {
//如果没有值,则返回空对象
if (!treeArr && !treeArr.length) return {};
// 因为我们要把数据格式化成一个哈希表结构,也就是对象,所以用了个reduce方便处理
return treeArr.reduce((obj, cur) => {
//cur就是数组当前的元素,是个对象,我们给他添加一个属性nodeKey,key的值为keyCount
let nodeKey = keyCount
cur.nodeKey = nodeKey;
//插入obj对象
obj[nodeKey] = {
nodeKey: keyCount,
parent,
node: cur // 把当前节点赋值给属性node
};
// 为了保证每一个key都是唯一,每次都要累加
keyCount++;
if (cur.children && cur.children.length) {
//如果有子节点 进行一次递归
obj = { ...obj,
...flat(cur.children, cur)
};
}
return obj;
}, {});
}
return flat(this.data); // 返回出格式化后的对象
},
},
created() {
//...省略其他created里面的代码
this.flatTreeMap= this.transferFlatTree(); // 执行拍平树形数据操作
},
//...省略css代码
如果把它打印出来,可以看得到我们已经把格式化成了想要的对象结构
另外,需要注意的是,因为我们是直接对
this.data
进行修改,添加新的属性,因此Node.vue
拿到的数据也是我们添加了nodeKey
后的新数据
点击下节点,看见已经被添加上了nodeKey
属性了
//Tree.vue
//...
methods:{
//...
transferFlatTree() {
//...
},
//向上递归勾选函数
updateTreeUp(nodeKey) {
// 获取该节点parent节点的nodeKey
const parentKey = this.flatTreeMap[nodeKey].parent.nodeKey;
//如果没有则返回,递归停止判断,如果没有父级节点则不继续递归
if (typeof parentKey == 'undefined') return;
// 获取当前nodeKey的节点数据
const node = this.flatTreeMap[nodeKey].node;
// 获取parent的节点数据
const parent = this.flatTreeMap[parentKey].node;
// 如果勾选状态一样则返回,不用做任何操作
if (node.checked == parent.checked) return;
// 否则,当子节点有勾选时,判断他的同级节点是不是都是勾选状态,如果是,则父级节点勾选
// 如果同级节点有些没有勾选,则返回falst,父级节点不勾选
if (node.checked == true) {
// 如果当前已勾选,则父几全部勾选
this.$set(
parent,
'checked',
parent['children'].every((node) => node.checked)
);
} else {
// 如果当前节点不勾选则父级节点不勾选
this.$set(parent, 'checked', false);
}
// 向上递归,直到根节点
this.updateTreeUp(parentKey);
},
handleCheck(v) {
this.updateTreeUp(v.nodeKey)
this.updateTreeDown(v, v.checked);
// 定义勾选方法
this.$emit('onChecked', v);
},
//...
我们定一个向上递归勾选的函数updateTreeUp
接收一个参数nodeKey
,也就是当前勾选节点的nodeKey
,这函数它主要有几个功能:
- 根据之前我们生成好的对照表
flatTreeMap
找到他的 node 节点 - 根据 node 节点找到 parent 节点
- 判断 node 节点的
checked
是否和 parent 节点的checked
一样,如果一样则不进行任何操作,如果不一样则判断同级节点的勾选情况,如果全部都是勾选,则 parent 节点checked
为 true,勾选。否则不勾选。 - 层层往上递归判断,直到根节点
最后在handleCheck
勾选函数里面执行updateTreeUp
,把当前节点的nodeKey
传进去,看下效果:
向上的联动勾选也实现了,另外还要实现一个跟勾选相关的功能,就是获取当前勾选的所有节点,供组件外部调用。
//Tree.vue
methods:{
//....
getCheckedNodes() {
// 获取勾选的节点
return Object.values(this.flatTreeMap).filter(
(item) => item.node.checked
);
},
getCheckedChildrenNodes() {
// 仅获取勾选的子节点
return Object.values(this.flatTreeMap).filter(
(item) => item.node.checked && !item.node.children
);
},
}
//...
很简单,只要我们把对照表里面的数据拿出来,筛选出checked
为 true 的数据就行,获取子节点的勾选数据在多加一个判断就是没有children
属性即可,可以试验下:
<!--app.vue-->
<template>
<div id="app">
<Tree
:data="data"
:showCheckBox="true"
@onChecked="handleChecked"
@onExpand="handleExpand"
@onSelect="hanldeSelect"
ref="tree"
/>
<button>获取checkedNodes</button>
<button>获取checkedChildNodes</button>
</div>
</template>
<script>
import Tree from './components/Tree';
export default {
//...
methods: {
//...
getCheckedNodes() { // 获取所有勾选节点
console.log(this.$refs.tree.getCheckedNodes());
},
getCheckedChildrenNodes() { //仅获取所有勾选的子节点
console.log(this.$refs.tree.getCheckedChildrenNodes());
},
},
components: {
Tree
}
};
</script>
<!--css代码...-->
看下功能清单,还剩下一个异步加载功能
- [x] 递归组件显示子节点
- [x] 支持 expand 展开功能
- [x] 支持 select 点击当前节点返回当前节点信息功能
- [x] 点击勾选时联动勾选功能
- [x] 获取当前所有勾选的节点
- [ ] 支持异步拉取数据渲染
5.异步加载功能
异步加载常见于树形结构数据不是一次性返回,而是分层级返回,点击父级节点就会请求接口拉取子节点数据。
嗯…改下数据结构先,因为真实开发项目时,后端返回的数据不可能只有这么点,像我们这边项目还会返回他下面有多少个部门和人数,那么我们可以添加一个属性叫depCount
显示该节点有多少个子部门。
data() {
return {
list: [
{
id: 1,
parentId: 0,
title: '公司',
depCount:11,
},
{
id: 4,
parentId: 1,
title: '开发一部',
depCount:1,
},
{
id: 2,
parentId: 1,
title: '开发二部',
depCount:4,
},
{
id: 5,
parentId: 3,
title: '前端组',
depCount:1,
},
{
id: 3,
parentId: 1,
title: '开发三部',
depCount:3,
},
{
id: 6,
parentId: 3,
title: '后端组',
depCount:0,
},
{
id: 7,
parentId: 4,
title: '爆破组',
depCount:0,
},
{
id: 8,
parentId: 2,
title: '测试组',
depCount:0,
},
{
id: 9,
parentId: 2,
title: '运维组',
depCount:2,
},
{
id: 10,
parentId: 9,
title: '西岚',
},
{
id: 11,
parentId: 9,
title: '东岚',
},
{
id: 12,
parentId: 5,
title: '南岚',
}
]
};
},
// ...
created() {
this.depData = this.deepCopy(this.list).map((item) => {
if(item.depCount!=null){ // 如果该节点有depCount则给他添加一个children属性
item.children = []
}
item.checked = false;
item.expand = true;
return item;
});
}
同时改下created
,给含有depCount
的数据加上children
属性
打开node.vue
添加一个loading
状态组件,同时在computed
里添加showLoading
状态
<template>
<ul class="tree-ul">
<!--tree的每一行-->
<li class="tree-li" @click.stop="handleSelect">
<van-checkbox class="checkbox" icon-size="18px" v-if="showCheckBox" :value="data.checked" @click.native.stop="handleCheck"></van-checkbox>
<span>{{ data.title }}{{data.key}}</span>
<span class="tree-expand" @click.stop="handleExpand">
<!--异步加载的时候展示loading的组件-->
<van-loading v-if="showLoading" class="tree-loading" color="#1989fa" />
<!--展开箭头组件-->
<van-icon v-if="showArrow" :name="arrowType" />
</span>
<!--node组件递归-->
<Node v-show="data.expand" :loadData="loadData" :showCheckBox="showCheckBox" v-for="(item, index) in data.children" :key="index" :data="item" />
</li>
</ul>
</template>
<script>
//...
export default{
computed: {
showArrow() {
// 1.如果数据没有children,说明是子组件,就不用展示下拉箭头
// 2.如果开启了异步加载,在loading的时候,不显示箭头
return (
(this.data.children &&
this.data.children.length &&
!this.showLoading) ||
(this.data.children &&
!this.data.children.length &&
this.loadData &&
!this.showLoading)
);
},
showLoading() {
// 判断是否有loading属性,并且判断你是否在开启状态
return 'loading' in this.data && this.data.loading;
}
}
//...
}
</script>
//...
好了,下面就可以开发异步加载方法了
在点击父级节点箭头的时候,才会去拉取异步数据来填充节点,因此这个功能是在handleExpand
里面进行的
同时拉到数据后就添加到节点的children
属性里面
最后在进行一次展开expand
操作,这么想思路就清晰了。
打开Node.vue
methods:{
//...
handleExpand() {
let node = this.data;
if (node.children && node.children.length === 0) {
if (this.loadData) {
this.$set(node, 'loading', true); // 显示loading
this.loadData(node, (arr, callback) => { // 这个loadData回调函数,由外部组件调用的时候传入arr和callback
if (arr.length) { // 如果外部传入的数组不为空
// 把arr作为当前父级节点的children
this.$set(node, 'children', arr);
this.$nextTick(() => {
//展开操作
this.handleExpand();
//执行外部传入的回调函数,成功的时候
callback('suc', node);
});
} else {
// 如果返回的是为空数组,则执行失败的回调
callback('fali')
}
//关闭loading
this.$set(node, 'loading', false);
});
}
return;
}
this.$set(this.data, 'expand', !this.data.expand);
this.broadCast('handleExpand', this.data);
},
}
最后我们在app.vue
定义下一个函数把它赋值给loadData
<template>
<div id="app">
<!--定义一个名为handleLoadData的函数-->
<Tree :data="data" :showCheckBox="true" @onChecked="handleChecked" @onExpand="handleExpand" @onSelect="hanldeSelect" :loadData="handleLoadData" ref="tree" />
<button @click="getCheckedNodes">获取checkedNodes</button>
<button @click="getCheckedChildrenNodes">获取checkedChildNodes</button>
</div>
</template>
<script>
//...
methods:{
// 一般这是要拉取后台数据,我们这里只能通过setTimeout来模拟
handleLoadData(node, callbacks) { //node为点前异步加载的父级节点,callbacks是回调函数,有两个参数,一个是arr,一个callback
//callback接收一个参数res,成功为suc,失败为fali
setTimeout(() => {
callback([{
id: new Date().getTime(),
parentId: node.id,
title:['北岚','测试狗','尤玉溪门徒','狂躁的韭菜','前端bb机'] [Math.floor(Math.random()*5)]
}], (res) => {
if(res=='suc') {
console.log('加载成功')
}
})
}, 500);
},
}
</script>
我这里为了方便演示,在app.vue
的created
里面执行deepCopy
的时候,修改了item.expand = false
看下效果,效果实现了:
但是有个问题,就是当我们是处于勾选状态的时候,新增加的节点并没有被勾选上,
所以还得对这部分进行处理,其实就是当我们监听到数据发生变化的时候,获取勾选的节点,遍历他们,在执行一次updateTreeDown
watch: {
data: {
deep: true,
handler() {
this.stateTree = this.data
this.flatTreeMap = this.transferFlatTree();
this.updateCheckedNodesChildren() // 执行遍历勾选节点操作
}
}
},
methods:{
//...
updateCheckedNodesChildren() {
// 获取勾选的节点,
const checkedNodes = this.getCheckedNodes();
checkedNodes.forEach((node) => {
// 勾选的节点的子节点也进行勾选
this.updateTreeDown(node.node, true);
});
},
},
created(){
//...
this.flatTreeMap = this.transferFlatTree();
this.updateCheckedNodesChildren()
}
在把测试数据改极端点,每次点击生成 1-10 条数据
打开app.vue
methods:{
//...
handleLoadData(node, callback) {
setTimeout(() => {
callback([...new Array(Math.floor(Math.random() * 10)+1).keys()].map(() => {
return {
id: new Date().getTime(),
parentId: node.id,
title: ['北岚', '测试狗', '尤玉溪门徒', '狂躁的韭菜', '前端bb机','前端狂魔','鱿鱼溪','普通的章鱼🐙','狂暴🌲人','鱿鱼🦑冬'][Math.floor(Math.random() * 9)],
children: []
}
}), (res) => {
if (res == 'suc') {
console.log('加载成功')
}
})
}, 500);
},
}
emmm…看下效果,没啥问题
完成基本的 Tree 组件
现在所有功能 都已经完成
- [x] 递归组件显示子节点
- [x] 支持 expand 展开功能
- [x] 支持 select 点击当前节点返回当前节点信息功能
- [x] 点击勾选时联动勾选功能
- [x] 获取当前所有勾选的节点
- [x] 支持异步拉取数据渲染
Tree 组件的插槽
嗯…最后,我们要完成一个插槽功能,起源是之前开会的时候,产品提了一句,后面有可能会改样式,就是说部门和用户的样式不一样,我寻思着,这不就是 parent 节点的样式和 node 节点不一样吗,如果我写死在组件里面的话,那后面要改只能跑到组件里面去修改了。
后面 灵鸡一动,灵机一动,用插槽不就行了吗,在外面自己自定义样式,不用修改组件的东西
但是有个问题,因为是递归组件,而且插槽在模板语法里面不能直接作为 props 传过去,
根据我的研究,有两种方法可以完成这个需求:
-
使用 render 函数+jsx 语法,把
this.$scopedSlots.default
传进去,这里有个前提,就是如果是 vuecli2.x 或者是自己起的脚手架的话,必须打一个babel-plugin-transform-vue-jsx
包,如果是 vue-cli3.x 的话,可以直接使用 jsx 语法 -
貌似可以通过使用
this.$parent
层层网上找到Tree
节点的插槽,然后渲染在 node 页面,但是我试了一波,发现并不好用
所以,我这里主要是讲第一种。
…其实就把模板语法改成 render 函数,因为我这里是没给他命名,所以默认插槽名是default
以下是完整代码
Tree.vue
//tree.vue
<script>
import Node from './Node';
import {
broadCastMixins
} from './util';
export default {
name:"Tree",
render() {
let data = this.stateTree;
let showCheckBox = this.showCheckBox;
let loadData = this.loadData
return (
<div class={'tree'}>
{data && data.length
? data.map((item, index) => {
return (
<Node
class={'tree-node'}
key={index}
data={item}
showCheckBox={showCheckBox}
loadData={loadData}
>
{/*这里是插槽*/}
{this.$scopedSlots.default}
</Node>
);
})
: ''}
</div>
);
},
components: {
Node
},
mixins: [broadCastMixins],
data() {
return {
stateTree: this.data,
flatTreeMap: {}
};
},
props: {
data: {
// 外部传入的数据
type: Array,
default () {
return [];
}
},
showCheckBox: {
// 配置项,是否显示checkbox
type: Boolean,
default: false
},
loadData: {
// 配置项,异步加载数据的回调方法,
type: Function
}
},
watch: {
data: {
deep: true,
handler() {
this.stateTree = this.data
this.flatTreeMap = this.transferFlatTree();
this.updateCheckedNodesChildren()
}
}
},
methods: {
transferFlatTree() {
let keyCount = 0; // 定义一个key
//treeArr为树形结构数组,parent为父级
function flat(treeArr, parent = '') {
//如果没有值,则返回空对象
if (!treeArr && !treeArr.length) return {};
// 因为我们要把数据格式化成一个哈希表结构,也就是对象,所以用了个reduce方便处理
return treeArr.reduce((obj, cur) => {
//cur就是数组当前的元素,是个对象,我们给他添加一个属性nodeKey,key的值为keyCount
let nodeKey = keyCount;
cur.nodeKey = nodeKey;
//插入obj对象
obj[nodeKey] = {
nodeKey: keyCount,
parent,
// ...cur
node: cur // 把当前节点赋值给属性node
};
// 为了保证每一个key都是唯一,每次都要累加
keyCount++;
if (cur.children && cur.children.length) {
//如果有子节点 进行一次递归
obj = { ...obj,
...flat(cur.children, cur)
};
}
return obj;
}, {});
}
return flat(this.stateTree); // 返回出格式化后的对象
},
updateCheckedNodesChildren() {
// 获取勾选的节点,
const checkedNodes = this.getCheckedNodes();
checkedNodes.forEach((node) => {
// 勾选的节点的子节点也进行勾选
this.updateTreeDown(node.node, true);
});
},
updateTreeUp(nodeKey) {
// 获取该节点parent节点的nodeKey
const parentKey = this.flatTreeMap[nodeKey].parent.nodeKey;
//如果没有则返回,递归停止判断,如果没有父级节点则不继续递归
if (typeof parentKey == 'undefined') return;
// 获取当前nodeKey的节点数据
const node = this.flatTreeMap[nodeKey].node
// 获取parent的节点数据
const parent = this.flatTreeMap[parentKey].node
// 如果勾选状态一样则返回,不用做任何操作
if (node.checked == parent.checked) return;
// 否则,当子节点有勾选时,判断他的同级节点是不是都是勾选状态,如果是,则父级节点勾选
// 如果同级节点有些没有勾选,则返回falst,父级节点不勾选
if (node.checked == true) {
// 如果当前已勾选,则父几全部勾选
this.$set(
parent,
'checked',
parent['children'].every((node) => node.checked)
);
} else {
// 如果当前节点不勾选则父级节点不勾选
this.$set(parent, 'checked', false);
}
// 向上递归,直到根节点
this.updateTreeUp(parentKey);
},
handleCheck(v) {
if (!this.flatTreeMap[v.nodeKey]) return;
const node = this.flatTreeMap[v.nodeKey]
this.$set(node, 'checked', v.checked);
this.updateTreeUp(v.nodeKey);
this.updateTreeDown(v, v.checked);
// 定义勾选方法
this.$emit('onChecked', v);
},
handleSelect(v) {
// 定义点击节点方法
this.$emit('onSelect', v);
},
handleExpand(v) {
// 定义点击展开方法
this.$emit('onExpand', v);
},
getCheckedNodes() {
// 获取勾选的节点
return Object.values(this.flatTreeMap).filter(
(item) => item.node.checked
);
},
getCheckedChildrenNodes() {
// 仅获取勾选的子节点
return Object.values(this.flatTreeMap).filter(
(item) => item.node.checked && !item.node.children
);
},
// node为当前勾选的节点,checked为否勾选的值
updateTreeDown(node, checked) {
this.$set(node, 'checked', checked); // 先设置它的勾选状态
if (node['children']) {
// 如果有子节点
node['children'].forEach((child) => {
//则进行一次递归,让子节点也设置同样的勾选值
this.updateTreeDown(child, checked);
});
}
}
},
created() {
this.on('handleCheck', this.handleCheck);
this.on('handleSelect', this.handleSelect);
this.on('handleExpand', this.handleExpand);
this.flatTreeMap = this.transferFlatTree();
this.updateCheckedNodesChildren()
}
};
</script>
<style>
.tree-node {
font-size: 30px;
width: 90%;
margin: 0 auto;
}
</style>
Node.vue
<script>
import { broadCastMixins } from './util';
export default {
name: 'Node', // 这个很关键,递归组件必须有name
render() {
let showCheckBox = this.showCheckBox;
let data = this.data;
let loadData = this.loadData
return (
<div>
<ul class={'tree-ul'}>
<li class={'tree-li'} onClick={(e) => this.handleSelect(e)}>
{showCheckBox && (
<van-checkbox
class={'checkbox'}
icon-size={'18px'}
value={data.checked}
onClick={(e) => this.handleCheck(e, data)}
/>
)}
{/*如果没有插槽则默认使用显示内容*/}
{this.$scopedSlots.default ? (
this.$scopedSlots.default({
data: data
})
) : (
<span>{data.title}</span>
)}
<span class={'tree-expand'}>
{this.showLoading ? (
<van-loading class={'tree-loading'} color={'#1989fa'} />
) : (
''
)}
{this.showArrow ? (
<van-icon
name={this.arrowType}
onClick={(e) => this.handleExpand(e)}
/>
) : (
''
)}
</span>
{data.expand &&
data.children.map((item, index) => {
return (
<Node key={index} data={item} showCheckBox={showCheckBox} loadData={loadData}>
{this.$scopedSlots.default}
</Node>
);
})}
</li>
</ul>
</div>
);
},
props: {
data: {
type: Object,
default() {
return {};
}
},
showCheckBox: {
type: Boolean,
default: false
},
loadData: {
type: Function
}
},
mixins: [broadCastMixins],
computed: {
showArrow() {
// 1.如果数据没有children,说明是子组件,就不用展示下拉箭头
// 2.如果开启了异步加载,在loading的时候,不显示箭头
return (
(this.data.children &&
this.data.children.length &&
!this.showLoading) ||
(this.data.children &&
!this.data.children.length &&
this.loadData &&
!this.showLoading)
);
},
showLoading() {
// 判断是否有loading属性,并且判断你是否在开启状态
return 'loading' in this.data && this.data.loading;
},
arrowType() {
//箭头方向,van组件提供的属性
return this.showArrow && this.data.expand ? 'arrow-down' : 'arrow';
}
},
methods: {
handleCheck(e) {
e.cancelBubble = true;
this.$set(this.data, 'checked', !this.data.checked);
this.broadCast('handleCheck', this.data);
}, //勾选方法
handleExpand(e) {
e.cancelBubble = true;
let node = this.data;
if (node.children && node.children.length === 0) {
if (this.loadData) {
this.$set(node, 'loading', true); // 显示loading
this.loadData(node, (arr, callback) => {
// 这个loadData回调函数,由外部组件调用的时候传入arr和callback
if (arr.length) {
// 如果外部传入的数组不为空
// 把arr作为当前父级节点的children
this.$set(node, 'children', arr);
this.$nextTick(() => {
//展开操作
this.handleExpand(e);
//执行外部传入的回调函数,成功的时候
callback('suc', node);
});
} else {
// 如果返回的是为空数组,则执行失败的回调
callback('falid');
}
this.$set(node, 'loading', false); //关闭loading
});
}
return;
}
this.$set(this.data, 'expand', !this.data.expand);
this.broadCast('handleExpand', this.data);
},
handleSelect(e) {
e.cancelBubble = true;
this.broadCast('handleSelect', this.data);
} //点击列表
},
};
</script>
<style>
.tree-ul,
.tree-li {
font-size: 20px;
list-style: none;
margin-left: 10px;
position: relative;
height: auto;
}
.tree-ul {
margin: 15px auto;
box-sizing: border-box;
}
.tree-li {
position: relative;
width: 100%;
box-sizing: border-box;
margin: 6px 3px;
padding-right: 3px;
padding-left: 10px;
}
.tree-expand {
height: 20px;
cursor: pointer;
position: absolute;
top: 4px;
right: 0;
margin: auto;
}
.checkbox {
display: inline-block !important;
vertical-align: middle;
margin-right: 4px;
}
.tree-loading {
width: 20px;
height: 20px;
position: absolute;
top: 0;
right: 0;
margin: auto;
}
</style>
用法的话也是一样,在app.vue
里面使用插槽,
另外的话,我们可以根据 slotProps 传过来的值,也就是每一个节点的 data,根据他做判断是父级节点,还是子节点,像我们这里是根据他的depCount
来区分
因此就可以自定义不同类型节点的样式的,我这里为了演示就用了一个AddrItem
的组件用来接收子组件的内容作展示
app.vue
<template>
<div id="app">
<Tree
:data="data"
:showCheckBox="true"
@onChecked="handleChecked"
@onExpand="handleExpand"
@onSelect="hanldeSelect"
:loadData="handleLoadData"
ref="tree"
>
<template v-slot="slotProps">
<span v-if="slotProps.data.depCount >= 0"
>{{ slotProps.data.title }}
</span>
<AddrItem
v-else
class="addrItem"
:key="slotProps.data.id"
:data="slotProps.data"
sim
/>
</template>
</Tree>
<div class="flex">
<van-button type="primary" @click="getCheckedNodes">获取checkedNodes</van-button>
<van-button type="primary" @click="getCheckedChildrenNodes">获取checkedChildNodes</van-button>
</div>
</div>
</template>
<script>
import Tree from './components/Tree';
import AddrItem from './AddrItem';
export default {
components: {
AddrItem,
Tree
},
data() {
return {
list: [
{
id: 1,
parentId: 0,
title: '公司',
depCount: 11
},
{
id: 4,
parentId: 1,
title: '开发一部',
depCount: 1
},
{
id: 2,
parentId: 1,
title: '开发二部',
depCount: 4
},
{
id: 5,
parentId: 3,
title: '前端组',
depCount: 1
},
{
id: 3,
parentId: 1,
title: '开发三部',
depCount: 3
},
{
id: 6,
parentId: 3,
title: '后端组',
depCount: 0
},
{
id: 7,
parentId: 4,
title: '爆破组',
depCount: 0
},
{
id: 8,
parentId: 2,
title: '测试组',
depCount: 0
},
{
id: 9,
parentId: 2,
title: '运维组',
depCount: 2
},
{
id: 10,
parentId: 9,
title: '西岚'
},
{
id: 11,
parentId: 9,
title: '东岚'
},
{
id: 12,
parentId: 5,
title: '南岚'
}
]
};
},
methods: {
getCheckedNodes() {
console.log(this.$refs.tree.getCheckedNodes());
},
getCheckedChildrenNodes() {
console.log(this.$refs.tree.getCheckedChildrenNodes());
},
formatTree(arr) {
console.log(arr);
// 有可能存在多个最外层的父级节点,先把他们找出来
function findParents(arr) {
// arr为原数组
//通过reduce方法把数组转换成对象,作为一个哈希表(说白了就是个对象)存储他们的id
const map = arr.reduce((obj, cur) => {
let id = cur['id']; // 获取每一项的id
obj[id] = id;
return obj;
}, {});
// 最后做一次筛选,找出最外层的父级节点数据
return arr.filter((item) => !map[item.parentId]);
}
let parents = findParents(arr); // 获取最外层父级节点
// 查找每个parents 对应的子孙节点,此处开始递归
function findChildren(parents) {
if (!parents) return;
parents.forEach((p) => {
arr.forEach((item) => {
// 如果原数组arr里面的每一项中的parentId恒等于父级的某一个节点的id,则把它推进父级的children数组里面
if (p.id === item.parentId) {
if (!p.children) {
p.children = [];
}
p.children.push(item);
}
});
// 最后进行一次递归,找儿子们的儿子们
findChildren(p.children);
});
}
findChildren(parents);
return parents;
},
handleChecked(v) {
console.log(v, 'checked');
},
handleExpand(v) {
console.log(v, 'expand');
},
hanldeSelect(v) {
console.log(v, 'select');
},
deepCopy(data) {
if (data == null) return;
let typeOf = (d) => {
return Object.prototype.toString.call(d);
};
let o = null;
if (typeOf(data) === '[object Object]') {
o = {};
for (let k in data) {
o[k] = this.deepCopy(data[k]);
}
} else if (typeOf(data) === '[object Array]') {
o = [];
for (let i = 0; i < data.length; i++) {
o.push(this.deepCopy(data[i]));
}
} else {
return data;
}
return o;
},
handleLoadData(node, callback) {
setTimeout(() => {
callback(
[...new Array(Math.floor(Math.random() * 10) + 1).keys()].map(() => {
return {
id: new Date().getTime(),
parentId: node.id,
title: [
'北岚',
'测试狗',
'尤玉溪门徒',
'狂躁的韭菜',
'前端bb机',
'前端狂魔',
'鱿鱼溪',
'普通的章鱼🐙',
'狂暴🌲人',
'鱿鱼🦑冬'
][Math.floor(Math.random() * 9)],
children: [],
depCount: Math.floor(Math.random() * 10) + 1
};
}),
(res) => {
if (res == 'suc') {
console.log('加载成功');
}
}
);
}, 500);
}
},
computed: {
data() {
return this.formatTree(this.depData);
}
},
created() {
this.depData = this.deepCopy(this.list).map((item) => {
if (item.depCount != null) {
item.children = [];
}
item.checked = false;
item.expand = false;
return item;
});
}
};
</script>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
#app {
position: relative;
width: 100%;
height: 100%;
padding-top: 100px;
}
.flex{
display: flex;
justify-content: space-around;
}
</style>
最后最后在看下整体效果,,
除了样式丑点之外都可以的,,
功能总览:
属性名 | 作用 | 类型 | 默认值 |
---|---|---|---|
data | 可嵌套的节点属性的数组,生成 tree 的数据 | Array | [] |
showCheckBox | 是否显示多选框 | Boolean | false |
事件:
事件名 | 作用 | 返回值 |
---|---|---|
onChecked | 勾选时触发 tree 的数据 | 当前已选中的节点数据 |
onExpand | 点击展开时触发 | 当前已展开的节点数据 |
onSelect | 点击节点时触发 | 当前点击的节点数据 |
methods
方法名 | 作用 |
---|---|
getCheckedNodes | 获取所有勾选的节点数据 |
getCheckedChildrenNodes | 仅获取所有勾选的子节点数据 |
话说文章这么长估计没人看到这里了吧…
写在最后
代码已经上传仓库:github
参考资料:iview 源码 , 《Vue.js 组件精讲》
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 西岚 原文链接:https://juejin.im/post/6860396460987875335