此级联选择器有三种状态:未选,半选,全选。半选就是此树结构的下方有选中的,全选就是此树结构的下方全部选中。实现上下级联动。
手写一个多选级联选择器,我们首先要创建一个外壳组件,用来处理默认数据增加一些显示隐藏,选中属性和回显功能。
我们把外壳组件起名为multipleChoice.vue,这里先放上template代码,已选显示的列表我没做,已选的ID会放在数据selected里,你们可以自行寻找。
<div v-click-outside="close" class="department-option" @click.stop="show = true">
<div class="now-text">
选择部门
</div>
<transition name="transition-drop">
<div class="department-box" v-show="show">
<MultipleChoiceItem v-model="listData" @child-change="childChange" :selectKey="selectKey" />
</div>
</transition>
</div>
接下来就是script代码。我们要引入组件 MultipleChoiceItem 组件,这个组件会放在下面。这是用来递归显示的组件。
注意:一下代码是multipleChoice.vue的script代码。内部实现了对数据的处理,给已选中的定义选中状态,给原始数据增加是否选中字段,显示隐藏字段,给每条数据都赋值一个父级原型链。为的是当前状态修改后,所有父级需要变更状态如(半选,全选,未选)。
props主要传入列表数据,是否需要自定义key包括返回的id,要显示的name,树结构的子级字段,v-model需要绑定的数据。
<script>
import MultipleChoiceItem from "./multipleChoiceItem.vue"
export default {
name: 'multipleChoice',
data() {
return {
show: false,
listData: [], // 列表数据
selected: [], // 最终选中
}
},
props: {
data: {
type: Array,
default: () => []
},
// 未选提示
placeholder: {
type: String,
default: '请选择'
},
/**
* 用到的key
* 默认 id name child
*/
selectKey: {
type: Object,
default: () => {
return { value: 'id', lable: 'name', child: 'child' }
}
},
value: {
type: Array,
default: () => []
}
},
watch: {
show(newVal) {
if(!newVal) {
this.updateData()
}
}
},
created() {
this.init();
},
methods: {
childChange(val) {
// console.log(val)
},
openCascader() {
this.show = true;
this.init()
},
init() {
if (this.data && this.data.length) {
let arr = JSON.parse(JSON.stringify(this.data))
this.washData(arr, null); // 洗数据
this.listData = arr;
}
},
washData(treeList, parent, flag) {
for (let i = 0; i < treeList.length; i++) {
let flag_n = false; // 父级有选中子级不显示 标识符
let jsonTree = JSON.parse(JSON.stringify(treeList[i])); // 赋值已选
const tree = treeList[i];
tree.parent = parent;
if(this.value.includes(tree[this.selectKey.value])) {
flag_n = true;
// 默认回选
if (tree.parent) {
const diffParnet = (child_item) => {
if(child_item.check != 1) { // 只有没有选中的父级才能是半选状态
child_item.check = 2;
}
if(child_item.parent) {
diffParnet(child_item.parent);
}
}
diffParnet(tree.parent)
}
tree.check = 1;
tree.show = true;
if(!flag) {
this.selected_item.push(jsonTree); // 回源默认选中
}
} else {
// 默认为0
tree.check = 0;
tree.show = false;
}
if (tree[this.selectKey.child]) {
this.washData(tree[this.selectKey.child], tree, flag_n);
}
}
},
// 弹窗关闭
updateData() {
this.selected = [];
this.findSelected(this.listData);
this.$emit('input', this.selected);
this.$emit('on-change', this.selected);
},
close() {
this.show = false;
},
// 查询已被选中的数据
findSelected(arr) {
arr.forEach(v => {
if (v.check == 1) {
this.selected.push(v.id)
}
if (v[this.selectKey.child] && v[this.selectKey.child].length) {
this.findSelected(v[this.selectKey.child])
}
})
}
},
components: {
MultipleChoiceItem
}
}
</script>
接下来是multipleChoice.vue的style代码
<style lang="less" scoped>
.department-option {
width: 300px;
height: 32px;
margin-top: 20px;
position: relative;
border: 1px solid #d9d9d9;
cursor: pointer;
.department-box {
width: 298px;
max-height: 180px;
overflow-y: auto;
}
.select-list {
display: inline-block;
background-color: #f5f5f5;
color: rgba(0, 0, 0, .85);
margin: 3px 0 3px 3px;
height: 24px;
line-height: 24px;
padding: 0 10px;
.icon-close {
margin-left: 6px;
font-size: 10px;
color: rgba(0, 0, 0, .45);
}
}
.now-text {
overflow: hidden;
color: #bfbfbf;
white-space: nowrap;
text-overflow: ellipsis;
pointer-events: none;
padding: 0 6px;
line-height: 30px;
}
input {
position: absolute;
width: 0;
border: 0;
left: 6px;
top: 0;
height: 30px;
padding: 6px 6px;
}
}
</style>
我们写好multipleChoice.vue组件后,要继续创建multipleChoiceItem.vue组件
multipleChoiceItem.vue组件是为了递归显示每条数据的,里面包括了多选,上级下级联动的逻辑。
下方是template代码,重复使用组件multipleChoiceItem(自己)来实现递归。
<template>
<div>
<div class="list" :class="{ 'child-list': is_child }" v-for="(item, index) in dataList">
<div class="main-body">
<span class="more-icon" :class="{ 'body-icon': item[selectKey.child], 'active': item.show }"
@click="changeShow(item, index)"></span>
<span class="check-box" @click="changeCheckStatus(item, index)">
<span class="chech-item" :class="isCheckClass(item.check)"></span>
{{ item[selectKey.lable] }}
</span>
</div>
<multipleChoiceItem v-if="item[selectKey.child] && item.show" v-model="item[selectKey.child]" :is_child="true" />
</div>
</div>
</template>
下方是script代码,内部实现了选中后递归所有子级选中,父级递归检测是全选还是半选状态。
<script>
// 多选级联选择器
export default {
name: 'multipleChoiceItem',
data() {
return {
dataList: []
}
},
props: {
value: {
type: Array,
default: () => []
},
is_child: {
type: Boolean,
default: false
},
/**
* 用到的key
* 默认 id name child
*/
selectKey: {
type: Object,
default: () => {
return { value: 'id', lable: 'name', child: 'child' }
}
},
},
created() {
this.dataList = this.value;
},
watch: {
},
methods: {
isCheckClass(val) {
if(val == 0) { // 未选中
return ''
} else if(val == 1) {
return 'pitch-on'; // 选中
} else if (val == 2) {
return 'half-select'; // 半选中
}
},
// 修改选中状态
changeCheckStatus(item, i) {
if(item.check == 0 || item.check == 2) {
item.check = 1;
} else {
item.check = 0;
}
try {
this.changeStatus(item, i)
} catch(err) {
console.log('err=', err)
}
},
// 父级自己联动
changeStatus(item, i) {
// console.log(item)
// 子级全部修改相同
this.diffChangeStatus(item)
// 父级联动
if (item.parent) {
const diffParnet = (child_item) => {
child_item.check = this.findParent(child_item);
if(child_item.parent) {
diffParnet(child_item.parent);
}
}
diffParnet(item.parent)
}
},
diffChangeStatus(item, i) {
item[this.selectKey.child] && item[this.selectKey.child].forEach(v => {
v.check = item.check;
if (v[this.selectKey.child] && v[this.selectKey.child].length) {
this.diffChangeStatus(v)
}
})
},
// 查找父级的选中的状态
findParent(item) {
let flag = 0;
let flag_arr = [];
let THIS = this;
function filterData(arr) {
arr.forEach(v => {
if (v.check == 1) {
flag = 1;
}
if(!flag_arr.includes(v.check)) {
flag_arr.push(v.check)
}
// if (v[THIS.selectKey.child]) {
// filterData(v[THIS.selectKey.child])
// }
})
}
filterData(item[THIS.selectKey.child]);
if(flag_arr.length > 1) {
return 2
} else {
return flag
}
},
// 修改当前显示
changeShow(item, i) {
item.show = !item.show;
},
},
}
</script>
下方是style代码
<style lang="less" scoped>
.check-box {
display: flex;
align-items: center;
}
.chech-item {
display: inline-block;
width: 14px;
height: 14px;
border: 1px solid #D7D7D7;
position: relative;
margin-right: 4px;
transition: all .3s;
// 半选中
&.half-select {
border-color: #2d8cf0;
background-color: #2d8cf0;
color: #2d8cf0;
&::after {
content: "";
width: 8px;
height: 2px;
background-color: #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-4px, -1px);
}
}
// 全选
&.pitch-on {
border-color: #2d8cf0;
background-color: #2d8cf0;
color: #2d8cf0;
&::after {
content: "";
width: 4px;
height: 8px;
position: absolute;
top: 1px;
left: 4px;
border: 2px solid #fff;
border-top: 0;
border-left: 0;
transition: all 0.2s ease-in-out;
transform: rotate(45deg) scale(1);
}
}
}
.list {
width: 100%;
&.child-list {
padding-left: 20px;
}
.more-icon {
display: inline-block;
position: relative;
width: 30px;
height: 100%;
&.body-icon::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
margin-top: -4px;
margin-left: 0px;
width: 0;
height: 0;
border-left: 4px solid #787878;
border-right: 4px solid transparent;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
}
&.active::after {
transform: rotate(90deg);
}
}
.main-body {
display: flex;
align-items: center;
height: 30px;
}
}
</style>