通过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(是否显示)
相关推荐
mldong1 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60612 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角3 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi5 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu7 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss