1.安装插件
npm install vue-virtual-scroll-list
2.封装组件
1> 新建文件夹 VirtualSelect
创建文件:index.vue,OptionNode.vue
/VirtualSelect/index.vue
<template> <div> <el-select size="mini" popper-class="virtualselect" class="virtual-select-custom-style" :value="defaultValue" filterable :filter-method="filterMethod" default-first-option :placeholder="placeholder" :disabled="disabled" :multiple="isMultiple" :collapse-tags="collapseTags" :allow-create="allowCreate" @visible-change="visibleChange" v-on="$listeners" v-bind="$attrs" @clear="clearChange" @focus="focusChange" > <!--clearable 清除按钮 --> <!-- 使用插件 --> <virtual-list ref="virtualList" class="virtualselect-list" :data-key="value" :data-sources="selectArr" :data-component="itemComponent" :keeps="keepsParams" :extra-props="{ label: label, value: value, isRight: isRight, isConcat: isConcat, concatSymbol: concatSymbol, concatPropArr: concatPropArr, isDifferent: isDifferent, selectOptionWidth: selectOptionWidth }" ></virtual-list> </el-select> </div> </template> <script> const validatenull = (val) => { if (typeof val === 'boolean') { return false } if (typeof val === 'number') { return false } if (val instanceof Array) { if (val.length === 0) return true } else if (val instanceof Object) { if (JSON.stringify(val) === '{}') return true } else { if ( val === 'null' || val === null || val === 'undefined' || val === undefined || val === '' ) return true return false } return false } // 引入插件 import virtualList from 'vue-virtual-scroll-list' import OptionNode from './OptionNode.vue' export default { name: 'VirtualSelect', components: { // 注册插件 'virtual-list': virtualList }, model: { prop: 'defaultValue', event: 'change' }, props: { // 数组 list: { type: Array, default() { return [] } }, // 显示名称 label: { type: String, default: '' }, // 标识 value: { type: String, default: '' }, // 是否拼接字段 isConcat: { type: Boolean, default: false }, // 拼接字段的符号 concatSymbol: { type: String, default: ' | ' }, // 要拼接的字段 concatPropArr: { type: Array, default() { return [] } }, // 反显是否与下拉选项不同 isDifferent: { type: Boolean, default: false }, // 显示右边 isRight: { type: Boolean, default: false }, // 加载条数 keepsParams: { type: Number, default: 10 }, // 绑定值 v-model defaultValue: { type: [Number, String, Array] }, // 是否多选 isMultiple: { type: Boolean, default: false }, // 多选折叠标签 collapseTags: { type: Boolean, default: false }, // 输入框占位文本 placeholder: { type: String, default: '请选择' }, // 是否禁用 disabled: { type: Boolean, default: false }, // 是否允许创建条目 allowCreate: { type: Boolean, default: false } }, data() { return { itemComponent: OptionNode, selectArr: [], selectOptionWidth: null, // 下拉框宽度 dataList: [] } }, watch: { list() { this.init() }, defaultValue: { handler(val) { if (validatenull(val)) this.clearChange() this.init() }, immediate: false, deep: true } }, mounted() { this.init() }, methods: { // 初始化 init() { this.list.forEach((item) => { if (validatenull(item[this.label])) { item[this.label] = item[this.value] } }) this.dataList = this.list if (!this.defaultValue || this.defaultValue?.length === 0) { this.selectArr = this.dataList } else { // 回显问题 // 由于只渲染固定keepsParams(10)条数据,当默认数据处于10条之外,在回显的时候会显示异常 // 解决方法:遍历所有数据,将对应回显的那一条数据放在第一条即可 this.selectArr = JSON.parse(JSON.stringify(this.dataList)) let obj = {} if (!this.isMultiple) { if (this.allowCreate) { const arr = this.selectArr.filter((val) => { return val[this.value] === this.defaultValue }) if (arr.length === 0) { const item = {} item[this.value] = this.defaultValue item[this.label] = this.defaultValue item.allowCreate = true this.selectArr.push(item) this.$emit('selChange', item) } else { this.$emit('selChange', arr[0]) } } // 单选 for (let i = 0; i < this.selectArr.length; i++) { const element = this.selectArr[i] if ( element[this.value]?.toString()?.toLowerCase() === this.defaultValue?.toString()?.toLowerCase() ) { obj = element this.selectArr?.splice(i, 1) break } } this.selectArr?.unshift(obj) } else if (this.isMultiple) { if (this.allowCreate) { this.defaultValue?.map((v) => { const arr = this.selectArr.filter((val) => { return val[this.value] === v }) if (arr?.length === 0) { const item = {} item[this.value] = v item[this.label] = v item.allowCreate = true this.selectArr.push(item) this.$emit('selChange', item) } else { this.$emit('selChange', arr[0]) } }) } // 多选 for (let i = 0; i < this.selectArr.length; i++) { const element = this.selectArr[i] this.defaultValue?.map((val) => { if ( element[this.value]?.toString()?.toLowerCase() === val?.toString()?.toLowerCase() ) { obj = element this.selectArr?.splice(i, 1) this.selectArr?.unshift(obj) } }) } } } }, // 搜索 filterMethod(query) { if (!validatenull(query?.trim())) { this.$refs.virtualList.scrollToIndex(0) // 滚动到顶部 setTimeout(() => { const orgArr = this.dataList.filter((item) => { let result = item[this.label] ?.toLowerCase() ?.indexOf(query?.trim()?.toLowerCase()) > -1 if (this.isRight || this.isConcat) { const concatPropArr = validatenull(this.concatPropArr) ? [this.label, this.value] : this.concatPropArr const checkResArr = concatPropArr.map((prop) => { return ( item[prop] ?.trim() ?.toLowerCase() ?.indexOf(query?.trim()?.toLowerCase()) > -1 ) }) result = checkResArr.some((val) => val) } return result }) this.selectArr = this.handleSort(orgArr) || [] }, 100) } else { setTimeout(() => { this.init() }, 100) } }, // 按拼音字母数字顺序排序 handleSort(arr) { const sortArr = arr.sort((a, b) => { const reg = /[a-zA-Z0-9]/ const x = a[this.label], y = b[this.label] if (reg.test(x) || reg.test(y)) { if (x > y) { return 1 } else if (x < y) { return -1 } else { return 0 } } else { return x.localeCompare(y) } }) return sortArr }, visibleChange(bool) { if (!bool) { this.$refs.virtualList.reset() this.init() } }, clearChange() { this.visibleChange(false) }, focusChange(event) { this.setOptionWidth(event) }, setOptionWidth(event) { // 下拉框弹出时,设置弹框的宽度 this.$nextTick(() => { this.selectOptionWidth = event.srcElement.offsetWidth + 'px' if (this.isMultiple) { this.selectOptionWidth = event.srcElement.offsetParent.clientWidth + 25 + 'px' } }) } } } </script> <style lang="scss" scoped> // .virtualselect ::v-deep .el-select-dropdown__item { // // .virtual-select-custom-style ::v-deep .el-select-dropdown__item { // // 设置最大宽度,超出省略号,鼠标悬浮显示 // // options 需写 :title="source[label]" // // width: 250px; // // display: inline-block; // // overflow: hidden; // // text-overflow: ellipsis; // // white-space: nowrap; // // z-index: 9; // } .virtualselect { // 设置最大高度 max-height: 245px; overflow-y: auto; // &-list { // max-height:245px; // overflow-y:auto; // } } .virtualselect-list { max-height: 245px; overflow-y: auto; } .el-select { width: 100%; } ::-webkit-scrollbar { width: 6px; height: 6px; background-color: transparent; cursor: pointer; margin-right: 5px; } ::-webkit-scrollbar-thumb { background-color: rgba(144, 147, 153, 0.3) !important; border-radius: 3px !important; } ::-webkit-scrollbar-thumb:hover { background-color: rgba(144, 147, 153, 0.5) !important; } ::-webkit-scrollbar-track { background-color: transparent !important; border-radius: 3px !important; -webkit-box-shadow: none !important; } ::v-deep .el-select__tags { flex-wrap: unset; overflow: auto; } </style> <style> .virtualselect .el-select-dropdown__item { display: block; /* max-width: 350px; */ overflow: visible; } .virtualselect .el-scrollbar__bar.is-vertical { width: 0px; } </style>
/VirtualSelect/OptionNode.vue
<template> <el-option :key="label + value" :label="isDifferent ? source[label] : handleConcat()" :value="source[value]" :disabled="source.disabled" :title="handleConcat()" :style="{ width: selectOptionWidth, 'min-width': '200px' }" > <span>{{ handleConcat() }}</span> <span v-if="isRight" style="float: right; color: #939393">{{ source[value] }}</span> </el-option> </template> <script> const validatenull = (val) => { if (typeof val === 'boolean') { return false } if (typeof val === 'number') { return false } if (val instanceof Array) { if (val.length === 0) return true } else if (val instanceof Object) { if (JSON.stringify(val) === '{}') return true } else { if ( val === 'null' || val === null || val === 'undefined' || val === undefined || val === '' ) return true return false } return false } export default { name: 'OptionNode', props: { // 每一行的索引 index: { type: Number, default: 0 }, // 每一行的内容 source: { type: Object, default() { return {} } }, // 需要显示的名称 label: { type: String, default: '' }, // 绑定的值 value: { type: String, default: '' }, // 是否拼接字段 isConcat: { type: Boolean, default: false }, // 拼接字段的符号 concatSymbol: { type: String, default: ' | ' }, // 要拼接的字段 concatPropArr: { type: Array, default() { return [] } }, // 反显是否与下拉选项不同 isDifferent: { type: Boolean, default: false }, // 右侧是否显示绑定的值 isRight: { type: Boolean, default() { return false } }, selectOptionWidth: { type: String, default: '350px' } }, methods: { handleConcat() { let result = this.source[this.label] if (this.isConcat) { const concatPropArr = validatenull(this.concatPropArr) ? [this.label, this.value] : this.concatPropArr result = concatPropArr .map((item) => { return this.source[item] }) .filter((val) => !validatenull(val)) .join(this.concatSymbol) } return result }, concatString(a, b) { a = a || '' b = b || '' if (this.isConcat) { return a + (a && b ? this.concatSymbol : '') + b } return a } } } </script>
2. 组件内使用 ,当前使用的是单选,多选绑定为空数组[]
index.vue
<template> <div> <!-- 虚拟滚动下拉框:label:标题,value:值 label和value:可设置为后端返回得字段名称 keeps-params:单次加载得条数 is-multiple:是否多选 list:绑定接口请求的数据list --> <virtual-select v-model="values" :list="optionsList" label="label" value="value" :placeholder-params="'请选择'" :keeps-params="10" :is-multiple="false" @change="handleChange" /> </div> </template> <script> import VirtualSelect from './VirtualSelect.vue' export default { name: 'index', components: { VirtualSelect }, data() { return { values:'',//绑定的值 optionsList:[ // { // label: '选项1', // value: 1 // }, // { // label: '选项2', // value: 2 // }, // { // label: '选项3', // value: 3 // }, // ...... ] } }, mounted() { // 模拟数据 this.getData() }, methods: { getData(){ // 模拟三万条数据 let testData=[] for (let i = 0; i < 30000; i++) { testData.push({ label: '选项'+i, value: i }) } // 设置数据 this.optionsList = testData }, // 监听 handleChange(value) { console.log(value)// 这里拿到的是选中的值 }, } } </script>