重大更新
最新版本的element已经有级联多选功能了
前沿
吐槽一下,程序猿最不愿听到的话之一,(人家某某网站就做出来了,你怎么做不出来,简直丧心病狂)小编最近一直在开发基于vue-elementui的pc端项目,就碰到了来自产品的这句话,都有种拿起显示器了。不过吐槽归吐槽,项目还是要写的。。。。。。在本项目中产品提的一个需求,就是人家某某网站上有的,而element-ui上没有,那就是Cascader 级联选择器,element-ui只支持单选,于是就开始了折腾,再折腾了快一周的时间吧,还是没搞出来,最后由于项目着急上线,只能暂时先放弃,所以就先搁置了,后来幸得于空,于是乎又是开始折腾,毕竟也是自己的问题。哎,不说了,show time.
该多选菜单基于 element-ui 的Cascader 层级菜单, 但是在我的一番折腾下开发出一套支持多选的,有禁用状态,以及灵活控制选几个,适应产品的奇葩需求,Cascader 层级菜单。羞于一提的是,我折腾了整整3天,才搞出来。在这里把我的心路历程记录下来,里边的注释写的个人感觉都挺全的,有不明白的也可以与我交流,共同探讨,方便后续学习与扩展。
先上个效果图
现附上该插件的菜单配置项,以方便后期维护
attributes属性说明
属性名 | 描述 | 类型 | 默认值 |
---|---|---|---|
width | 菜单选择面板的宽度 | String | 220px |
height | 菜单选择面板的高度 | String | 240px |
options | 选择器菜单数据配置项 | Array | [] |
inputValue | 选择器输入框内显示的内容 | String | 220px |
outputType | 选中项输出的字段名,outputType 用于输出选中选择项对象的某一字段, 默认值: value, 当传入 outputType 为item时, 输出选中这一项的对象(不包括 children 属性); | String | value |
disabledPair | 互斥选项对儿,就是选择一个其他的就被禁用 | Object | -- |
事件名称
事件名称 | 说明 | 回调参数 |
---|---|---|
on-selected | 选择器中的某一项被选中的时候触发的事件 | 数组,数组内包含被选中的值 |
options 菜单配置,就是完全按照elementui Cascader 的options的格式
属性名 | 描述 | 类型 |
---|---|---|
value | 选项的值 | String or Number |
label | 选项的名称 | String |
checked | 该选项是否被选中 | Boolean |
children | 如果存在下一级菜单,是属于该选项的下一级选项值, 非必须 | Array |
multiple | 是否多选 | true为多选,false为单选 |
disabled | 是否禁用 | true为禁用,false为不禁用 |
再简单介绍一下disabledPair属性
disabledPair 用于设置禁用对, 对象形式, 接收两个属性: thisPair thatPair:
disabledPair: {
thisPair: [1], //这里的1是value的值
thatPair: [2],
}
那么, 当值为 1 的选项被选中的时候, 值为 2 的选项将会被禁用掉, 反之亦然。但其他选项的值不会受到影响 除了传递单独的项之外, 还可以单独传入一个 all。disabledPair: {
thisPair: [1],
thatPair: ["all"]
}
首先,先建一个公共的文件夹MulitileCascader,里边包含有三个自己封装的文件
一,index.vue 此页面为主要出口文件,会发射出一个得到选中后的item的方法以及数组。
<template>
<span class="dropTreeLists">
<span class="benchmark">基准 :</span>
<multiCascader :options="configOptions"
@on-selected="getSelected"
:inputValue="configTips"></multiCascader>
</span>
</template>
<script>
import multiCascader from "./MulCheckCascader.vue";
//这个也是我们项目的接口,不必纠结,倒是换位自己的接口就好了
import { getlistBenchmark } from "@/api/basicManage";
export default {
components: {
multiCascader
},
data() {
return {
configTips: "请选择基准",
//模板勿删
configOptions: [
{
value: "1",
label: "一级菜单",
checked: false, //控制是否默认选中
multiple: false, //是否多选 false为该一级菜单不多选,true表示多选
children: [
{
value: 11,
checked: false,
multiple: false,
disabled:true, //是否禁用
label: "二级菜单",
children: [
{
value: "21",
checked: false,
multiple: false, //是否多选 false为该一级菜单不多选,true表示多选
disabled :true, //是否禁用
label: "三级菜单1"
},
{
value: "22",
checked: false,
label: "三级菜单2"
}
]
},
{
value: "12",
checked: false,
multiple: false,
label: "二级菜单",
children: [
{
value: "399300",
checked: true,
label: "三级菜单复制"
},
{
value: "399300",
checked: false,
label: "三级菜单"
}
]
}
]
}
],
commonLength: ""
};
},
mounted() {
this.MulitGetlistBenchmark(); //多选
},
methods: {
// 点击每一个item的时候的操作 在这个方法内灵活判断多选的状态以及禁用状态
getSelected(val) {
let strnum = val.length;
console.log(val);
// 当选中的指数大于1并且小于10的时候让所有的指数都可以选择(没有禁用状态)
if (val.length > 1 && val.length < 10) {
this.LessThanThen(this.configOptions);
}
// 必须保留一个选中的
if (val.length == 1) {
let moreOne = val[0];
this.LessThanMoreOne(this.configOptions, moreOne);
}
// 当选中的指数大于10的时候让除选中的之外的指数都变为禁用状态
if (val.length >= 10) {
let moreOne = val;
this.LessThanMoreTen(this.configOptions, moreOne);
}
if (strnum !== this.commonLength) {
//将选中后的数组暴漏出去,在需要的页面使用
this.$emit("CheckedsIndexCodes", val);
}
this.commonLength = val.length;
// 勿删后期需求改变会用
// this.selectGroups = val;
// this.configTips = `已选择${val.length}个分组`;
},
// 此递归为当选中的指数大于10的时候让除选中的之外的指数都变为禁用状态
LessThanMoreTen(datas, moreOne) {
for (var i in datas) {
if (datas[i].multiple !== false) {
// console.log(datas[i]);
datas[i].disabled = true;
for (let d = 0; d < moreOne.length; d++) {
if (datas[i].value == moreOne[d]) {
datas[i].disabled = false;
}
}
} else {
this.LessThanMoreTen(datas[i].children, moreOne);
}
}
},
// 此递归为当选中的为选中的只剩下一个的时候禁止取消,也就是必须保留一个选中的
LessThanMoreOne(datas, moreOne) {
for (var i in datas) {
if (datas[i].multiple !== false) {
// console.log(datas[i]);
if (datas[i].value == moreOne) {
datas[i].disabled = true;
}
} else {
this.LessThanMoreOne(datas[i].children, moreOne);
}
}
},
// 此递归为当选中的为 满足该条件时(val.length > 1 && val.length < 10) 所有的item的都可以选则
LessThanThen(datas) {
for (var i in datas) {
if (datas[i].multiple !== false) {
// console.log(datas[i]);
datas[i].disabled = false;
} else {
this.LessThanThen(datas[i].children);
}
}
},
// 此递归为初始化时默认选中沪深300,由于只有一个所以禁用沪深300
getArrayList(datas) {
for (var i in datas) {
if (datas[i].multiple !== false) {
// console.log(datas[i]);
datas[i].disabled = false;
if (datas[i].value === "399300") {
datas[i].disabled = true;
datas[i].checked = true;
}
} else {
// console.log(datas[i]);
//每次在传入父节点的childreg去查找,自己调用自己的方法
this.getArrayList(datas[i].children);
}
}
},
MulitGetlistBenchmark() {
//此接口为我们项目中的接口,上边有数据模板,可根据数据模板来写数据。
getlistBenchmark({}).then(response => {
this.configOptions = response.data.data;
this.getArrayList(this.configOptions);
});
}
}
};
</script>
<style lang="scss" scoped>
.benchmark {
font-size: 14px;
}
</style>
二,MulCheckCascader.vue //此页面为基础模板,会在该页面引用递归出来的多选的item的字模板,并且该页面会接受引用页面传过来的数据,方便灵活控制尺寸,数据,是否禁用等的状态。
<template lang='html'>
<div class='multil-cascader'>
<el-popover placement="top-start" popper-class="multi-cascader-popover" :visible-arrow="showArrow" trigger="click" @hide="whenPopoverHide" @show="whenPopoverShow">
<muContent
:height="height"
:width="width"
:option="options"
@handleOutPut="whenOutPut"
:selectedValues="selectedValues"
:outputType="outputType"
:disabledPair="disabledPair">
</muContent>
<el-input popper-class="slect-panel" v-if="activeItem[0] && activeItem[0].level === 0" v-model="inputValue" readonly slot="reference" :suffix-icon="inputArrow"/>
</el-popover>
</div>
</template>
<script>
import muContent from "./multiContent";
export default {
name: "multiCascader",
props: {
options: {
type: Array,
default() {
return [];
}
},
width: {
type: String,
default: ""
},
height: {
type: String,
default: ""
},
inputValue: {
type: String,
default() {
return "";
}
},
// 输出值的类型
outputType: {
type: String,
default() {
return "value";
}
},
// 互斥对儿
disabledPair: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
// 被选中的值
selectedValues: [],
showArrow: true,
activeItem: [],
outputValue: [],
optionDicts: [],
inputArrow: "el-icon-arrow-down",
popoverWidth: "",
// 展开之后的数组, 将每一个children 展开
flatOptions: []
};
},
watch: {
"options": function () {
this.initData();
}
},
components: {
muContent
},
created() {
this.initData();
this.setOptionDicts(this.options);
this.toFlatOption(this.options);
},
methods: {
whenPopoverHide() {
this.inputArrow = "el-icon-arrow-down";
},
whenPopoverShow() {
this.inputArrow = "el-icon-arrow-up";
},
// 初始化数据 对于每一项 options 添加相关字段并且获取到当前被点击到的元素
initData() {
this.setLevel();
const { width, height } = this;
const checkedValues = [];
let childrenValues = [];
const getChecked = (item) => {
const { checked, value, children, level, siblingValues } = item;
if (siblingValues) {
const tempValues = [...siblingValues];
item.siblingValues = tempValues;
}
childrenValues.push(value);
if (children && children.length > 0) {
children.forEach(child => {
getChecked(child);
});
} else {
if (checked && item[this.outputType]) checkedValues.push(item[this.outputType]);
}
};
this.activeItem = this.options;
this.options.forEach(child => {
getChecked(child);
// 设置当前item 的 childrenValues, 包含当前item 下的所有值的 value
child.childrenValues = [...childrenValues];
childrenValues = [];
});
this.selectedValues = checkedValues;
this.whenOutPut(this.selectedValues);
},
getTypeOptions(values, outputType) {
const outputValues = [...values];
const finalOutputArr = [];
return this.flatOptions.reduce((pev, cur) => {
const { value: curVal } = cur;
if (outputType === "item") {
if (outputValues.includes(curVal)) pev.push(cur);
} else {
if (outputValues.includes(curVal) && cur[outputType]) pev.push(cur[outputType]);
}
return pev;
}, []);
},
// 展开配置中的各项, [{}], 排除 children 属性
toFlatOption(option) {
const getItems = (arr, cur) => {
const keys = Object.keys(cur);
const newObj = {};
const curChild = cur.children;
const hasChild = curChild && curChild.length > 0;
keys.forEach(key => key !== "children" && (newObj[key] = cur[key]));
arr.push(newObj);
return (hasChild ? curChild.reduce(getItems, arr) : arr);
};
this.flatOptions = option.reduce(getItems, []);
},
// 设置配置的字典
setOptionDicts(options) {
if (!Array.isArray(options)) {
const { label, value } = options;
this.optionDicts.push({ value, label });
const children = options.children;
if (children) {
this.setOptionDicts(children);
}
} else {
options.forEach(opt => {
this.setOptionDicts(opt);
});
}
},
// 触发 on-selected 事件
whenOutPut(value) {
// 根据选中的值数组 value 输出特定 outputType 类型
if (this.outputType !== "value") {
this.outputValue = this.getTypeOptions(value, this.outputType);
} else {
this.outputValue = value;
}
this.$emit("on-selected", this.outputValue);
},
// 设定层级
setLevel() {
const siblingValues = [];
let tempLevel = 0;
if (this.options.length) {
const addLevel = option => {
const optChild = option.children;
if (option.level === tempLevel) {
siblingValues.push(option.value);
}
if (optChild) {
optChild.forEach(opt => {
opt.level = option.level + 1;
addLevel(opt);
});
}
};
this.options.forEach(option => {
if (!option.level) {
option.level = 0;
tempLevel = option.level;
}
addLevel(option);
option.siblingValues = siblingValues;
});
}
},
showSecondLevel(item) {
this.activeItem = item;
}
}
};
</script>
<style lang='scss' scoped>
.vk-menu-item {
display: flex;
justify-content: space-between;
align-items: center;
list-style-type: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
outline: none;
padding: 8px 20px;
font-size: 14px;
width: 100%;
&:hover {
background-color: rgba(125,139,169,.1);
}
}
.multil-cascader{
width: 155px;
display: inline;
}
.multil-cascader:hover{
cursor: pointer;
}
</style>
三,multiContent.vue 该页面为递归的所有children的Li的显示,以及选中点击事件
<template lang="html">
<div class="popver-content">
<div class="multiCascader-multil-content" :style="contentStyle">
<ul class="multiCascader-multi-menu">
<li v-for="(item, index) of option"
:key="index"
style="border:1px solid transparent;"
:class="[ 'multiCascader-menu-item', { 'item-disabled': item.disabled }]"
@click="showNextLevel(item)">
<el-checkbox v-if="item.multiple != false" :disabled="item.disabled" v-model="item.checked" @change="checkChange(item)">{{ item.label }}</el-checkbox>
<span v-else>{{ item.label }}</span>
<i class="el-icon-arrow-right" v-show="item.children && item.children.length > 0"></i>
</li>
</ul>
</div>
<!-- 递归调用自身组件 -->
<muContent
@handleSelect="whenSelected"
:height="height"
:width="width"
v-if="(activeItem && activeItem.children) && (activeItem.children.length > 0)"
:selectedValues="selectedValues"
@handleOutPut="whenOutPut"
:disabledPair="disabledPair"
:option="activeItem.children" >
</muContent>
</div>
</template>
<script>
const vm = this;
import Vue from "vue";
export default {
name: "muContent",
props: {
option: {
type: Array,
default() {
return [];
}
},
// 被选中的值
selectedValues: {
type: Array,
default() {
return [];
}
},
// 设置的宽度
width: {
type: String,
default: ""
},
height: {
type: String,
default: ""
},
// 禁用字段
disabledPair: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
activeItem: "",
tempActiveItem: "",
contentStyle: {
width: "",
height: ""
},
checkArr: [],
checkDisabled: false
};
},
created() {
this.initData();
this.whenOutPut(this.selectedValues);
},
methods: {
// 逐级上传
whenOutPut(val) {
this.$emit("handleOutPut", val);
},
initData() {
const { width, height } = this;
this.contentStyle = Object.assign({}, this.contentStyle, {
width,
height
});
},
// 获取到选中的值
checkChange(item) {
const getCheckedItems = item => {
const { value, checked, level } = item;
if (checked && level) {
this.selectedValues.push(value);
} else if (!checked) {
item.disabled = false;
if (this.selectedValues.includes(value)) {
this.selectedValues.splice(
this.selectedValues.findIndex(slectVal => slectVal === value),
1
);
}
}
const itemChild = item.children;
if (itemChild) {
itemChild.forEach(child => (child.checked = checked));
}
};
this.recursiveFn(item, getCheckedItems);
this.disabeldAction(item);
this.activeItem = item;
this.$emit("handleSelect", this.option);
this.$emit("handleOutPut", this.selectedValues);
},
// 当二级菜单改变的时候
whenSelected(val) {
let allChildCancelChecked = true;
if (Array.isArray(val) && val.length > 0) {
allChildCancelChecked = val.every(child => child.checked === false);
}
this.activeItem.checked = !allChildCancelChecked;
this.disabeldAction(this.activeItem);
this.$emit("handleSelect", this.option);
},
// 递归函数
recursiveFn(curItem, cb) {
cb(curItem);
const children = curItem.children;
if (children && children.length > 0) {
children.forEach(item => {
this.recursiveFn(item, cb);
});
}
},
// 设置 disabled 值 values: 互斥的另一方数组, curItem 当前选中的值
setDisabled(exceptValues, curItem, values) {
const {
checked: curChecked,
childrenValues,
value: curValue,
siblingValues
} = curItem;
this.checkArr = [];
if (values.includes("all")) {
if (siblingValues) {
this.checkArr = new Array(
siblingValues.length - exceptValues.length
).fill(true);
}
} else {
this.checkArr = new Array(values.length).fill(true);
}
const getCheckArr = item => {
const { value, checked } = item;
if (!exceptValues.includes(value)) return;
this.checkArr.push(checked);
this.checkArr.shift();
};
const resetDistable = child => {
if (!values.includes(child.value)) return;
child.disabled = this.checkArr.some(val => val === true);
};
this.option.forEach(opt => {
this.recursiveFn(opt, getCheckArr);
});
this.option.forEach(opt => {
this.recursiveFn(opt, resetDistable);
});
},
// disabled action
// 根据选中的值进行设置是否可选
disabeldAction(item) {
const { thatPair, thisPair } = this.disabledPair;
if (!thatPair || !thisPair) {
return;
}
const pairs = [...thatPair, ...thisPair];
const { value: itemVal } = item;
const belongPair = pairs.includes(itemVal) || pairs.includes("all");
let distableValues = [];
let ableValues = [];
if (!belongPair) return;
if (
thisPair.includes(item.value) ||
(thisPair.includes("all") && !thatPair.includes(item.value))
) {
this.setDisabled(thisPair, item, thatPair);
return;
}
if (
thatPair.includes(item.value) ||
(thatPair.includes("all") && !thisPair.includes(item.value))
) {
this.setDisabled(thatPair, item, thisPair);
}
this.$emit("handleSelect", this.option);
this.disabeldAction(this.activeItem);
},
// 设置 disabled 值 values: 互斥的另一方数组, curItem 当前选中的值
setDisabled(exceptValues, curItem, values) {
const {
checked: curChecked,
childrenValues,
value: curValue,
siblingValues
} = curItem;
this.checkArr = [];
if (values.includes("all")) {
if (siblingValues) {
this.checkArr = new Array(
siblingValues.length - exceptValues.length
).fill(true);
}
} else {
this.checkArr = new Array(values.length).fill(true);
}
const toDisabled = item => {
const { value, checked } = item;
if (
values.includes(value) ||
(values.includes("all") && !exceptValues.includes(value))
) {
if (siblingValues && siblingValues.includes(value)) {
this.checkArr.push(checked);
this.checkArr.shift();
}
}
const itemChild = item.children;
if (itemChild && itemChild.length > 0) {
itemChild.forEach(child => {
toDisabled(child);
});
}
};
this.option.forEach(child => {
toDisabled(child);
});
this.option.forEach(child => {
if (
exceptValues.includes(child.value) ||
(exceptValues.includes("all") && !values.includes(child.value))
) {
child.disabled = this.checkArr.some(val => val === true);
}
});
},
// disabled action
// 根据选中的值进行设置是否可选
disabeldAction(item) {
const { thatPair, thisPair } = this.disabledPair;
if (!thatPair || !thisPair) {
return;
}
const pairs = [...thatPair, ...thisPair];
if (pairs.includes(item.value) || pairs.includes("all")) {
if (
thisPair.includes(item.value) ||
(thisPair.includes("all") && !thatPair.includes(item.value))
) {
this.setDisabled(thatPair, item, thisPair);
return;
}
if (
thatPair.includes(item.value) ||
(thatPair.includes("all") && !thisPair.includes(item.value))
) {
this.setDisabled(thisPair, item, thatPair);
}
}
},
//点击每一个列表的操作并且给下一个列表赋值
showNextLevel(item) {
//先清空,后赋值,否则会导致多级列表同时存在
this.activeItem = "";
if (item.disabled) return;
setTimeout(() => {
this.activeItem = item;
}, 10);
}
}
};
</script>
<style lang='scss' scoped>
.popver-content {
display: flex;
justify-content: space-between;
}
.multiCascader-multil-content {
display: inline-block;
max-height: 250px;
overflow-y: auto;
// border-right: 1px solid red;
}
.multiCascader-menu-item {
display: flex;
justify-content: space-between;
align-items: center;
list-style-type: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
outline: none;
padding: 8px 20px;
font-size: 14px;
&:hover {
background-color: rgba(125, 139, 169, 0.1);
}
}
.item-disabled {
color: #c0c4cc;
cursor: not-allowed;
}
</style>
接下来就到需要引用的页面了。
<template>
<div class="performanceBox">
<!-- 级联选择器多选 -->
<choiceindex v-on:CheckedsIndexCodes="FromTreeCheckeds"></choiceindex>
</div>
</template>
<script>
引用上边创建的MultipleChoice文件夹下的index出口文件就好了。
import choiceindex from "@/components/MultipleChoice/index"; //级联选择多选 完成
export default {
components: {
choiceindex,
},
data() {
return {
SaveCascadeIndexCodes: [], //保存级联选择器多选的基准code
SaveJiZhunParams: [], //保存业绩表现需要的参数
};
},
methods: {
//多选选择基准时的code
FromTreeCheckeds(IndexCodes) {
//IndexCodes就是选中的item的数组,操作他就好了
// console.log(IndexCodes);
this.SaveCascadeIndexCodes = IndexCodes;
},
}
};
</script>
<style rel="stylesheet/scss" lang="scss">
</style>
结束语
这个插件到此也就完成了,终于解决了这个深坑,希望能帮助到小伙伴们,有什么不足的大家多多提出宝贵的意见,共同探讨,进步。