Element UI 自定义下拉框组件

效果图

支持全选反选,及模糊搜索

组件源码

<template>
  <div>
    <el-select
        ref="subjectSelect"
        v-model="subjectNameIds"
        :disabled="hideSelect"
        style="width:100%"
        multiple
        :key="selectKeys"
        value-key="id"
        @click.native="subjectSelectClick"
        @change="subjectChange"
    >
      <el-option
          v-for="item in copy_subjectTypeList"
          :key="item.id"
          :label="item.name"
          :value="item.id"
      >
        {{ item.name }}
      </el-option>
    </el-select>
    <el-popover
        ref="popover"
        placement="bottom"
        trigger="manual"
        v-model="popShow"
    >
      <div class="close-btn">
        <el-button
            size="mini"
            type="primary"
            @click="subjectSelectClick"
        >关闭
        </el-button
        >
      </div>


      <div class="poper__head">
        <div class="checkBoxTitle">
          <el-checkbox
              :disabled="hideCheckBox"
              :indeterminate="isIndeterminate"
              v-model="checkAll"
              @change="handleCheckAllChange"
          >全选
          </el-checkbox
          >
        </div>
        <div>
          <el-input
              placeholder="请输入关键字模糊搜索"
              v-model.trim="subject_name"
              class="input-with-select"
              clearable
          >
            <el-button
                slot="append"
                icon="el-icon-search"
                @click="searchSubjectName"
            ></el-button>
          </el-input>
        </div>
      </div>
      <br/>
      <div class="popChild">
        <el-checkbox-group
            :disabled="hideCheckBox"
            v-model="checkedSubjects"
            @change="handleCheckedCitiesChange"
        >
          <el-checkbox
              v-for="item in copy_subjectTypeList"
              :label="item.id"
              :key="item.id"
          >{{ item.name }}
          </el-checkbox
          >
        </el-checkbox-group>
      </div>
    </el-popover>
  </div>
</template>




<script>
export default {
  props: {
    subjectTypeList: { //传入的可筛选数组
      type: Array,
      default: []
    },
    hideSelect: {
      type: Boolean,
      default: false
    },
    hideCheckBox: {
      type: Boolean,
      default: false
    },
    panelTitle: {
      type: String,
      default: "请选择数据"
    }
  },
  data() {
    return {
      alreadySelectList: [],//已经选中的数据,编辑时展示
      copy_subjectTypeList: [],//复制的可筛选数组,子组件不可直接更改父组件传来的值
      subjectNameIds: [],//当前选中的科目
      primeSubjectTypeList: [],
      selectKeys: 1,
      checkAll: false,
      isIndeterminate: false,
      checkedSubjects: [], //当前勾选的复选框
      subject_name: "",
      popShow: false
    };
  },


  watch: {
    subjectTypeList() {
      this.copy_subjectTypeList = this.subjectTypeList.concat();
      this.primeSubjectTypeList = this.subjectTypeList.concat();
    }
  },


  created() {
  },


  mounted() {
    this.initSelectAndPanel();
    this.$refs.popover.$refs.popper.style.width = this.$el.clientWidth + "px";
  },


  methods: {


    //子组件需要每次操作后向父级传递选中科目
    emitParent() {
      this.$emit("selectData", this.subjectNameIds);
    },


    //初始化当前下拉框和复选框
    initSelectAndPanel() {
      this.subject_name = "";
      this.checkAll = false;
      this.isIndeterminate = false;
      this.popShow = false;
      this.checkedSubjects = [];
      this.subjectNameIds = [];
      this.copy_subjectTypeList = [];
      this.primeSubjectTypeList = [];
      this.updateSelectCompoment([]);
    },


    //设置当前已经选中的数据,仅作展示
    // (当前逻辑编辑时不可修改,如需编辑时可修改需要变动当前页面逻辑和调用逻辑)
    setSelfAlreadySelect(list) {
      this.subjectNameIds = list;
      let test = this.copy_subjectTypeList.filter(item => {
        return item.id == list[0];
      });
      this.$forceUpdate();
    },


    subjectChange(val) {
      this.checkedSubjects = val;
      this.handleCheckedCitiesChange(this.subjectNameIds);
    },
    //点击下拉框时打开pop框
    subjectSelectClick() {
      if (!this.hideSelect) {
        this.subject_name = "";
        //只有新增时才可显示
        this.$refs.subjectSelect.blur();
        this.popShow = !this.popShow;
      }
    },
    //每此点击复选框的时候判断:全选按钮显示逻辑和下拉框选项合并逻辑
    handleCheckedCitiesChange(value) {
      this.subjectNameIds = value;
      //如果当前下拉框里的列表选项已被全部选择: 全选为true,否则为false
      let ids = this.subjectNameIds.sort();
      let list = this.copy_subjectTypeList.sort((a, b) => {
        return a.id > b.id;
      });
      if (
          value.length > 0 &&
          (value.length > this.copy_subjectTypeList.length ||
              value.length == this.copy_subjectTypeList.length)
      ) {


        let temp = 0;
        for (let i = 0; i < ids.length; i++) {
          let Index = list.findIndex((item) => {
            return item.id === ids[i];
          });
          temp = Index >= 0 ? (temp += 1) : temp;
        }
        // console.log("相同的数量", temp);
        if (temp > 0) {
          if (list.length == temp) {
            this.checkAll = true;
            this.isIndeterminate = false;
          }
          else {
            this.checkAll = false;
            this.isIndeterminate = true;
          }
        }
        else {
          this.checkAll = false;
        }
      }
      else if (
          value.length > 0 &&
          value.length < this.copy_subjectTypeList.length
      ) {
        //判断剩余的选项里有咩有列表里的元素
        let temp = 0;
        for (let i = 0; i < ids.length; i++) {
          let Index = list.findIndex((item) => {
            return item.id === ids[i];
          });
          temp = Index >= 0 ? (temp += 1) : temp;
        }
        this.isIndeterminate = temp > 0 ? true : false;
      }
      else {
        // console.log("选项里没有数据");
        this.isIndeterminate = false;
        this.checkAll = false;
      }
      this.emitParent();
      this.$forceUpdate();
    },
    //模糊搜索科目,每次都从primeSubjectTypeList列表里搜
    async searchSubjectName() {
      if (this.subject_name != "") {
        this.copy_subjectTypeList = this.fuzzySearch(
            this.subject_name,
            this.primeSubjectTypeList
        );
        if (this.copy_subjectTypeList.length == 0) {
          this.isIndeterminate = false;
          this.checkAll = false;
        }
        else {
          this.checkSubjectList();
        }
      }
      else {
        this.copy_subjectTypeList = this.primeSubjectTypeList; //直接显示原始数据,不需要再次向服务器请求
        this.checkSubjectList();
      }
    },
    //每次搜索出结果进行比对:检测两个数组显示反选全选的显示
    checkSubjectList() {
      //需要判断当前已选择的和下拉框里的数组进行比较,用来显示全选按钮的状态
      let ids = this.subjectNameIds.sort();
      let list = this.copy_subjectTypeList.sort();


      let temp = 0;
      for (let i = 0; i < ids.length; i++) {
        let Index = list.findIndex((item) => {
          return item.id === ids[i];
        });
        temp = Index >= 0 ? (temp += 1) : temp;
      }
      //默认将两个bool设置成false,根据结果设置不同显示
      this.isIndeterminate = false;
      this.checkAll = false;
      if (ids.length > list.length || ids.length == list.length) {
        if (temp > 0 && list.length == temp) {
          // console.log("完全包含列表里所有选项");
          this.checkAll = true;
        }
        else if (temp > 0 && list.length != temp) {
          // console.log("不完全包含列表里的选项");
          this.isIndeterminate = true;
        }
      }
      else {
        if (temp > 0) {
          this.isIndeterminate = true;
        }
      }
    },
    fuzzySearch(str, container) {
      var newList = [];
      var startChar = str.charAt(0);
      var strLen = str.length;
      for (var i = 0; i < container.length; i++) {
        var obj = container[i];
        var isMatch = false;
        for (var p in obj) {
          var curItem = "";
          if (obj[p] != null) {
            curItem = obj[p];
          }
          for (var j = 0; j < curItem.length; j++) {
            if (curItem.charAt(j) == startChar) {
              if (curItem.substring(j).substring(0, strLen) == str) {
                isMatch = true;
                break;
              }
            }
          }
        }
        if (isMatch) {
          newList.push(obj);
        }
      }
      return newList;
    },
    handleCheckAllChange(val) {
      let ids = this.copy_subjectTypeList.map((item) => {
        return item.id;
      });
      //如果是全选需要将已保存的和下拉框里的合并去重
      //如果是反选,需要将已选的数组进行判断是否有下拉框里的数组,有的话直接移除
      // console.log("当前是否是全选", this.checkAll);
      // console.log("当前下拉框里的数组", ids);


      if (this.checkAll) {
        //去重显示合并后的数组
        let newArr = [];
        let arr = this.subjectNameIds.concat(ids).sort();
        for (let i in arr) {
          if (newArr.indexOf(arr[i]) < 0) {
            newArr.push(arr[i]);
          }
        }
        // console.log("去重显示合并后的数组", newArr);
        this.subjectNameIds = newArr;
        this.checkedSubjects = newArr;
        this.isIndeterminate = false;
      }
      else {
        let temp = [];
        for (let i in this.subjectNameIds) {
          if (ids.indexOf(this.subjectNameIds[i]) < 0) {
            temp.push(this.subjectNameIds[i]);
          }
        }
        // console.log("不相同的元素", temp);
        this.subjectNameIds = temp;
        //如果是取消全选,清空下拉框的数据,再重新渲染下拉组件,否则下拉框不显示选项文字
        this.updateSelectCompoment(temp);
        if (this.subject_name != "") {
          this.copy_subjectTypeList = this.primeSubjectTypeList;
          this.subject_name = "";
          this.handleCheckedCitiesChange(this.subjectNameIds);
        }
        if (temp.length == 0) {
          this.isIndeterminate = false;
        }
        this.checkedSubjects = temp;
      }
      this.emitParent();
      this.$forceUpdate();
      // console.log('当前选中的科目提交给父级',this.subjectNameIds)
    },


    updateSelectCompoment(temp) {
      if (this.$refs.subjectSelect) {
        this.$refs.subjectSelect.cachedOptions = temp;
        this.$refs.subjectSelect.selected = temp;
        this.selectKeys += 1;
        this.$forceUpdate();
      }
    }
  }
};
</script>




<style scoped>
.close-btn {
  direction: rtl;
  padding-bottom: 15px;
}


.poper__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  flex-direction: row;
}


.popChild {
  max-height: 300px;
  overflow-y: scroll;
}


/deep/ .el-select__tags {
  max-height: 400px;
  overflow-y: auto;
}


/deep/ .el-checkbox {
  line-height: 26px;
}




/deep/ .el-checkbox-group {
  display: flex;
  flex-direction: column;
}




/deep/ .el-tag.el-tag--info {
  max-width: 540px;
  height: auto;
}




/deep/ .el-select__tags-text {
  max-width: 540px;
  white-space: normal;
}




/deep/ .el-input__inner {
  height: auto;
}


.checkBoxTitle {
  font-size: 20px;
}
</style>

使用方式

调用该组件的脚本:
需要手动修改下拉框展示样式,否则下拉框和pop框宽度不一致。
如果是编辑时需要展示数据,则调用this.$refs.xxxx.setSelfAlreadySelect(list) ,传入要展示的数据id数组

<template>
    <div class="app-container">
        <el-form inline>
            <el-form-item label-width="80" label="测试">
                <SelectComponent style="width: 380px" :subjectTypeList="list" @selectData="selectData"/>
            </el-form-item>
        </el-form>
    </div>
</template>

<script>
import SelectComponent from './select-pop.vue'

export default {
  name: 'index2',
  components: { SelectComponent },
  data(){
    return{
      list:[]
    }
  },

  created() {},

  mounted() {
    this.list = [
     {id:1,name:'测试'},
     {id:2,name:'测试1'},
     {id:3,name:'测试2'},
     {id:4,name:'测试3'},
     {id:5,name:'测试4'},
     {id:6,name:'测试44'},
     {id:7,name:'测试45'},
     {id:8,name:'测试46'},
     {id:9,name:'测试47'},
     {id:10,name:'测试48'},
     {id:11,name:'试测试测试测试测试测试49'},
    ]
  },




  methods:{
    selectData(list){
      console.log('子组件返回的选项',list)
    }
  }
}
</script>




<style scoped>


</style>