最近接了个需求,用element
中的选项el-cascader
实现如下2个需求,而且还不止,先做出一些总结。官网中el-cascader
作为级联选择组件,并不能同时支持一级多选,二级单选的功能,只能要么是单选或者多选。不过既然产品提了这个需求,皱着眉头也得上啊。网上也没找到相关特别明显能找到的示例插件,最终只能手撸一个出来。难受
element
中级联选择框组件el-cascader
实现一级目录下的末梢节点只能选中一个但是不同一级目录下的末梢节点可以选择多个。
汇总方式选择不同选项,数据格式显示不同的选项
先说下思路,由于一般来讲级联选择框都只需要提交子级的选项,所以先明确emitPath
需要设置为false
。multiple
设置为true
。
有了这两个前提,那怎么做呢?
逻辑当然就是监听change
事件,通过动态修改el-cascader
组件绑定的值的数据,强行不让勾选同一父级下的二级选项。(但是我这边思路优化了下,每次勾选了同一个二级选项之后,取消之前已经勾选的二级选项,具体看后面的代码)
一开始以为很简单,但是做下来发现,坑啊,是真的坑。
HTML
代码先上html
的代码:
<el-cascader
:key="cascaderKey"
ref="cascaderRef"
style="width: 100%"
:options="groupOptions"
:props="{
value: 'value',
label: 'label',
multiple: true,
emitPath: false,
checkStrictly: true,
children: 'children'
}"
class="tree-search"
v-model="devGroupIds"
@change="(val) => handleCascaderChange(val)"
clearable
filterable
:show-all-levels="false"
:collapse-tags="true"
/>
这个代码就不多解释了, 需要注明的是这里加了这个key
,具体原因后面解释,因为这是这个组件最大的坑。
JS
代码继续上js
的代码,主要是handleCascaderChange
方法:
// 切换分组
handleCascaderChange(valueArr) {
// 判断当前选中的节点的父级节点是否同时存在其他已经勾选的节点
if (valueArr.length > 1) {
const newArr = valueArr.filter(t => !this.preSelectedGroupIds.includes(t))
let indexInAll, indexInCurrent
const newGroupId = newArr[0] || ''
let checkedGroupInParentGroup = []
let oldCheckedGroupIdInParentGroup = ''
// 找到当前新勾选的分组id
if (newGroupId) {
let parentGroup = []
parentGroup = this.groupOptions.find(group => group.children.some(t => t.value === newGroupId))
// 通过当前分组id找到所属一级分组
for (const group of parentGroup.children) {
if (valueArr.includes(group.value)) {
checkedGroupInParentGroup.push(group.value) // 找到该一级分组下勾选的二级分组,最多只有两个
}
if (checkedGroupInParentGroup.length > 1) {
break
}
}
// 当前一级分组如果存在多个已经勾选的二级分组
if (checkedGroupInParentGroup.length > 1) {
checkedGroupInParentGroup.forEach(t => {
if (t !== newGroupId) {
oldCheckedGroupIdInParentGroup = t // 找到一级分组下不是当前勾选分组的id,也就是之前勾选的二级分组id
}
})
// 找到之前勾选的二级分组id在所有已经勾选的二级分组id中的序号和在当前一级分组下所有二级分组忠的序号
indexInAll = valueArr.findIndex(t => t === oldCheckedGroupIdInParentGroup)
// indexInCurrent = parentGroup.children.findIndex(t => t.value === oldCheckedGroupIdInParentGroup)
// this.$nextTick(() => {
// let panelId = this.$refs.cascaderRef.panel.$refs.menu[1].$el.id //其中menu[1]表示右侧的面板 menu[0]即为左侧的面板
// let liId = document.getElementById(panelId + '-' + indexInCurrent)
// // 之前勾选的二级分组id对应的元素,删除其被勾选的样式
// // liId.children[0].click()
// liId.children[0].classList.remove('is-checked')
// liId.children[0].children[0].classList.remove('is-checked')
// })
// 之前勾选的二级分组id需要从已勾选的分组id中移除
valueArr.splice(indexInAll, 1)
// 将当前选中的新分组id放到第一位,以便重新打开时,可以默认打开面板时打开当前分组
const index = valueArr.indexOf(newGroupId)
if (index !== -1) {
valueArr.unshift(valueArr.splice(index, 1)[0])
}
this.devGroupIds = valueArr
// 由于
this.cascaderKey ++
console.log('cascaderKey', this.cascaderKey)
// cascaderKey修改后,组件会重新渲染,这里模拟重新渲染后默认展开数据面板
this.$nextTick(() => {
const cascader = this.$refs.cascaderRef
const trigger = cascader.$el.querySelector(".el-cascader__tags")
trigger.click()
})
}
}
}
this.preSelectedGroupIds = JSON.parse(JSON.stringify(valueArr))
}
定义preSelectedGroupIds
这个全局变量,用来存上一次已经勾选够的选项id
,所以preSelectedGroupIds
的初始值需要获取devGroupIds
的初始值,为了回显的时候也能正常使用。
接下来就是每次触发勾选事件后,通过与preSelectedGroupIds
比对,就能找到当前勾选的选项是哪个。
因为change
事件的参数只有当前全部选项的选项,太坑了,正常逻辑肯定是需要再提供一个当前选择的选项数据的。
找到当前的勾选数据之后,再去找这个选项所属的父级分组。
找到父级分组后,再去找这个父级分组下已经勾选的二级分组,注意这个已经勾选的二级分组肯定是不会超过两条的,因为我们每次都会只保留一个二级分组,所以再勾选一个新的,最多就是勾选了两个。
找到之后,为了实现保留当前勾选的选项,所以要把之前勾选的选项也找到,这样才能取消之前选项的勾选:
这样就找到了,然后就去修改组件绑定的数据,正常来讲就大功告成了。
万万没想到,el-cascader
组件在手动设置了绑定的选项值后,UI
是不会更新的。即便使用this.$set
也是没没用的。
再解决这个问题之前,我的思路是通过找到需要取消勾选的选项的dom
元素,直接移除 is-checked class
样式,这样就直接UI
上操作dom
,UI
肯定就改变了。想法很美好,确实也做到了。额,一点点。。。
确实有一点点效果,但是还有很多问题,如果重新打开面板,被取消的数据又勾上,原因很简单,之前修改的绑定的值是没有直接更新到UI
上的,所以UI
上还是认为那个被删除的选项同时被移除 is-checked
样式的选择没有被删除。
只能上大招了,操作key
。没有办法了,只能让组件重新渲染才能让el-cascader
组件接受绑定的已经选择的选项已经被修改的事实。动用了key
之后,通过找到需要取消勾选的选项的dom
元素,直接移除 is-checked class
样式。但是这样,就有个问题,每次选择完之后,面板就会被关掉,因为组件重新渲染了。
再加了最后一个逻辑:每次更新了key
之后,模拟点击组件的事件,让面板默认打开:
这还不够, 打开后面板会默认打开第一个选项所在的一级分组,所以为了模拟重新展示面板后,还是打开当前选择的选项的分组,需要再做一步:
但是其实还有一个小问题,那就是用户还是能看到面板闪了一下的,因为被关闭了又打开了一次。
<template>
<el-cascader
:key="cascaderKey"
ref="cascader"
:placeholder="placeholder"
:options="options"
v-bind="$attrs"
:clearable='clearable'
filterable
collapse-tags
class="customCascader"
v-on="$listeners"
@expand-change='expandChange'
@change="change"
v-model="val"
>
<slot></slot>
</el-cascader>
</template>
<script>
export default {
name: 'CustomCascader',
props: {
options: {
type: Array,
default: () => []
},
value: {
type: [Array, String],
default: () => []
},
clearable: {
type: Boolean,
default: true
},
placeholder: {
type: String,
default: ''
},
// 是否需要处理二级选项单选
secondGroupSingleCheck: {
type: Boolean,
default: false
},
expandChange: {
type: Function,
default: () => {}
}
},
data() {
return {
val: null,
cascaderKey: 0,
preSelectedGroupIds: null
}
},
methods: {
change(valueArr) {
if (!this.secondGroupSingleCheck) {
this.$emit('input', valueArr)
} else { // 二级选项只能单选
// 判断当前选中的节点的父级节点是否同时存在其他已经勾选的节点
if (valueArr.length > 1) {
const newArr = valueArr.filter(t => !this.preSelectedGroupIds.includes(t))
let indexInAll
const newGroupId = newArr[0] || ''
let checkedGroupInParentGroup = []
let oldCheckedGroupIdInParentGroup = ''
// 找到当前新勾选的分组id
if (newGroupId) {
let parentGroup = []
parentGroup = this.options.find(group => group.children.some(t => t.value === newGroupId))
// 通过当前分组id找到所属一级分组
for (const group of parentGroup.children) {
if (valueArr.includes(group.value)) {
checkedGroupInParentGroup.push(group.value) // 找到该一级分组下勾选的二级分组,最多只有两个
}
if (checkedGroupInParentGroup.length > 1) {
break
}
}
// 当前一级分组如果存在多个已经勾选的二级分组
if (checkedGroupInParentGroup.length > 1) {
checkedGroupInParentGroup.forEach(t => {
if (t !== newGroupId) {
oldCheckedGroupIdInParentGroup = t // 找到一级分组下不是当前勾选分组的id,也就是之前勾选的二级分组id
}
})
// 找到之前勾选的二级分组id在所有已经勾选的二级分组id中的序号和在当前一级分组下所有二级分组忠的序号
indexInAll = valueArr.findIndex(t => t === oldCheckedGroupIdInParentGroup)
// 之前勾选的二级分组id需要从已勾选的分组id中移除
valueArr.splice(indexInAll, 1)
// 将当期选中的新分组id放到第一位,以便重新打开时,可以默认打开面板时打开当前分组
const index = valueArr.indexOf(newGroupId)
if (index !== -1) {
valueArr.unshift(valueArr.splice(index, 1)[0])
}
// 由于
this.cascaderKey ++
// cascaderKey修改后,组件会重新渲染,这里模拟重新渲染后默认展开数据面板
this.$nextTick(() => {
const cascader = this.$refs.cascader
const trigger = cascader.$el.querySelector(".el-cascader__tags")
trigger.click()
})
}
}
}
}
}
},
watch: {
value: {
handler(val){
this.val = val
this.preSelectedGroupIds = JSON.parse(JSON.stringify(val))
},
immediate: true
}
}
}
</script>
<style lang="scss" scoped>
.customCascader ::v-deep.el-cascader__tags {
flex-wrap: nowrap;
}
.customCascader ::v-deep .el-cascader__tags .el-tag {
max-width: 50%;
}
.customCascader ::v-deep.el-cascader__search-input {
min-width: 22px;
}
::v-deep.customCascader .el-input__inner {
height: 32px !important;
}
::v-deep.customCascader .el-input__suffix {
z-index: 100;
}
</style>
本人每篇文章都是一字一句码出来,希望对大家有所帮助,多提提意见。顺手来个三连击,点赞收藏关注✨,一起加油☕