iview-select-tree树型选择器组件封装
实现效果: 二级单选,二级多选(新建与编辑)
具体效果:
1、数据手风琴样式显示,默认第一组数据展开
2、每次只能选择同一组数据,选择不同组数据时原来被选择的数据清除
3、编辑时只用传一组数据,因为不能选择不同组数据
4、编辑时原先被选中的数据不能被删除,只能新增同组下的其他数据
数据格式:
// treeData: flag - 新建或编辑; title,value必填,name必须没有;
// children里面的name,title必填,value前一个数字必须与父级一致。即父级value为m,则子级value为m-n
[
{
checked: false,
expand: true,
flag: "add",
selected: false,
title: "单元测试1",
value: "0",
children: [
{
checked: false,
id: "00",
level: "1",
name: "pp1",
selected: false,
title: "pp1",
value: "0-0",
},
{
checked: false,
id: "01",
level: "2",
name: "pp2",
selected: false,
title: "pp2",
value: "0-1",
}
],
},
{
checked: false,
expand: false,
flag: "add",
selected: false,
title: "单元测试2",
value: "1",
children: [
{
checked: false,
id: "10",
level: "3",
name: "pp3",
selected: false,
title: "pp3",
value: "1-0",
},
{
checked: false,
id: "11",
level: "4",
name: "pp4",
selected: false,
title: "pp4",
value: "1-1",
}
],
}
]
<template>
<div class="ivu-select ivu-select-default">
<div tabindex="0" class="ivu-select-selection ivu-form-item-content">
<div @mouseover="mouseover" @mouseleave="mouseleave">
<div @click="clickInputShow">
<div v-show="multiple" class="ivu-tag ivu-tag-checked " v-for="(item,index) in multipleShowVal" :key="item.id">
<span class="ivu-tag-text" :class="{'notClick': clickDisabled[item.id]}">{{item.title}}</span>
<i class="ivu-icon ivu-icon-ios-close" :class="{'notClick': clickDisabled[item.id]}" @click.stop="!clickDisabled[item.id]?removeVal(index): ''"></i>
</div>
<span v-show="!multiple && queryVal!=''" class="ivu-select-selected-value">{{queryVal.title}}</span>
<span v-if="multipleShowVal.length === 0 && multiple " class="ivu-select-placeholder">请选择</span>
<span v-if="queryVal === '' && !multiple " class="ivu-select-placeholder">请选择</span>
</div>
<i :class="'ivu-icon ivu-icon-' +iconVal+ ' ivu-select-arrow'" @click="clickIcon"></i>
</div>
<div v-show="showTree" class="ivu-select-dropdown"
style="max-height: 200px;overflow-y:scroll;z-index:9999;width:100%;" >
<div style="width: 95%;margin-left: 10px;">
<Tree
:data="queryData"
:multiple="multiple"
ref="tree"
@on-select-change="selectChange"
@on-check-change="selectChange"
@on-toggle-expand="toggleExpand"
show-checkbox
check-directly>
</Tree>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'selectTree',
props: {
// 下拉树的数据
treeData: {
type: Array
},
// 指定选中项目的 value 值,可以使用 v-model 双向绑定数据。单选时只接受 String 或 Number,多选时只接受 Array
value: {},
// 是否允许多选
multiple: {
type: Boolean,
default: true
},
// 是否可以清空选项,只在单选时有效
clearable: {
type: Boolean,
default: false
},
// 是否禁用或启用当前的下拉框
disabled: {
type: Boolean,
default: true
}
},
data () {
return {
queryVal: '',
hideValue: '',
valueType: 'string',
multipleShowVal: [], // 选中的title值
multipleHideVal: [], // 选中的value值
showTree: false,
iconVal: 'ios-arrow-down',
cloneData: JSON.parse(JSON.stringify(this.treeData)),
showData: [],
queryData: [],
clickDisabled: {},
}
},
methods: {
clickIcon () {
if(this.disabled){
if (this.iconVal === 'ios-close-circle') {
this.clearVal()
} else {
this.showSelectTree()
}
}
},
pickTree (val) {
if (this.valueType === 'string') {
for (let i = 0; i < this.showData.length; i++) {
if (this.showData[i].value === val) {
this.showData[i].selected = false;
this.showData[i].checked = false;
} else if (this.showData[i].children !== undefined) {
this.recursionPickTree(val, this.showData[i].children)
}
}
} else {
for (let j = 0; j < val.length; j++) {
for (let i = 0; i < this.showData.length; i++) {
if (this.showData[i].value === val[j]) {
this.showData[i].selected = false;
this.showData[i].checked = false;
} else if (this.showData[i].children !== undefined) {
this.recursionPickTree(val[j], this.showData[i].children)
}
}
}
}
},
recursionPickTree (val, data) {
for (let i = 0; i < data.length; i++) {
if (data[i].value === val) {
data[i].selected = false;
data[i].checked = false;
} else if (data[i].children !== undefined && data[i].children.length > 0) {
this.recursionPickTree(val, data[i].children)
}
}
},
clearVal () {
if(this.disabled){
if (this.clearable && !this.multiple && this.iconVal === 'ios-close-circle') {
this.pickTree(this.hideValue)
this.queryVal = ''
this.hideValue = ''
if (this.showTree) {
this.iconVal = 'ios-arrow-up'
} else {
this.iconVal = 'ios-arrow-down'
}
this.$emit('input', '')
}
}
},
mouseover () {
if (this.clearable && !this.multiple && this.iconVal !== 'ios-close-circle' && this.hideValue !== '') {
this.iconVal = 'ios-close-circle'
}
},
mouseleave () {
if (this.clearable && !this.multiple) {
if (this.showTree) {
this.iconVal = 'ios-arrow-up'
} else {
this.iconVal = 'ios-arrow-down'
}
}
},
selectChange (obj,curentObj) {
// 当前已选中的节点数组obj、当前项curentObj
let hideVal;
let showVal;
if(this.multiple) {
let obj1 = [], obj2 = [];
obj.filter(item => {
// children项有name值,obj里面有选中数据的父元素
if(item.name && item.value.toString().charAt(0) === curentObj.value.toString().charAt(0)) {
obj1.push(item); // 当前需要选中的数据
} else if(item.value.toString().charAt(0) !== curentObj.value.toString().charAt(0)) {
obj2.push(item); // 上次选中的数据
}
})
for(let i in obj2) { // 上次选中的数据取消选中
obj2[i].checked = false;
obj2[i].selected = false;
}
if(obj2.length !== 0) {
for(let i in this.showData) {
if(this.showData[i].value === obj2[0].value.toString().charAt(0)) {
this.showData[i].indeterminate = false; // 上次选中的数据父节点取消半选
}
}
}
this.multipleShowVal = [];
this.multipleHideVal = [];
let len = obj1.length;
if(len) {
for(let i = 0; i < len; i++) {
this.multipleShowVal.push(obj1[i]);
this.multipleHideVal.push(obj1[i].value);
}
}
this.$emit('input', this.multipleHideVal);
hideVal = this.multipleHideVal;
showVal = this.multipleShowVal;
}else {
// 过滤掉所选择的父元素
let obj1 = [], obj2 = [];
if(obj.length !== 0) {
if(!isNaN(Number(curentObj.value))) {
// 点击的是父元素
obj1.push(curentObj.children[0]);
obj2 = obj.filter(item => {
return item.value !== curentObj.children[0].value
})
} else {
// 点击子元素
obj1.push(curentObj)
obj2 = obj.filter(item => {
return item.value !== curentObj.value
})
}
}
for(let i in obj2) { // 上次选中的数据取消选中
obj2[i].checked = false;
obj2[i].selected = false;
for(let j in this.showData) {
if(this.showData[j].value === obj2[i].value.toString().charAt(0)) {
this.showData[j].indeterminate = false; // 上次选中的数据父节点取消半选
}
}
}
if(obj1.length !== 0) {
this.queryVal = obj1[0];
this.hideValue = obj1[0].value;
this.$emit('input', obj1[0].value);
} else {
this.queryVal = '';
this.hideValue = '';
this.$emit('input', '');
}
this.showTree = false;
this.iconVal = 'ios-arrow-down';
hideVal = this.hideValue;
showVal = this.queryVal;
this.$emit("on-select-change", this.queryVal,this.hideValue);
this.$emit("on-check-change", this.queryVal,this.hideValue);
}
this.$emit("on-select-change", hideVal, showVal);
this.$emit("on-check-change", hideVal, showVal);
},
toggleExpand(val) {
// 手风琴效果,每次只展开一组数据
// 不选择任何数据时,展开与否与treeData有关;选择任意数据时,展开与否与showData有关
for(let i in this.treeData) {
if(this.treeData[i].value !== val.value) {
this.treeData[i].expand = false;
}
}
for(let i in this.showData) {
if(this.showData[i].value !== val.value) {
this.showData[i].expand = false;
}
}
},
// 点击图标的时候展示树形菜单
clickInputShow () {
if(this.disabled){
this.showSelectTree()
}
},
showSelectTree () {
if (this.showTree) {
this.showTree = false
} else {
this.showTree = true
}
if (this.iconVal !== 'ios-close-circle') {
if (this.iconVal === 'ios-arrow-down') {
this.iconVal = 'ios-arrow-up'
} else if (this.iconVal === 'ios-arrow-up') {
this.iconVal = 'ios-arrow-down'
}
}
},
// 多选模式的时候删除节点的数据
removeVal (index) {
if(this.disabled) {
if(this.showData[0].flag == 'add') {
let showData = this.showData.filter(item => {
return item.value === this.multipleHideVal[index].toString().charAt(0)
})
showData[0].selected = false;
showData[0].checked = false;
let len = showData[0].children.length;
for (let i = 0; i < len; i++) {
if(showData[0].children[i].value === this.multipleHideVal[index]) {
showData[0].children[i].selected = false;
showData[0].children[i].checked = false;
}
}
this.multipleShowVal.splice(index, 1);
this.multipleHideVal.splice(index, 1);
if(this.multipleHideVal.length === 0) {
showData[0].indeterminate = false;
}
} else if(this.showData[0].flag == 'update') {
this.showData[0].selected = false;
this.showData[0].checked = false;
let len = this.showData[0].children.length;
for (let i = 0; i < len; i++) {
if(this.showData[0].children[i].value === this.multipleHideVal[index]) {
this.showData[0].children[i].selected = false;
this.showData[0].children[i].checked = false;
}
}
this.multipleShowVal.splice(index, 1);
this.multipleHideVal.splice(index, 1);
}
}
},
isNotClick() {
if(this.showData.length && this.showData[0].flag == 'update') {
for(let i in this.showData[0].children) {
this.clickDisabled[this.showData[0].children[i].id] = this.showData[0].children[i].disabled;
}
}
},
// 验证当前下拉
checkValueType () {
if (typeof this.value !== 'string') {
this.valueType = 'array'
}
},
initQueryMultiple () {
if(this.value) {
for (let i = 0; i < this.value.length; i++) {
for (let j = 0; j < this.cloneData.length; j++) {
if (this.cloneData[j].value === this.value[i]) {
this.multipleShowVal.push(this.cloneData[j])
this.multipleHideVal.push(this.cloneData[j].value)
this.$emit('input', this.multipleHideVal)
this.cloneData[j].selected = true;
this.cloneData[j].checked = true;
break
} else if (this.cloneData[j].children !== undefined) {
this.recursionQueryTreeData(this.cloneData[j].children, this.value[i])
}
}
}
}
},
initQueryVal () {
this.$emit('input', '')
for (let i = 0; i < this.cloneData.length; i++) {
if (this.cloneData[i].value === this.value) {
this.hideValue = this.cloneData[i].value
this.queryVal = this.cloneData[i]
this.$emit('input', this.cloneData[i].value)
this.cloneData[i].selected = true;
this.cloneData[i].checked = true;
return
} else if (this.cloneData[i].children !== undefined) {
this.recursionQueryTreeData(this.cloneData[i].children, this.value)
}
}
},
recursionQueryTreeData (data, val) {
for (let i = 0; i < data.length; i++) {
if (data[i].value === val) {
// 当前会多选
if (this.multiple) {
this.multipleShowVal.push(data[i])
this.multipleHideVal.push(data[i].value)
this.$emit('input', this.multipleHideVal)
} else {
this.hideValue = data[i].value
this.queryVal = data[i]
this.$emit('input', data[i].value)
}
data[i].selected = true;
data[i].checked = true;
} else if (data[i].children !== undefined) {
this.recursionQueryTreeData(data[i].children, val)
}
}
},
removeShowVal(data,val,showVal,hideVal){
for(let j = 0; j < data.length; j++){
if(data[j].value === val){
showVal.push(data[j])
hideVal.push(data[j].value)
break
} else if(data[j].children!=undefined && data[j].children.length>0){
this.removeShowVal(data[j].children,val,showVal,hideVal)
}
}
}
},
watch:{
treeData(val){
this.cloneData = JSON.parse(JSON.stringify(val));
this.showData = JSON.parse(JSON.stringify(this.cloneData));
this.queryData = this.showData;
let multipleHideValNew = [];
let multipleShowValNew = [];
if (this.multiple) {
for(let i = 0;i<this.multipleHideVal.length;i++){
this.removeShowVal(this.showData,this.multipleHideVal[i],multipleShowValNew,multipleHideValNew);
}
this.multipleHideVal = multipleHideValNew;
this.multipleShowVal = multipleShowValNew;
} else {
this.removeShowVal(this.showData,this.hideValue,multipleShowValNew,multipleHideValNew);
if(multipleHideValNew.length === 0){
this.pickTree(this.hideValue);
this.queryVal = '';
this.hideValue = '';
this.$emit('input', '');
}
}
}
},
mounted () {
// 因为父组件中要传递的 props 属性 是通过 发生ajax请求回来的, 请求的这个过程是需要时间的,
// 但是子组件的渲染要快于ajax请求过程,所以此时 created 、 mounted 这样的只会执行一次的生命周期钩子,已经执行了,
// 但是 props 还没有流进来(子组件),所以只能拿到默认值。
// 解决方法:
// 1、父组件用v-if,当父组件有数据才渲染;
// 2、子组件用watch,并且加上deep属性,可以直接在handler里面传入在子组件定义好的方法;
this.checkValueType();
// 要先初始化选中的数据,再将初始化好的数据赋值给到当前的tree,否则将导致选中的效果显示不出来
if (this.multiple) {
this.initQueryMultiple();
} else {
this.initQueryVal();
}
if(this.treeData.length && this.treeData[0].flag == 'add') {
this.multipleShowVal = [];
this.multipleHideVal = [];
this.showData = JSON.parse(JSON.stringify(this.treeData));
this.queryData = this.treeData;
this.$emit('input', '');
} else if(this.treeData.length && this.treeData[0].flag == 'update') {
this.showData = JSON.parse(JSON.stringify(this.cloneData));
this.queryData = this.showData;
}
this.isNotClick();
}
}
</script>
<style lang="less" scoped>
.ivu-select-placeholder {
display: block;
height: 30px;
line-height: 30px;
color: #c5c8ce;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 8px;
padding-right: 22px;
}
.ivu-select-selected-value {
display: block;
height: 30px;
line-height: 30px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 8px;
padding-right: 24px;
}
.notClick {
// pointer-events: none;
color:blue;
cursor:not-allowed;
}
/deep/ .ivu-select-item-selected.ivu-select-item-focus {
background: #fff;
}
/deep/ .ivu-select-item-selected, .ivu-select-item-selected:hover {
background: #fff;
}
/deep/ .ivu-select-item-focus, .ivu-select-item:hover {
background: #fff;
}
/deep/ .ivu-tree ul {
text-align: left;
}
</style>