最近在使用自己封装的 Tree 的时候, 发现只有点击复选框的时候, 全选 复选框可以联动, 通过 setCheckedNodes 、 setCheckedKeys 和 default-checked-keys 设置目前勾选的节点时, 无法联动, 需要再优化
如下图所示:

通过 node 设置后, 全选并没有显示为半选状态
方法一(推荐, 以后只维护这个)
源码在这里
需要用到 check-change 这个 Event
节点选中状态发生变化时的回调
共三个参数, 依次为: 传递给 data 属性的数组中该节点所对应的对象、节点本身是否被选中、节点的子树中是否有被选中的节点
需要在这个方法里判断全选的状态, 但发现这个逻辑有点麻烦: 首先你要对传入的三个参数都需要判断, 还需要和 allNodes 做判断.
对, 其实不需要判断传入的三个参数, 直接拿到 allNodes, 根据所有根节点的选中状态来处理全选的状态
但这个 Event 只要有节点变化就会触发, 像上图例子会调用好几次, 而且点击复选框的时候也会调用. 咱们在点击复选框的时候已经处理过逻辑了, 如果要使用 check-change, 那么 check 这个事件就不能要了
1 2 3 4 5 6 7 8 9 10
   | <el-tree   :ref="ref"   v-bind="$attrs"   :node-key="nodeKey"   :show-checkbox="showCheckbox"   v-on="$listeners"   @check-change="handleCheckChange" >   <slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot> </el-tree>
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
   | data() {   return {     ref: 'elTree',     isIndeterminate: false,     isCheckAll: false   } }, watch: {   defaultCheckedKeys: {     handler: 'handleCheckChange',     immediate: true   } }, methods: {   handleCheckChange() {     if (!this.showCheckAll || !this.showCheckbox) {       return     }          this.debounce(       this.$nextTick(() => {         this.handleCheckAllStatus()       }),       100     )   },      debounce(func, wait) {     var timeout
      return function() {       var context = this       var args = arguments
        clearTimeout(timeout)       timeout = setTimeout(function() {         func.apply(context, args)       }, wait)     }   },   handleCheckAllStatus() {     const elTreeStore = this.$refs[this.ref].store     const allNodes = elTreeStore       ._getAllNodes()       .filter(({ level, visible }) => level === 1 && visible)                    this.checkAll = allNodes.every(({ checked }) => checked)     this.isIndeterminate =       allNodes.some(({ indeterminate }) => indeterminate) ||       (allNodes.some(({ checked }) => checked) && !this.checkAll)   } }
  | 
 
方法二
源码在这里
处理一下传入 el-tree 的 data 数据, 加上一个 全选 的根节点
1 2 3 4 5 6 7 8 9 10
   | <el-tree   :ref="ref"   v-bind="$attrs"   :data="treeData"   :node-key="nodeKey"   :show-checkbox="showCheckbox"   v-on="$listeners" >   <slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot> </el-tree>
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
   | props: {   data: {     type: Array,     default() {       return []     }   } }, data() {   return {     treeData: [],     ref: 'elTree'   } }, mounted() {   this.treeData =     this.showCheckAll && this.showCheckbox       ? [           {             [this.$refs[this.ref].props.label]: '全选',             [this.$refs[this.ref].nodeKey || 'id']: 'rootId',              [this.$refs[this.ref].props.children]: this.data           }         ]       : this.data }
  | 
 
好了, 组件封装完了. 全选的状态由 el-tree 内部处理, 咱们什么也不用管, 完成!
才怪嘞!!! 这样虽然简单, 但毕竟修改数据了, 一旦涉及到修改数据, 就会有很多麻烦需要处理.
比如: getChecked 这些获取选中节点的方法有可能会返回咱们添加的全选的数据, 而且全选的 check 和 check-change 也会触发(不过这个影响不算太大, 可以根据实际需求灵活应变)
那就只能修改 getChecked 方法, 先拿到当前选中的数据, 过滤掉 全选 的数据, 不能使用 el-tree 的默认方法了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
   | data() {   return {     treeData: [],     ref: 'elTree',     checkAllId: '__rootId__'   } }, watch: {   data: {     handler: 'handleData',     immediate: true   } } computed: {   isCheckAll() {     return this.showCheckAll && this.showCheckbox   } }, methods: {   handleData() {     if (this.isCheckAll && this.data.length) {       this.treeData = [         {           [this.$refs[this.ref].props.label]: '全选',           [this.nodeKey]: this.checkAllId,           [this.$refs[this.ref].props.children]: this.data         }       ]     } else {       this.treeData = this.data     }   },   getCheckedNodes(leafOnly, includeHalfChecked) {     if (this.isCheckAll) {       return this.$refs[this.ref]         .getCheckedNodes(leafOnly, includeHalfChecked)         .filter(node => node[this.nodeKey] !== this.checkAllId)     }     return this.$refs[this.ref].getCheckedNodes(leafOnly, includeHalfChecked)   },   getHalfCheckedNodes() {     if (this.isCheckAll) {       return this.$refs[this.ref]         .getHalfCheckedNodes()         .filter(node => node[this.nodeKey] !== this.checkAllId)     }     return this.$refs[this.ref].getHalfCheckedNodes()   },   getCheckedKeys(leafOnly) {     if (this.isCheckAll) {       return this.$refs[this.ref].getCheckedKeys(leafOnly).filter(key => key !== this.checkAllId)     }     return this.$refs[this.ref].getCheckedKeys(leafOnly)   },   getHalfCheckedKeys() {     if (this.isCheckAll) {       return this.$refs[this.ref].getHalfCheckedKeys().filter(key => key !== this.checkAllId)     }     return this.$refs[this.ref].getHalfCheckedKeys()   } }, mounted() {      for (let key in this.$refs[this.ref]) {     if (!(key in this) && typeof this.$refs[this.ref][key] === 'function') {       this[key] = this.$refs[this.ref][key].bind(this.$refs[this.ref])     }   } }
  | 
 
文本溢出显示省略号
刚好最近封装了一个文本溢出显示省略号的组件, 可以拿来一用
1 2 3
   | <slot slot-scope="{ node, data }" v-bind="{ node, data }">   <text-ellipsis :content="node.label"></text-ellipsis> </slot>
  | 
 
单选时增加 disabled
el-tree 只对选择框处理了 disabled, 单选时却没有, 所以我们需要增加一下
disabled 有两种方式:
- 通过 
props 的 disabled 字段和在 data 中 disabled 对应字段来禁用 
- 通过 
props 的 disabled 方法来禁用 
我们保持这种方式不变, 通过源码可知 disabled 保存在 node 里, 所以我们只需要在这里取即可
我们只需要做两件事:
- 设置 
disabled 样式 
- 当点击 
disabled 节点后, 通过 setCurrentKey 来重置当前选中节点 
1 2 3 4 5 6 7 8 9 10
   | <el-tree   v-on="{ ...$listeners, 'current-change': handleCurrentChange, 'node-click': handleNodeClick }" >   <slot slot-scope="{ node, data }" v-bind="{ node, data }">     <text-ellipsis       :class="{ 'custom-disabled': node.disabled }"       :content="node.label"     ></text-ellipsis>   </slot> </el-tree>
   | 
 
这里使用 v-on="{ ...$listeners, 'current-change': handleCurrentChange, 'node-click': handleNodeClick }" 是防止 emit 两遍 current-change 和 node-click 方法
1 2 3 4
   | .custom-disabled {   color: #94969a;   cursor: not-allowed; }
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   |  handleCurrentChange(data, node) {   const { key, disabled } = node   if (disabled) {          this.$refs[this.ref].setCurrentKey(this.currentKey)     return   }   this.currentKey = key   this.$emit('current-change', data, node) }, handleNodeClick(data, node, self) {   const { disabled } = node   if (disabled) return   this.$emit('node-click', data, node, self) }
 
 
  | 
 
el-tree 节点还有右键事件和拖拽事件, 目前项目中没有用到, 就不处理 disabled 了, 如果有用到的话就需要处理一下
过滤时加载对应子节点
el-tree 不会返回过滤节点的子节点, 具体看这里: el-tree 节点过滤加载对应子节点 | Henry
所以我们需要自定义一下过滤方法
1 2 3 4
   | <el-tree   :filter-node-method="filterNodeMethod" > </el-tree>
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | props: {   filterNodeMethod: {     type: Function,     default(value, data, node) {       if (!value) return true       let parentNode = node.parent,         labels = [node.label],         level = 1       while (level < node.level) {         labels = [...labels, parentNode.label]         parentNode = parentNode.parent         level++       }       return labels.some(label => label.indexOf(value) !== -1)     }   } }, methods: {      filter(value) {     this.$refs[this.ref].filter(value)   } }
  | 
 
单选只能选择叶子节点
最近在工作中经常遇到只能单选叶子节点的功能, 所以我们就需要封装一下.
以前在 SelectTree 中做过这个功能, 但其实应该是 Tree 的功能, 所以就搬到这里啦
基于 ElementUI 封装的 SelectTree | Henry
由以前经验可知需要分两种情况:
- 使用 
el-tree 自带的 node.isLeaf 判断是否是叶子节点 
- 传入 
isLeafMethod 函数来自定义叶子节点 
而且处理逻辑与 disabled 类似, 就提取一个函数处理即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
   |  handleCurrentChange(data, node) {   const { key, disabled } = node   if (this.handleDisabled(disabled, data, node)) {     return   }   this.currentKey = key   this.$emit('current-change', data, node) }, handleNodeClick(data, node, self) {   const { disabled } = node   if (this.handleDisabled(disabled, data, node)) {     return   }   this.$emit('node-click', data, node, self) }, handleDisabled(disabled, data, node) {   if (     disabled ||     (this.isLeafMethod ? !this.isLeafMethod(data, node) : this.currentIsLeaf && !node.isLeaf)   ) {     this.$refs[this.ref].setCurrentKey(this.currentKey)     return true   } }
 
 
  | 
 
问题
方法一 data 为空数组, 全选仍存在(已解决)
由于全选标签只判断了 showCheckAll && showCheckbox, 所以没数据的时候也是会显示的, 所以还需要判断是否有数据: v-if="showCheckAll && showCheckbox && data.length"
方法一 data 改变后, 显示有问题(已解决)
目前 data 改变后, 没有重新处理全选的状态, 导致全选选中状态有问题.
所以需要 watch data; 而且发现 store._getAllNodes() 会保留历史数据: 比如第一次 data 就一条数据, _getAllNodes 获取的数据为一条, data 改变为两条数据后, _getAllNodes 获取的数据为三条
通过查看源码发现 nodesMap 保存所有数据, 只要注册节点并且该节点不在 nodesMap 中, 就添加, _getAllNodes 获取的就是这里的所有数据
所以 data 改变后, 需要重新加载 el-tree:
1 2 3 4 5 6
   | watch: {   data() {     this.key = Math.random()     this.handleCheckChange()   } }
  | 
 
这样修改后, 全选状态确实可以正常显示了, 但绑定的 el-tree 方法又出现问题了: 绑定方法只在 mounted 中绑定了一次, 通过修改 key 重新加载 el-tree 后, 绑定的方法仍然是重新加载前的, 而且重新绑定也不行; 有一些方法需要用到 store, data 改变后, store 没有更新, 导致 getCheckedNodes 等方法仍获取的是第一次的值, 所以需要重新加载一下 tree 组件
所以不能在封装组件中通过修改 key 来重新加载组件, 会导致绑定的方法有问题
目前暂时在父组件中使用 v-if 控制, 以后看看有没有好方法, 这样上面的代码也不用加了
对比 data 改变前后通过 _getAllNodes 获取的数据发现, 改变前选中的数据, 改变后 checked 仍为 true, 但通过 getCheckedKeys 获取的数据只有改变后选中的数据, 是不是可以查看源码看看这部分是如何实现的?
node_modules/element-ui/packages/tree/src/model/tree-store.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
   | getCheckedNodes(leafOnly = false, includeHalfChecked = false) {   const checkedNodes = [];   const traverse = function(node) {     const childNodes = node.root ? node.root.childNodes : node.childNodes;
      childNodes.forEach((child) => {       if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) {         checkedNodes.push(child.data);       }
        traverse(child);     });   };
    traverse(this);
    return checkedNodes; }
  getCheckedKeys(leafOnly = false) {   return this.getCheckedNodes(leafOnly).map((data) => (data || {})[this.key]); }
  | 
 
原来人家并没有用 _getAllNodes 获取全部数据, 而是通过 root 拿到所有根节点数据, 再依次遍历子节点来拿到所有选中的数据
那咱们也用 root 不就行了吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
   | watch: {   data: {     handler: 'handleData',     immediate: true,     deep: true   } }, methods: {   handleData() {     this.$nextTick(() => {       this.allNodes = this.getAllNodes(this.$refs[this.ref].root[childNodes])       this.allNodes.length &&         (this.maxLevel = Math.max.apply(           null,           this.allNodes.map(({ level }) => level)         ))       this.$emit('max-level', this.maxLevel)       this.handleCheckChange()       return Promise.resolve()     })   },   getAllNodes() {     let allNodes = []     const traverse = function(node) {       const childNodes = node.root ? node.root.childNodes : node.childNodes       childNodes.forEach(child => {         allNodes.push(child)         traverse(child)       })     }     traverse(this.$refs[this.ref])     return allNodes   } }
  | 
 
这次把 allNodes 缓存起来了, 避免每次都要获取一下, 由于是引用地址, 所以状态变化后会跟着改变的; 并且还 emit 出去了 max-level, 省的还要在通过 getTreeMaxLevel 获取
defaultExpandAll defaultExpandedKeys 无效(已解决)
由于目前在初始化的时候就执行了 expandToLevel 方法, 导致只展开到 level 级
而如果用户没有传入 level, 只传入 defaultExpandAll 或 defaultExpandedKeys, 默认 level 为 1, 那只能展开到一级
所以需要添加 isFirst 判断: 默认为 true, 调用 expandToLevel 时判断是否是第一次调用, 第一次调用的时候判断 defaultExpandAll 或 defaultExpandedKeys 是否有值, 有值的话就不执行后面的代码, 并且把 isFirst 置为 false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
   | props: {   defaultExpandAll: {     type: Boolean,     default: false   },   defaultExpandedKeys: {     type: Array,     default() {       return []     }   } }, data() {   return {     isFirst: true   } }, methods: {   expandToLevel(level) {     if (this.isFirst && (this.defaultExpandAll || this.defaultExpandedKeys.length)) {       this.isFirst = false       return     }     this.isFirst = false        } }
  |