美文网首页
vue-element-ui-Cascader 级联选择器支持多

vue-element-ui-Cascader 级联选择器支持多

作者: 郝艳峰Vip | 来源:发表于2018-11-01 14:07 被阅读0次

重大更新

最新版本的element已经有级联多选功能了

前沿


吐槽一下,程序猿最不愿听到的话之一,(人家某某网站就做出来了,你怎么做不出来,简直丧心病狂)小编最近一直在开发基于vue-elementui的pc端项目,就碰到了来自产品的这句话,都有种拿起显示器了。不过吐槽归吐槽,项目还是要写的。。。。。。在本项目中产品提的一个需求,就是人家某某网站上有的,而element-ui上没有,那就是Cascader 级联选择器,element-ui只支持单选,于是就开始了折腾,再折腾了快一周的时间吧,还是没搞出来,最后由于项目着急上线,只能暂时先放弃,所以就先搁置了,后来幸得于空,于是乎又是开始折腾,毕竟也是自己的问题。哎,不说了,show time.

该多选菜单基于 element-ui 的Cascader 层级菜单, 但是在我的一番折腾下开发出一套支持多选的,有禁用状态,以及灵活控制选几个,适应产品的奇葩需求,Cascader 层级菜单。羞于一提的是,我折腾了整整3天,才搞出来。在这里把我的心路历程记录下来,里边的注释写的个人感觉都挺全的,有不明白的也可以与我交流,共同探讨,方便后续学习与扩展。
先上个效果图


微信图片_20181012184700.png

现附上该插件的菜单配置项,以方便后期维护

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">基准&nbsp;:</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>

结束语


这个插件到此也就完成了,终于解决了这个深坑,希望能帮助到小伙伴们,有什么不足的大家多多提出宝贵的意见,共同探讨,进步。

相关文章

网友评论

      本文标题:vue-element-ui-Cascader 级联选择器支持多

      本文链接:https://www.haomeiwen.com/subject/jucxxqtx.html