通过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(是否显示)
相关推荐
一 乐1 天前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
admin and root1 天前
「AI安全篇」实战AntiDebug自动化JS逆向加解密MCP
javascript·人工智能·网络安全·自动化·漏洞挖掘·cnvd·src赏金
前端snow1 天前
ai agent --- postgreSQL 关系型数据库
前端
卷无止境1 天前
ECharts:把数据变成故事的可视化利器
前端
Eric_见嘉1 天前
在职前端 Skill 和 MCP 分享
前端·后端·agent
liangshanbo12151 天前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js
wengad1 天前
从 wangEditor 迁到 wangEditor-next
前端·javascript·vue.js
行者-全栈开发1 天前
【前端/JS框架】CVE-2026-3854:GitHub Enterprise Server X-Stat注入XSS漏洞深度剖析与紧急修复指南
前端·javascript·github
计算机魔术师1 天前
AMD砸82亿买下李飞飞,这次不卖芯片卖什么?
前端
hzxpaipai1 天前
杭州企业官网怎么建设?从策划到上线的5步建站方法
运维·服务器·前端·网络