element-ui 使用el-select创建虚拟下拉列表

实现思路:

1: 获取列表数据,计算出总数total

2: 根据行高,视口高度,计算出需要显示的数量

3: 监听滚动区域,计算需要位移的距离,添加节流控制避免卡顿

4: 显示列表渲染

直接贴代码

虚拟列表组件:

<template>
  <el-select
    v-model="value"
    class="chooseSelect"
    :popper-class="popperClass"
    :filterable="filterable"
    :clearable="clearable"
    :filter-method="filterMethod"
    :disabled="disabled"
    :multiple="selectAttributes.multiple || false"
    :multiple-limit="selectAttributes.multipleLimit || 0"
    :placeholder="placeholderValue"
    @visible-change="handleVisibleChange"
    @change="changeValue"
  >
    <el-option
      v-for="(item, index) in optionsData"
      :key="index"
      :label="item.label"
      :value="item"
    ></el-option>
  </el-select>
</template>

<script>
import { _throttle } from '@/utils/public'

/**
 * 长列表select
 */
export default {
  props: {
    // 绑定值
    model: {
      type: [String, Number, Boolean],
      default: ''
    },
    // 下拉数据
    options: {
      type: [Array, Object],
      default: () => ([])
    },
    // 支持过滤
    filterable: {
      type: Boolean,
      default: false
    },
    // 支持清除
    clearable: {
      type: Boolean,
      default: false
    },
    // 占位符
    placeholder: {
      type: String,
      default: '请选择'
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false
    },
    // 传入为Array时,需要定义标签和value的name [labelName, valueName]
    labelValue: {
      type: Array,
      default: () => (['label', 'value'])
    },
    // el-select配置项
    selectAttributes: {
      type: Object,
      default: () => ({})
    }
  },
  computed: {
    popperClass() {
      return 'popperClass' + Date.now()
    }
  },
  watch: {
    options: {
      immediate: true,
      handler(val) {
        this.filterOptions = this.handleOption(val)
        this.modelToValue(this.model)
      },
      deep: true
    },
    filterOptions: {
      handler() {
        this.init()
      },
      deep: true
    },
    model: {
      immediate: true,
      handler(val) {
        this.modelToValue(val)
      }
    }
  },
  data() {
    return {
      value: null,
      // 过滤后源数据
      filterOptions: [],
      // 下拉框数据
      optionsData: [],
      // 可视区域的高度250(element默认,修改的话需要对应修改样式)
      viewHeight: 250,
      // 当前行的默认高度30(element默认,修改的话需要对应修改样式)
      rowHeight: 30,
      startIndex: 0,
      endIndex: 0,
      // 滚动容器
      scrollView: null,
      // 占位容器
      placeholderView: null,
      // 是否注册监听
      isSignIn: false,
      // 当前选中的item
      cacheItem: {},
      placeholderValue: ''
    }
  },
  mounted() {
    this.placeholderValue = this.placeholder
  },
  beforeDestroy() {
    if (this.scrollView && this.isSignIn) {
      this.scrollView.removeEventListener('scroll', this.handleScroll)
    }
  },
  methods: {
    // 用于 初始化回显
    modelToValue(val) {
      const options = this.handleOption(this.options) || []
      const item = options.find(item => val === item.value) || {}
      this.value = item.label || this.value || null
      this.cacheItem = item
    },
    init() {
      const { filterOptions, rowHeight, viewHeight } = this
      // 滚动区域
      const scrollView = document.querySelector(`.${this.popperClass} .el-scrollbar__wrap`)
      // 虚拟列表占位区域
      const placeholderView = document.querySelector(`.${this.popperClass} .el-select-dropdown__list`)
      // 注册监听
      if (scrollView && !this.isSignIn) {
        this.scrollView = scrollView
        this.isSignIn = true
        this.scrollView.addEventListener('scroll', this.handleScroll)
      }
      if (placeholderView) {
        this.placeholderView = placeholderView
        // 所有数据的总条数
        const total = filterOptions.length
        if (total < Math.floor(viewHeight / rowHeight)) {
          this.placeholderView.style.height = '100%'
        } else {
          this.placeholderView.style.height = `${total * rowHeight}px`
        }
        this.handleScroll()
      }
    },
    // 滚动事件回调触发更新数据
    updateOptions() {
      const { filterOptions, viewHeight, rowHeight } = this
      const total = filterOptions.length
      // 可视区域的条数 向上取整
      const limit = Math.ceil(viewHeight / rowHeight)
      // 设置末位索引
      this.endIndex = Math.min(this.startIndex + limit, total)
      // 如果endIndex是最后一个,则重计算startIndex,避免后缀白条或被隐藏的情况
      if (this.endIndex === total) {
        this.startIndex = Math.max(this.endIndex - limit, 0)
      }
      this.optionsData = filterOptions.slice(this.startIndex, this.endIndex)
    },
    // 下拉框重新显示时归位
    handleVisibleChange(val) {
      // 当打开下拉框时,重置scrollView的paddingTop,避免白屏
      if (this.placeholderView) {
        this.$nextTick(() => {
          this.placeholderView.style.paddingTop = '0px'
        })
      }
      this.filterMethod('')
      this.updateOptions()
      // 处理filterable时value直接在input中
      if (val && this.filterable) {
        this.$nextTick(() => {
          this.placeholderValue = this.value || this.placeholder
          this.value = null
        })
      } else {
        this.$nextTick(() => {
          this.placeholderValue = this.placeholder
          this.value = this.cacheItem.label
        })
      }
    },
    // 滚动监听
    handleScroll: _throttle(function() {
      const { rowHeight, startIndex } = this
      const scrollTop = this.scrollView.scrollTop
      // 计算当前滚动位置,获取当前开始的起始位置
      const currentIndex = Math.floor(scrollTop / rowHeight)
      // 根据滚动条获取当前索引与起始索引不相等时,将滚动的当前位置设置为起始位置
      if (currentIndex !== startIndex) {
        // 重新赋值
        this.startIndex = Math.max(currentIndex, 0)
      }
      if (startIndex === 0) {
        this.placeholderView.style.paddingTop = '0px'
      } else {
        this.placeholderView.style.paddingTop = `${startIndex * rowHeight}px`
      }
      this.updateOptions()
    }, 20),
    // 过滤数据
    filterMethod(val) {
      if (val) {
        this.value = val
      }
      const options = this.handleOption(this.options)
      this.filterOptions = options.filter(item => {
        return item.label.includes(val)
      })
    },
    // 处理Option类型问题
    handleOption(options) {
      const arr = []
      if (options instanceof Array) {
        options.forEach(item => {
          arr.push({
            label: item[this.labelValue[0]],
            value: item[this.labelValue[1]],
            item: item
          })
        })
      } else if (options instanceof Object) {
        // 对象类型 { 1: 'xxx' }转换为数组类型
        for (const key in options) {
          arr.push({
            label: options[key],
            value: key
          })
        }
      }
      return arr
    },
    changeValue(val) {
      // 使用label,options改变时会导致只显示value
      this.value = val.label
      this.cacheItem = val || {}
      this.$emit('update:model', val.value || null)
      this.$emit('change', val)
    }
  }
}
</script>

<style lang="scss" scoped>

</style>

父组件调用:

<virtual-select
  :options="upCompanyList"
  :model.sync="editForm.parentId"
  :label-value="['customName', 'id']"
  filterable
  clearable
  placeholder="请选择上级公司"
  @change="changeParent"
>
</virtual-select>

节流函数

// 节流
export function _throttle(fn, interval) {
  let last
  let timer
  const intervalTimer = interval || 200
  return function() {
    const th = this
    const args = arguments
    const now = +new Date()
    if (last && now - last < intervalTimer) {
      clearTimeout(timer)
      timer = setTimeout(function() {
        last = now
        fn.apply(th, args)
      }, intervalTimer)
    } else {
      last = now
      fn.apply(th, args)
    }
  }
}

效果图

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
引用\[1\]和\[2\]中提到了使用this.$forceUpdate()来重新渲染el-select组件。这个方法可以在数据赋值后手动调用,强制组件重新渲染。在el-select组件上添加key属性,将其绑定到一个会动态变化的值上,比如后端返回的数据集合的长度,也可以解决样式不一致的问题。所以,如果在element-ui使用el-select绑定函数时出现样式问题,可以尝试使用this.$forceUpdate()方法重新渲染组件,并在el-select上添加key属性绑定一个动态变化的值。 #### 引用[.reference_title] - *1* [element-ui 解决 el-select 设置默认值后无法切换选项](https://blog.csdn.net/weixin_44640323/article/details/122175514)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [element-ui 解决 el-select 设置初始默认值后切换选项无效问题](https://blog.csdn.net/sea9528/article/details/121403975)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [< element-UIel-select组件,下拉时内容无法显示滚动条 >](https://blog.csdn.net/MrWen2395772383/article/details/126159719)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值