Henry Henry
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)

Henry

小学生中的前端大佬
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Vue

  • ElementUI

    • 基于 ElementUI 封装的 TextEllipsis
    • 基于 ElementUI 封装的 Tree
    • 基于 ElementUI 封装的 Tree2
      • 方法一(推荐, 以后只维护这个)
      • 方法二
      • 文本溢出显示省略号
      • 单选时增加 disabled
      • 过滤时加载对应子节点
      • 单选只能选择叶子节点
      • 问题
        • 方法一 data 为空数组, 全选仍存在(已解决)
        • 方法一 data 改变后, 显示有问题(已解决)
        • defaultExpandAll defaultExpandedKeys 无效(已解决)
    • 基于 ElementUI 封装的 SelectTree
    • 基于 ElementUI 封装的 NumberInput
    • 基于 ElementUI 封装的基础 table 和 form
    • 基于 ElementUI 封装的 TreeTable
    • 基于 VuePress 搭建一个类似 ElementUI 的说明文档
    • el-tree 节点过滤加载对应子节点
    • 简单调试 node_modules 源码
    • ElementUI 问题集合
    • 树形表格更新后保持折叠状态
    • 开始和完成时间互相限制
  • React

  • AntD

  • 前端
  • ElementUI
Henry
2020-01-31
目录

基于 ElementUI 封装的 Tree2

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

如下图所示:

setCheckedNodes

通过 node 设置后, 全选并没有显示为半选状态

# 方法一(推荐, 以后只维护这个)

源码 (opens new window)在这里

需要用到 check-change 这个 Event

节点选中状态发生变化时的回调

共三个参数, 依次为: 传递给 data 属性的数组中该节点所对应的对象、节点本身是否被选中、节点的子树中是否有被选中的节点

需要在这个方法里判断全选的状态, 但发现这个逻辑有点麻烦: 首先你要对传入的三个参数都需要判断, 还需要和 allNodes 做判断.

对, 其实不需要判断传入的三个参数, 直接拿到 allNodes, 根据所有根节点的选中状态来处理全选的状态

但这个 Event 只要有节点变化就会触发, 像上图例子会调用好几次, 而且点击复选框的时候也会调用. 咱们在点击复选框的时候已经处理过逻辑了, 如果要使用 check-change, 那么 check 这个事件就不能要了

<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
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)
    // 关于 filter 的说明:
    // 全选的状态其实只和根节点的状态有关, 而且也处理了 set 方法中 leafOnly 为 true 的情况
    // visible 结合过滤使用
    this.checkAll = allNodes.every(({ checked }) => checked)
    this.isIndeterminate =
      allNodes.some(({ indeterminate }) => indeterminate) ||
      (allNodes.some(({ checked }) => checked) && !this.checkAll)
  }
}
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

# 方法二

源码 (opens new window)在这里

处理一下传入 el-tree 的 data 数据, 加上一个 全选 的根节点

<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
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
}
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

好了, 组件封装完了. 全选的状态由 el-tree 内部处理, 咱们什么也不用管, 完成!

才怪嘞!!! 这样虽然简单, 但毕竟修改数据了, 一旦涉及到修改数据, 就会有很多麻烦需要处理.

比如: getChecked 这些获取选中节点的方法有可能会返回咱们添加的全选的数据, 而且全选的 check 和 check-change 也会触发(不过这个影响不算太大, 可以根据实际需求灵活应变)

那就只能修改 getChecked 方法, 先拿到当前选中的数据, 过滤掉 全选 的数据, 不能使用 el-tree 的默认方法了

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() {
  // 绑定 el-tree 方法
  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
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

# 文本溢出显示省略号

刚好最近封装了一个文本溢出显示省略号的组件, 可以拿来一用

<slot slot-scope="{ node, data }" v-bind="{ node, data }">
  <text-ellipsis :content="node.label"></text-ellipsis>
</slot>
1
2
3

# 单选时增加 disabled

el-tree 只对选择框处理了 disabled, 单选时却没有, 所以我们需要增加一下

disabled 有两种方式:

  1. 通过 props 的 disabled 字段和在 data 中 disabled 对应字段来禁用
  2. 通过 props 的 disabled 方法来禁用

我们保持这种方式不变, 通过源码可知 disabled 保存在 node 里, 所以我们只需要在这里取即可

我们只需要做两件事:

  1. 设置 disabled 样式
  2. 当点击 disabled 节点后, 通过 setCurrentKey 来重置当前选中节点
<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>
1
2
3
4
5
6
7
8
9
10

这里使用 v-on="{ ...$listeners, 'current-change': handleCurrentChange, 'node-click': handleNodeClick }" 是防止 emit 两遍 current-change 和 node-click 方法

.custom-disabled {
  color: #94969a;
  cursor: not-allowed;
}
1
2
3
4
// 处理单选的 disabled
handleCurrentChange(data, node) {
  const { key, disabled } = node
  if (disabled) {
    // setCurrentKey 传入 null 或 undefined 时会清空选中节点
    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)
}
// setCurrentKey 这两个方法中有一个调用就够了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

el-tree 节点还有右键事件和拖拽事件, 目前项目中没有用到, 就不处理 disabled 了, 如果有用到的话就需要处理一下

# 过滤时加载对应子节点

el-tree 不会返回过滤节点的子节点, 具体看这里: el-tree 节点过滤加载对应子节点 | Henry

所以我们需要自定义一下过滤方法

<el-tree
  :filter-node-method="filterNodeMethod"
>
</el-tree>
1
2
3
4
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)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 单选只能选择叶子节点

最近在工作中经常遇到只能单选叶子节点的功能, 所以我们就需要封装一下.

以前在 SelectTree 中做过这个功能, 但其实应该是 Tree 的功能, 所以就搬到这里啦

基于 ElementUI 封装的 SelectTree | Henry

由以前经验可知需要分两种情况:

  1. 使用 el-tree 自带的 node.isLeaf 判断是否是叶子节点
  2. 传入 isLeafMethod 函数来自定义叶子节点

而且处理逻辑与 disabled 类似, 就提取一个函数处理即可:

// 处理单选的 disabled 和 isLeaf
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
  }
}
// setCurrentKey 这两个方法中有一个调用就够了
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

# 问题

# 方法一 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:

<el-tree
  :key="key"
>
1
2
3
watch: {
  data() {
    this.key = Math.random()
    this.handleCheckChange()
  }
}
1
2
3
4
5
6

这样修改后, 全选状态确实可以正常显示了, 但绑定的 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

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]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

原来人家并没有用 _getAllNodes 获取全部数据, 而是通过 root 拿到所有根节点数据, 再依次遍历子节点来拿到所有选中的数据

那咱们也用 root 不就行了吗?

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
  }
}
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

这次把 allNodes 缓存起来了, 避免每次都要获取一下, 由于是引用地址, 所以状态变化后会跟着改变的; 并且还 emit 出去了 max-level, 省的还要在通过 getTreeMaxLevel 获取

# defaultExpandAll defaultExpandedKeys 无效(已解决)

由于目前在初始化的时候就执行了 expandToLevel 方法, 导致只展开到 level 级

而如果用户没有传入 level, 只传入 defaultExpandAll 或 defaultExpandedKeys, 默认 level 为 1, 那只能展开到一级

所以需要添加 isFirst 判断: 默认为 true, 调用 expandToLevel 时判断是否是第一次调用, 第一次调用的时候判断 defaultExpandAll 或 defaultExpandedKeys 是否有值, 有值的话就不执行后面的代码, 并且把 isFirst 置为 false

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
    // do something
  }
}
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
编辑 (opens new window)
#Js#ElementUI
上次更新: 5/27/2023, 1:02:05 PM
基于 ElementUI 封装的 Tree
基于 ElementUI 封装的 SelectTree

← 基于 ElementUI 封装的 Tree 基于 ElementUI 封装的 SelectTree→

最近更新
01
version 1.15
07-01
02
version 1.14
06-27
03
version 1.13
06-27
更多文章>
Theme by Vdoing | Copyright © 2017-2023 HenryTSZ | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式