赞
踩
本组件基于下文代码有所改动:element 下拉数据过多,导致列表卡顿加载慢,使用虚拟列表方式_element下拉框数据过多_风掠过有空白的博客-CSDN博客
一、问题描述
表单中某下拉框,由于数据过多,选择的时候会因为数据量过大导致页面卡顿,于是对于el-select进行二次封装,实现虚拟滚动。
二、实现如下:
可把代码拉下来查看:vue-demo: vueDemo (https://gitee.com/kyang00/vue-demo.git)
看起来是加载了全部数据,实际上只加载了自己设定的10条(可修改)数据。
Step1: npm install vue-virtual-scroll-list --save
Step2: 创建两个文件
一、Select.vue
- <template>
- <div>
- <el-select
- popper-class="virtualselect"
- class="virtual-select-custom-style"
- :popper-append-to-body="false"
- :value="defaultValue"
- filterable
- :filter-method="filterMethod"
- default-first-option
- clearable
- :placeholder="placeholderParams"
- :multiple="isMultiple"
- :allow-create="allowCreate"
- @visible-change="visibleChange"
- v-on="$listeners"
- @clear="clearChange"
- >
- <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
- }"
- ></virtual-list>
- </el-select>
- </div>
- </template>
- <script>
- import {
- validatenull
- } from '@/utils'
- import virtualList from 'vue-virtual-scroll-list'
- import ElOptionNode from './el-option-node'
- export default {
- components: {
- 'virtual-list': virtualList
- },
- model: {
- prop: 'bindValue',
- event: 'change'
- },
- props: {
- // // 父组件传的值
- // selectData: {
- // type: Object,
- // default() {
- // return {}
- // }
- // },
- // 数组
- list: {
- type: Array,
- default() {
- return []
- }
- },
- // 显示名称
- label: {
- type: String,
- default: ''
- },
- // 标识
- value: {
- type: String,
- default: ''
- },
- // 是否拼接label | value
- isConcat: {
- type: Boolean,
- default: false
- },
- // 拼接label、value符号
- concatSymbol: {
- type: String,
- default: ' | '
- },
- // 显示右边
- isRight: {
- type: Boolean,
- default: false
- },
- // 加载条数
- keepsParams: {
- type: Number,
- default: 10
- },
- // 绑定的默认值
- bindValue: {
- type: [String, Array],
- default() {
- if (typeof this.bindValue === 'string') return ''
- return []
- }
- },
- // 是否多选
- isMultiple: {
- type: Boolean,
- default: false
- },
- placeholderParams: {
- type: String,
- default: '请选择'
- },
- // 是否允许创建条目
- allowCreate: {
- type: Boolean,
- default: false
- }
- },
- data() {
- return {
- itemComponent: ElOptionNode,
- selectArr: [],
- defaultValue: null // 绑定的默认值
- }
- },
- watch: {
- 'list'() {
- this.init()
- },
- bindValue: {
- handler(val, oldVal) {
- this.defaultValue = this.bindValue
- if (validatenull(val)) this.clearChange()
- this.init()
- },
- immediate: false,
- deep: true
- }
- },
- mounted() {
- this.defaultValue = this.bindValue
- this.init()
- },
- methods: {
- init() {
- if (!this.defaultValue || this.defaultValue?.length === 0) {
- this.selectArr = this.list
- } else {
- // 回显问题
- // 由于只渲染固定keepsParams(10)条数据,当默认数据处于10条之外,在回显的时候会显示异常
- // 解决方法:遍历所有数据,将对应回显的那一条数据放在第一条即可
- this.selectArr = JSON.parse(JSON.stringify(this.list))
- if (typeof this.defaultValue === 'string' && !this.isMultiple) {
- let obj = {}
- if (this.allowCreate) {
- const arr = this.selectArr.filter(val => {
- return val[this.value] === this.defaultValue
- })
- if (arr.length === 0) {
- const item = {}
- // item[this.value] = `Create-${this.defaultValue}`
- 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]?.toLowerCase() === this.defaultValue?.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] = `Create-${v}`
- 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]?.toLowerCase() === val?.toLowerCase()) {
- obj = element
- this.selectArr?.splice(i, 1)
- this.selectArr?.unshift(obj)
- }
- })
- }
- }
- }
- },
- // 搜索
- filterMethod(query) {
- if (!validatenull(query?.trim())) {
- this.$refs.virtualList.scrollToIndex(0) // 滚动到顶部
- setTimeout(() => {
- this.selectArr = this.list.filter(item => {
- return this.isRight || this.isConcat
- ? (item[this.label].trim()?.toLowerCase()?.indexOf(query?.trim()?.toLowerCase()) > -1 || item[this.value]?.toLowerCase()?.indexOf(query?.trim()?.toLowerCase()) > -1)
- : item[this.label]?.toLowerCase()?.indexOf(query?.trim()?.toLowerCase()) > -1
- })
- }, 100)
- } else {
- setTimeout(() => {
- this.init()
- }, 100)
- }
- },
- visibleChange(bool) {
- if (!bool) {
- this.$refs.virtualList.reset()
- this.init()
- }
- },
- clearChange() {
- if (typeof this.defaultValue === 'string') {
- this.defaultValue = ''
- } else if (this.isMultiple) {
- this.defaultValue = []
- }
- this.visibleChange(false)
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .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;
- }
- .virtualselect {
- // 设置最大高度
- &-list {
- max-height:245px;
- overflow-y:auto;
- }
- }
- ::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- background-color: transparent;
- cursor: pointer;
- margin-right: 5px;
- }
- ::-webkit-scrollbar-thumb {
- background-color: rgba(144,147,153,.3) !important;
- border-radius: 3px !important;
- }
- ::-webkit-scrollbar-thumb:hover{
- background-color: rgba(144,147,153,.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>

二、el-option-node.vue
- <template>
- <el-option
- :key="label+value"
- :label="concatString(source[label], source[value])"
- :value="source[value]"
- :disabled="source.disabled"
- :title="concatString(source[label], source[value])"
- >
- <span>{{ concatString(source[label], source[value]) }}</span>
- <span
- v-if="isRight"
- style="float:right;color:#939393"
- >{{ source[value] }}</span>
- </el-option>
- </template>
- <script>
- export default {
- name: 'ItemComponent',
- props: {
- // 每一行的索引
- index: {
- type: Number,
- default: 0
- },
- // 每一行的内容
- source: {
- type: Object,
- default() {
- return {}
- }
- },
- // 需要显示的名称
- label: {
- type: String,
- default: ''
- },
- // 绑定的值
- value: {
- type: String,
- default: ''
- },
- // 是否拼接label | value
- isConcat: {
- type: Boolean,
- default: false
- },
- // 拼接label、value符号
- concatSymbol: {
- type: String,
- default: ' | '
- },
- // 右侧是否显示绑定的值
- isRight: {
- type: Boolean,
- default() {
- return false
- }
- }
- },
- methods: {
- concatString(a, b) {
- a = a || ''
- b = b || ''
- if (this.isConcat) {
- // return a + ((a && b) ? ' | ' : '') + b
- return a + ((a && b) ? this.concatSymbol : '') + b
- }
- return a
- }
- }
- }
- </script>
-

三、utils index.js
- /**
- * 判断是否为空
- */
- export function 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;
- }

四、组件使用
- <template>
- <div>
- <virtual-select
- v-model="selectValue"
- :list="selectValueOptions"
- label="name"
- value="code"
- :placeholder-params="'请选择(单选)'"
- :keeps-params="10"
- :is-concat="true"
- :concat-symbol="' || '"
- :is-multiple="false"
- @change="changeVal"
- />
- <virtual-select
- v-model="selectValue2"
- :list="selectValueOptions"
- label="name"
- value="code"
- placeholder-params="请选择(多选)"
- :keeps-params="10"
- :is-concat="false"
- :is-multiple="true"
- :is-right="true"
- :allow-create="true"
- @change="changeVal"
- />
- </div>
- </template>
-
- <script>
- // 引入组件
- import VirtualSelect from '@/components/common/VirtualSelect/Select'
- export default {
- name: 'VirtualSelectDemo',
- // 注册组件
- components: { VirtualSelect },
- data () {
- return {
- selectValue: null,
- selectValue2: null,
- selectValueOptions: []
- }
- },
- created() {
- this.getList(10000, 1000).then(res => {
- this.selectValueOptions = res
- })
- },
- methods: {
- changeVal(val) {
- console.log(val, 'val')
- },
- getList(num = 10000, time) {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- const tempArr = []
- let count = num
- while (count--) {
- const index = num - count
- tempArr.push({
- code: `${index}$${Math.random().toString(16).substring(9)}`,
- index,
- name: `测试数据-${index}`,
- value: index
- })
- }
- resolve(tempArr)
- }, time)
- })
- },
- }
- }
- </script>
- <style scoped>
- </style>

总结:
- list的值是自己的选择框下拉值集数据。
- label 和value和v-model和原来的el-select属性一样。基本能满足一般需求。
- keepsParams 显示滚动加载的条数
isMultiple 是否多选
bindValue 绑定的默认值 (defaultValue)
isRight 是否右边显示value(code)值
isConcat 是否拼接 label 和 value
concatSymbol 拼接 label 和 value 的符号
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。