我们在键盘上留下的余温, 也将随时代传递到更远的将来


:D 获取中...

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

如下图所示:

setCheckedNodes

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

方法二

源码在这里

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

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 这些获取选中节点的方法有可能会返回咱们添加的全选的数据, 而且全选的 checkcheck-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() {
// 绑定 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
<slot slot-scope="{ node, data }" v-bind="{ node, data }">
<text-ellipsis :content="node.label"></text-ellipsis>
</slot>

单选时增加 disabled

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

disabled 有两种方式:

  1. 通过 propsdisabled 字段和在 datadisabled 对应字段来禁用
  2. 通过 propsdisabled 方法来禁用

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

我们只需要做两件事:

  1. 设置 disabled 样式
  2. 当点击 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-changenode-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
// 处理单选的 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 这两个方法中有一个调用就够了

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

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

  1. 使用 el-tree 自带的 node.isLeaf 判断是否是叶子节点
  2. 传入 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
// 处理单选的 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 这两个方法中有一个调用就够了

问题

方法一 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
<el-tree
:key="key"
>
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, 只传入 defaultExpandAlldefaultExpandedKeys, 默认 level1, 那只能展开到一级

所以需要添加 isFirst 判断: 默认为 true, 调用 expandToLevel 时判断是否是第一次调用, 第一次调用的时候判断 defaultExpandAlldefaultExpandedKeys 是否有值, 有值的话就不执行后面的代码, 并且把 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
// do something
}
}

 评论

 无法加载Disqus评论系统,请确保您的网络能够正常访问。