重大更新
最新版本的element已經(jīng)有級聯(lián)多選功能了
前沿
吐槽一下,程序猿最不愿聽到的話之一,(人家某某網(wǎng)站就做出來了,你怎么做不出來,簡直喪心病狂)小編最近一直在開發(fā)基于vue-elementui的pc端項目,就碰到了來自產(chǎn)品的這句話,都有種拿起顯示器了。不過吐槽歸吐槽,項目還是要寫的。。。。。。在本項目中產(chǎn)品提的一個需求,就是人家某某網(wǎng)站上有的,而element-ui上沒有,那就是Cascader 級聯(lián)選擇器,element-ui只支持單選,于是就開始了折騰,再折騰了快一周的時間吧,還是沒搞出來,最后由于項目著急上線,只能暫時先放棄,所以就先擱置了,后來幸得于空,于是乎又是開始折騰,畢竟也是自己的問題。哎,不說了,show time.
該多選菜單基于 element-ui 的Cascader 層級菜單, 但是在我的一番折騰下開發(fā)出一套支持多選的,有禁用狀態(tài),以及靈活控制選幾個,適應產(chǎn)品的奇葩需求,Cascader 層級菜單。羞于一提的是,我折騰了整整3天,才搞出來。在這里把我的心路歷程記錄下來,里邊的注釋寫的個人感覺都挺全的,有不明白的也可以與我交流,共同探討,方便后續(xù)學習與擴展。
先上個效果圖
微信圖片_20181012184700.png
現(xiàn)附上該插件的菜單配置項,以方便后期維護
attributes屬性說明
| 屬性名 | 描述 | 類型 | 默認值 |
|---|---|---|---|
| width | 菜單選擇面板的寬度 | String | 220px |
| height | 菜單選擇面板的高度 | String | 240px |
| options | 選擇器菜單數(shù)據(jù)配置項 | Array | [] |
| inputValue | 選擇器輸入框內顯示的內容 | String | 220px |
| outputType | 選中項輸出的字段名,outputType 用于輸出選中選擇項對象的某一字段, 默認值: value, 當傳入 outputType 為item時, 輸出選中這一項的對象(不包括 children 屬性); | String | value |
| disabledPair | 互斥選項對兒,就是選擇一個其他的就被禁用 | Object | -- |
事件名稱
| 事件名稱 | 說明 | 回調參數(shù) |
|---|---|---|
| on-selected | 選擇器中的某一項被選中的時候觸發(fā)的事件 | 數(shù)組,數(shù)組內包含被選中的值 |
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 此頁面為主要出口文件,會發(fā)射出一個得到選中后的item的方法以及數(shù)組。
<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的時候的操作 在這個方法內靈活判斷多選的狀態(tài)以及禁用狀態(tài)
getSelected(val) {
let strnum = val.length;
console.log(val);
// 當選中的指數(shù)大于1并且小于10的時候讓所有的指數(shù)都可以選擇(沒有禁用狀態(tài))
if (val.length > 1 && val.length < 10) {
this.LessThanThen(this.configOptions);
}
// 必須保留一個選中的
if (val.length == 1) {
let moreOne = val[0];
this.LessThanMoreOne(this.configOptions, moreOne);
}
// 當選中的指數(shù)大于10的時候讓除選中的之外的指數(shù)都變?yōu)榻脿顟B(tài)
if (val.length >= 10) {
let moreOne = val;
this.LessThanMoreTen(this.configOptions, moreOne);
}
if (strnum !== this.commonLength) {
//將選中后的數(shù)組暴漏出去,在需要的頁面使用
this.$emit("CheckedsIndexCodes", val);
}
this.commonLength = val.length;
// 勿刪后期需求改變會用
// this.selectGroups = val;
// this.configTips = `已選擇${val.length}個分組`;
},
// 此遞歸為當選中的指數(shù)大于10的時候讓除選中的之外的指數(shù)都變?yōu)榻脿顟B(tài)
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]);
//每次在傳入父節(jié)點的childreg去查找,自己調用自己的方法
this.getArrayList(datas[i].children);
}
}
},
MulitGetlistBenchmark() {
//此接口為我們項目中的接口,上邊有數(shù)據(jù)模板,可根據(jù)數(shù)據(jù)模板來寫數(shù)據(jù)。
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的字模板,并且該頁面會接受引用頁面?zhèn)鬟^來的數(shù)據(jù),方便靈活控制尺寸,數(shù)據(jù),是否禁用等的狀態(tài)。
<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: "",
// 展開之后的數(shù)組, 將每一個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";
},
// 初始化數(shù)據(jù) 對于每一項 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);
});
}
},
// 觸發(fā) on-selected 事件
whenOutPut(value) {
// 根據(jù)選中的值數(shù)組 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);
},
// 遞歸函數(shù)
recursiveFn(curItem, cb) {
cb(curItem);
const children = curItem.children;
if (children && children.length > 0) {
children.forEach(item => {
this.recursiveFn(item, cb);
});
}
},
// 設置 disabled 值 values: 互斥的另一方數(shù)組, 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
// 根據(jù)選中的值進行設置是否可選
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: 互斥的另一方數(shù)組, 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
// 根據(jù)選中的值進行設置是否可選
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">
<!-- 級聯(lián)選擇器多選 -->
<choiceindex v-on:CheckedsIndexCodes="FromTreeCheckeds"></choiceindex>
</div>
</template>
<script>
引用上邊創(chuàng)建的MultipleChoice文件夾下的index出口文件就好了。
import choiceindex from "@/components/MultipleChoice/index"; //級聯(lián)選擇多選 完成
export default {
components: {
choiceindex,
},
data() {
return {
SaveCascadeIndexCodes: [], //保存級聯(lián)選擇器多選的基準code
SaveJiZhunParams: [], //保存業(yè)績表現(xiàn)需要的參數(shù)
};
},
methods: {
//多選選擇基準時的code
FromTreeCheckeds(IndexCodes) {
//IndexCodes就是選中的item的數(shù)組,操作他就好了
// console.log(IndexCodes);
this.SaveCascadeIndexCodes = IndexCodes;
},
}
};
</script>
<style rel="stylesheet/scss" lang="scss">
</style>
結束語
這個插件到此也就完成了,終于解決了這個深坑,希望能幫助到小伙伴們,有什么不足的大家多多提出寶貴的意見,共同探討,進步。
