通过vue-virtual-scroller封装虚拟滚动el-select

通过vue-virtual-scroller封装虚拟滚动el-select组件的代码。该组件解决了大量数据渲染时的性能问题,支持普通列表和分组列表两种虚拟滚动模式

组件内容

javascript 复制代码
<!-- 使用文档 https://blog.csdn.net/weixin_49455560/article/details/160083586?spm=1011.2415.3001.5331 -->
<template type="text/x-template" id="virtual-select">
  <el-select :size="size" v-model="value" :loading="loading" @change="handleChange" @focus="handleFocus" @blur="handleBlur" :multiple="multiple" :filterable="filterable" :filter-method="handleFilterMethod" @visible-change="handleVisibleChange" :disabled="disabled" :collapse-tags="collapseTags" :placeholder="placeholder" :clearable="clearable">
    <!-- begin 普通下拉 -->
    <recycle-scroller v-if="type == 1" :items="filteredItems" :item-size="itemSize" :key-field="props.value" style="max-height: 250px">
      <template v-slot:default="{item, index}">
        <el-option :label="item[props.label]" :value="item[props.value]" :key="item[props.value]">
          <slot name="default" :item="item"></slot>
        </el-option>
      </template>

      <template #empty v-if="!filteredItems.length">
        <div class="empty-txt">暂无数据</div>
      </template>
    </recycle-scroller>
    <!-- end 普通下拉 -->

    <!-- begin 分组下拉 -->
    <dynamic-scroller v-if="type == 2" :items="filteredGroupItems" :min-item-size="groupItemSize" :key-field="props.groupId" style="max-height: 250px">
      <template v-slot="{ item, index, active }">
        <dynamic-scroller-item :item="item" :active="active" :size-dependencies="[ item[props.children] || [] ]" :data-index="index">
          <el-option-group v-if="item[props.children].length > 0" :label="item[props.group]" :key="item[props.groupId]">
            <el-option v-for="(citem, cindex) in item[props.children] || []" :key="citem[props.value]" :label="citem[props.label]" :value="citem[props.value]">
              <slot name="default" :item="citem"></slot>
            </el-option>
          </el-option-group>
        </dynamic-scroller-item>
      </template>

      <template #empty v-if="!filteredGroupItems.length">
        <div class="empty-txt">暂无数据</div>
      </template>
    </dynamic-scroller>
    <!-- end 分组下拉 -->
  </el-select>
</template>
<link rel="stylesheet" href="/assets/js/vue-virtual-scroller/vue-virtual-scroller.css" />
<script src="/assets/js/vue-virtual-scroller/vue-virtual-scroller.min.js"></script>

<script>
  Vue.component("recycle-scroller", VueVirtualScroller.RecycleScroller);
  Vue.component("dynamic-scroller", VueVirtualScroller.DynamicScroller);
  Vue.component("dynamic-scroller-item", VueVirtualScroller.DynamicScrollerItem);

  Vue.component("virtual-select", {
    template: "#virtual-select",
    model: {
      prop: "value",
      event: "change",
    },
    props: {
      type: {
        type: Number,
        default: 1, // 1-普通虚拟下拉 2-分组虚拟下拉
      },
      value: {
        type: [String, Array],
        required: true,
      },
      loading: {
        type: Boolean,
        default: false,
      },
      items: {
        type: Array,
        default: [],
      },
      multiple: {
        type: Boolean,
        default: false,
      },
      filterable: {
        type: Boolean,
        default: false,
      },
      filterMethod: {
        type: Function,
        default: null,
      },
      clearable: {
        type: Boolean,
        default: false,
      },
      placeholder: {
        type: String,
        default: "请选择",
      },
      size: {
        type: String,
        default: "mini",
      },
      disabled: {
        type: Boolean,
        default: false,
      },
      collapseTags: {
        type: Boolean,
        default: false,
      },
      // 单行高度(px)
      itemSize: {
        type: Number,
        default: 34,
      },
      // 分组项最小高度(px)
      groupItemSize: {
        type: Number,
        default: 64,
      },
      // 自定义属性名称
      props: {
        type: Object,
        default: () => {
          return {
            group: "group", // 分组名称
            groupId: "groupId", // 分组id
            label: "label", // 选项名称
            value: "value", // 选项值
            children: "children", // 子数据
          };
        },
      },
    },
    computed: {
      // 过滤后的普通列表数据
      filteredItems() {
        if (!this.filterable || !this.query || this.filterMethod) {
          return this.items;
        }
        const labelField = this.props.label;
        return this.items.filter(item => String(item[labelField]).toLowerCase().includes(this.query.toLowerCase()));
      },
      // 过滤后的分组列表数据
      filteredGroupItems() {
        if (!this.filterable || !this.query || this.filterMethod) {
          return this.items.filter(group => group[this.props.children] && group[this.props.children].length);
        }
        const labelField = this.props.label;
        const childrenField = this.props.children;
        const keyword = this.query.toLowerCase();

        return this.items
          .map(group => {
            const filteredChildren = (group[childrenField] || []).filter(child => String(child[labelField]).toLowerCase().includes(keyword));
            return {
              ...group,
              [childrenField]: filteredChildren,
            };
          })
          .filter(group => group[this.props.children] && group[this.props.children].length);
      },
    },
    data: () => {
      return {
        query: "",
      };
    },
    methods: {
      // 过滤方法
      handleFilterMethod(val) {
        this.query = val;
        if (this.filterMethod) {
          this.filterMethod(this.query);
        }
      },
      // 选择方法
      handleChange(val) {
        this.$emit("change", val);
      },
      // 可见性改变方法
      handleVisibleChange(visible) {
        if (visible) {
          this.query = "";
        }
        this.$emit("visible-change", visible);
      },
      // 聚焦方法
      handleFocus() {
        this.$emit("focus", this);
      },
      // 失焦方法
      handleBlur() {
        this.$emit("blur", this);
      },
    },
  });
</script>
<style>
  .empty-txt {
    line-height: 32px;
    text-align: center;
    color: #999;
  }
</style>

基础用法(普通列表)

javascript 复制代码
<template>
	<virtual-select v-model="selectedValue" :type="1" :items="options" placeholder="请选择" filterable clearable></virtual-select>
</template>

<script>
export default {
  data() {
    return {
      selectedValue: '',
      options: Array.from({ length: 10000 }, (_, i) => ({
        label: `选项 ${i + 1}`,
        value: i + 1
      }))
    }
  }
}
</script>

分组列表用法

javascript 复制代码
<template>
	<virtual-select v-model="selectedValue" :type="2" :items="groupOptions" placeholder="请选择分组" filterable></virtual-select>
</template>

<script>
export default {
  data() {
    return {
      selectedValue: '',
      groupOptions: Array.from({ length: 200 }, (_, i) => {
        return {
          group: `分组 ${i + 1}`,
          groupId: i + 1,
          children: Array.from({ length: 3 }, (_, j) => ({
            label: `选项 ${i + 1}${j + 300}`,
            value: `${i + 1}${j + 300}`,
          })),
        };
      }),
    }
  }
}
</script>

API 文档

Props

参数 说明 类型 默认值
type 类型:1-普通虚拟下拉,2-分组虚拟下拉 Number 1
value 绑定值(支持 v-model) String / Array 必填
loading 是否显示加载中状态 Boolean false
items 数据源 Array []
multiple 是否多选 Boolean false
filterable 是否可搜索 Boolean false
filter-method 自定义过滤方法 Function null
clearable 是否可清空 Boolean false
placeholder 占位符 String '请选择'
size 尺寸 String 'mini'
disabled 是否禁用 Boolean false
collapse-tags 多选时是否折叠标签 Boolean false
item-size 普通列表单行高度(px) Number 34
group-item-size 分组列表最小高度(px) Number 64
props 字段映射配置 Object 见下方说明

props 默认值

javascript 复制代码
{
  group: 'group',        // 分组名称字段
  groupId: 'groupId',    // 分组ID字段
  label: 'label',        // 选项名称字段
  value: 'value',        // 选项值字段
  children: 'children'   // 子数据字段
}

Events

事件名 说明 回调参数
change 值变化时触发 当前选中的值
focus 获得焦点时触发 (event: Event)
blur 失去焦点时触发 (event: Event)
visible-change 下拉框显示/隐藏时触发 visible(是否显示)
相关推荐
小李子呢02112 小时前
前端八股Vue(7)---computed计算属性和watch侦听器
前端·javascript·vue.js
CCIE-Yasuo2 小时前
Win11-Microsoft Edge使用起来CPU飙升以及卡顿问题解决
前端·microsoft·edge·排故
吴声子夜歌2 小时前
ES6——对象的扩展详解
开发语言·javascript·es6
Ruihong3 小时前
Vue3 转 React:组件透传 Attributes 与 useAttrs 使用详解|VuReact 实战
vue.js·react.js
是江迪呀3 小时前
实时看大家都在干嘛?我靠一行监听函数,做了个轻互动小程序
前端·微信小程序
QCzblack3 小时前
BugKu BUUCTF ——Reverse
java·前端·数据库
gwjcloud3 小时前
基于linux下docker部署前端vue项目
前端·javascript·vue.js
小李子呢02113 小时前
前端八股CSS(1)---响应式布局的方法
前端·css
小李子呢02114 小时前
前端八股Vue(6)---v-if和v-for
前端·javascript·vue.js