Vue el-checkbox 虚拟滚动解决多选框全选卡顿问题 - 高性能处理大数据量选项列表

news2025/7/17 19:20:45

一、背景

在我们开发项目中,经常会遇到需要展示大量选项的多选框场景,比如权限配置、数据筛选等。当选项数量达到几百甚至上千条时,传统的渲染方式全选时会非常卡顿,导致性能问题。本篇文章,记录我使用通过虚拟滚动实现大数据量全选卡顿问题~封装成组件啦可以直接用!

二、效果图

在这里插入图片描述

三、功能特点

  • 虚拟滚动:只渲染可视区域的选项,大幅提升性能
  • 搜索过滤:支持选项实时搜索
  • 全选/反选:一键操作所有选项
  • 默认选中:支持初始化选中项
  • 性能优化:使用节流和防抖处理滚动和搜索

四、组件virtual-checkbox.vue完整代码

<template>
  <div class="virtual-checkbox">
    <el-input 
      v-if="showSearch"
      v-model="keyword" 
      prefix-icon="el-input__icon el-icon-search" 
      type="text" 
      placeholder="搜索" 
      @input="seachKey">
    </el-input>
    <el-checkbox v-model="checkAll" :style="`height:${itemH}px`" class="check-all-box" :indeterminate="isIndeterminate" @change="handleCheckAllChange">
      全选
    </el-checkbox>
    <div ref="scrollBox" :style="`width:${viewW}px;height:${viewH}px;line-height:${itemH}px;overflow-y:auto`" @scroll="handleScroll">
      <div :style="`height:${scrollH}px;min-height:${viewH - 22}px`" class="list">
        <el-checkbox-group v-if="searchOptions.length" v-model="checkedList" :style="`transform:translateY(${offsetY}px)`" @change="handleCheckChange">
          <el-checkbox v-for="item in viewOptions" :key="item.value" :label="item.value" :style="`height:${itemH}px`" @change="handleCheckChange">
            {{ item.label }}
          </el-checkbox>
        </el-checkbox-group>
        <div v-else class="empty-text" :style="`height:${viewH - 22}px`">
          暂无数据
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { throttle, debounce } from 'lodash'
/**
 * @component VirtualCheckbox
 * @description 虚拟滚动多选框组件,用于处理大数据量的选项列表。
 * 实现了以下功能:
 * 1. 虚拟滚动:只渲染可视区域的选项,优化性能
 * 2. 搜索过滤:支持选项搜索
 * 3. 全选/反选:支持一键全选/反选
 * 4. 默认选中:支持默认值回显
 */
export default {
  props: {
    // 所有选项数据数组,格式:[{label: '选项名', value: '选项值'}]
    options: {
      type: Array,
      default: function () { return [] }
    },
    // 默认选中项的值数组
    defaultChecked: {
      type: Array,
      default: function () { return [] }
    },
    // 虚拟列表可视区域高度(像素)
    viewH: {
      type: Number,
      default: function () { return 200 }
    },
    // 虚拟列表可视区域宽度(像素)
    viewW: {
      type: Number,
      default: function () { return 300 }
    },
    // 每个选项的高度(像素)
    itemH: {
      type: Number,
      default: function () { return 20 }
    },
    // 是否显示搜索框
    showSearch: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      checkAll: false,
      isIndeterminate: false,
      searchOptions: [], // 搜索后的数据
      checkedList: [], // 当前选中的数据
      viewOptions: [], // 显示区域的数据
      keyword: '', // 搜索关键字
      offsetY: 0 // 偏移量
    }
  },
  computed: {
    scrollH() {
      return this.searchOptions.length * this.itemH
    },
    // 计算可视区域需要显示的选项数量
    visibleCount() {
      return Math.floor(this.viewH / this.itemH) + 1
    },
    // 计算当前显示区域的起始索引
    startIndex() {
      return Math.floor(this.offsetY / this.itemH)
    }
  },
  watch: {
    // 监听默认勾选变化 渲染勾选
    defaultChecked: {
      handler(val) {
        this.checkedList = val
        this.handleCheckAllIndeterminate()
      },
      deep: true,
      immediate: true
    }
  },
  beforeDestroy() {
    // 清理防抖和节流函数
    if (this.throttledScroll) {
      this.throttledScroll.cancel()
    }
    if (this.debouncedSearch) {
      this.debouncedSearch.cancel()
    }
  },
  created() {
    this.initData()
    // 创建节流函数
    this.throttledScroll = throttle(this.handleScrollContent, 10)
    // 创建防抖函数
    this.debouncedSearch = debounce(this.handleSearch, 300)
  },
  methods: {
    /**
     * 处理单个选项的选中状态变化
     * @emits change - 触发选中数据变化事件
     */
    handleCheckChange() {
      this.handleCheckAllIndeterminate()
      this.$emit('change', this.getCheckedData())
    },

    /**
     * 处理全选/取消全选
     * @param {Boolean} val - 是否全选
     * @emits change - 触发选中数据变化事件
     */
    handleCheckAllChange(val) {
      this.checkedList = val ? this.options.map(item => item.value) : []
      this.isIndeterminate = false
      this.$emit('change', this.getCheckedData())
    },
    // 处理全选是否选中或者半选
    handleCheckAllIndeterminate() {
      this.checkAll = this.checkedList.length === this.options.length
      this.isIndeterminate = this.checkedList.length > 0 && this.checkedList.length < this.options.length
    },
    // 滚动事件
    handleScroll(e) {
      this.throttledScroll(e)
    },

    handleScrollContent(e) {
      let scrollTop = e.target.scrollTop
      this.offsetY = scrollTop - scrollTop % this.itemH
      this.viewOptions = this.searchOptions.slice(
        this.startIndex,
        this.startIndex + this.visibleCount
      )
    },
    // 搜索
    seachKey() {
      this.debouncedSearch()
    },
    // 搜索具体实现
    /**
     * 搜索过滤
     * @description 支持对选项label的模糊搜索,大小写不敏感
     */
    handleSearch() {
      if (this.keyword) {
        this.searchOptions = this.options.filter(item =>
          String(item.label).toLowerCase().includes(this.keyword.toLowerCase())
        )
      } else {
        this.searchOptions = JSON.parse(JSON.stringify(this.options))
      }
      this.viewOptions = this.searchOptions.slice(0, Math.floor(this.viewH / this.itemH) + 1)
      this.initScroll()
    },
    // 重置滚动
    initScroll() {
      const scrollBox = this.$refs.scrollBox
      if (scrollBox) {
        scrollBox.scrollTop = 0  // 将 scrollTop 设置为 0,确保每次弹出时滚动条回到顶部
        this.offsetY = 0
      }
    },
    // 初始化数据
    initData() {
      this.keyword = ''
      this.checkAll = false
      this.isIndeterminate = false
      this.checkedList = [...this.defaultChecked]
      this.searchOptions = this.options.length ? JSON.parse(JSON.stringify(this.options)) : []
      this.viewOptions = this.searchOptions.slice(0, Math.floor(this.viewH / this.itemH) + 1)
      this.initScroll()
      this.handleCheckAllIndeterminate()
      this.$emit('change', this.getCheckedData())
    },

    // 重置所有状态
    reset() {
      this.initData()
    },

    /**
     * 获取当前选中的数据
     * @returns {Object} 包含选中项的值数组和完整数据数组
     * @returns {Array} checkedValues - 选中项的value数组
     * @returns {Array} checkedItems - 选中项的完整数据数组
     */
    getCheckedData() {
      return {
        // 选中项的value数组
        checkedValues: this.checkedList,
        // 选中项的完整数据数组
        checkedItems: this.options.filter(item => this.checkedList.includes(item.value))
      }
    }
  }
}
</script>

<style lang="scss" scoped>
  ::v-deep .el-checkbox-group {
    display: flex;
    flex-direction: column;
    .el-checkbox {
      display: block;
    }
  }
  .check-all-box {
    margin-top: 10px;
  }
  .empty-text {
    color: #ccc;
    font-size: 12px;
    text-align: center;
    display: flex;
    justify-content: center;
    align-items: center;
  }
</style>

五、使用示例

<template>
  <div class="check-box">
    <div class="title">
      全选案例
    </div>
    <VirtualCheckbox :options="options" :default-checked="defaultCheckList" :view-h="500" :item-h="30" @change="change"></VirtualCheckbox>
  </div>
</template>

<script>
import VirtualCheckbox from './virtual-checkbox.vue'
export default {
  components: { VirtualCheckbox },
  data() {
    return {
      defaultCheckList: [], // 默认选中项
      checkList: [], // 当前选中项
      options: [] // 所有选项
    }
  },
  created() {
    this.getOptions()
  },
  methods: {
    getOptions() {
      const data = []
      for (let i = 1; i < 1000; i++) {
        data.push({
          value: i,
          label: '选项' + i
        })
      }
      this.options = data
      this.defaultCheckList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    },
    change(val) {
      this.checkList = val.checkedValues // 当前选中的id集合
    }
  }
}
</script>

<style lang="scss" scoped>
  .check-box {
    border: 1px solid red;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    .title {
      font-size: 30px;
      font-weight: bold;
      margin-bottom: 10px;
    }
  }
</style>

六、 注意事项

  1. 项目记得下载lodash,组件使用了lodash的防抖节流
  2. options 数据格式必须符合 {label: string, value: string|number} 的格式
  3. itemH 需要与实际选项高度一致,否则可能导致滚动计算错误
  4. 组件销毁时会自动清理节流和防抖函数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2342140.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

声音识别(声纹识别)和语音识别的区别

目录 引言一、语音识别1.声学模型2.语言模型3.词典 二、声音识别&#xff08;声纹识别&#xff09;三、语音识别、声音识别、语义识别的区别四、总结 引言 咋一看这个标题是不是很多小伙伴都迷糊了&#xff0c;哇哈&#xff0c;这两个不是一样的吗&#xff1f; 结论是&#x…

使用Mybaitis-plus提供的各种的免写SQL的Wrapper的使用方式

文章目录 内连接JoinWrappers.lambda和 new MPJLambdaWrapper 生成的MPJLambdaWrapper对象有啥区别&#xff1f;LambdaQueryWrapper 和 QueryWrapper的区别&#xff1f;LambdaQueryWrapper和MPJLambdaQueryWrapper的区别&#xff1f;在作单表更新时建议使用&#xff1a;LambdaU…

springboot-基于Web企业短信息发送系统(源码+lw+部署文档+讲解),源码可白嫖!

摘要 当今社会已经步入了科学技术进步和经济社会快速发展的新时期&#xff0c;国际信息和学术交流也不断加强&#xff0c;计算机技术对经济社会发展和人民生活改善的影响也日益突出&#xff0c;人类的生存和思考方式也产生了变化。本系统采用B/S架构&#xff0c;数据库是MySQL…

秀丸编辑器 使用技巧

参考资料 第II部〜知っていると便利な秀丸の機能 検索テキストファイルの16進表示について秀丸エディタヘルプ目次秀丸エディタ&#xff31;&#xff06;&#xff21;集(第9.6版)&#xff08;HTML 形式&#xff09;テンプレート&#xff08;Ver9.43対応版&#xff09; 目录 零…

什么是量子计算?它能做什么?

抛一枚硬币。要么正面朝上&#xff0c;要么反面朝上&#xff0c;对吧&#xff1f;当然&#xff0c;那是在我们看到硬币落地的结果之后。但当硬币还在空中旋转时&#xff0c;它既不是正面也不是反面&#xff0c;而是正面和反面都有一定的可能性。 这个灰色地带就是量子计算的简…

【新能源科学与技术】MATALB/Simulink小白教程(一)实验文档【新能源电力转换与控制仿真】

DP读书&#xff1a;新能源科学与工程——专业课「新能源发电系统」 2025a 版本 MATLAB下面进入正题 仿真一&#xff1a;Buck 电路一、仿真目的二、仿真内容&#xff08;一&#xff09;Buck电路基本构成及工作原理&#xff08;二&#xff09;Buck电路仿真模型及元件连接&#xf…

快速生成安卓证书并打包生成安卓apk(保姆教程)

一.生成安卓证书 目前市面上生成可以快速生成安卓证书的网站有很多个人推荐香蕉云编以下是网站链接 香蕉云编-app打包上架工具类平台 1.进入网站如下图 2.点击生成签名证书 3.点击立即创建证书 4.点击创建安卓证书 5.按照指引完成创建 6.点击下载就可使用 二.打包安卓apk …

mysql mvvc 实现方案

Mysql 事务隔离级别 并发问题 mysql中事务并发时&#xff0c;会产生的问题如下 脏读: 读到了其他事务中&#xff0c;暂未提交的数据 脏读 (Dirty Read) 是数据库事务隔离级别中最低的一种隔离级别 (READ UNCOMMITTED) 下可能出现的一种并发问题。 它指的是一个事务读取了另…

校园外卖服务系统的设计与实现(代码+数据库+LW)

摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;外卖信息因为其管理内容繁杂&#xff0c;管理数量繁多导致手工进行处理不能满足广…

纷析云:开源财务管理软件的创新与价值

在企业数字化转型中&#xff0c;纷析云作为一款优秀的开源财务管理软件&#xff0c;正为企业财务管理带来新变革&#xff0c;以下是其核心要点。 一、产品概述与技术架构 纷析云采用微服务架构&#xff0c;功能组件高内聚低耦合&#xff0c;可灵活扩展和定制。前端基于现代框…

Centos安装Dockers+Postgresql13+Postgis3.1

centos8安装docker步骤 1、# 强制卸载 podman 和 buildah 执行命令&#xff1a; yum erase podman buildah 2、# 添加阿里云仓库 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 3、# 安装基础依赖包 yum install…

【计算机网络 | 第二篇】常见的通信协议(一)

HTTP和HTTPS有什么区别&#xff1f; 端口号&#xff1a;HTTP默认是80端口&#xff0c;HTTPS默认是443。 URL前缀&#xff1a;HTTPHTTP 的 URL 前缀是 http://&#xff0c;HTTPS 的 URL 前缀是 https://。 安全性和资源消耗&#xff1a;HTTP协议运行在TCP上&#xff0c;都是明…

单片机——使用printf调试

配置printf()输出函数 1、来自于<stdio.h> 2、运行C语言时&#xff0c;输出到终端 3、单片机没有终端&#xff0c;需要使用串口&#xff0c;将要输出的内容传到电脑&#xff08;串口调试助手&#xff09;上 例子如下 #include <stdio.h> #include &qu…

4.23晚间工作总结

主要工作&#xff1a;将ClassicDetail界面拆分成utils,apis,stores,css,vue多个文件&#xff0c;方便后续重用 具体代码截图&#xff1a;

JavaEE学习笔记(第二课)

1、好用的AI代码工具cursor 2、Java框架&#xff1a;Spring(高级框架)、Servelt、Struts、EJB 3、Spring有两层含义&#xff1a; ①Spring Framework&#xff08;原始框架&#xff09; ②Spring家族 4、Spring Boot(为了使Spring简化) 5、创建Spring Boot 项目 ① ② ③…

约束constraint

创建表时&#xff0c;可以给表的字段添加约束&#xff0c;可以保证数据的完整性、有效性。比如大家上网注册用户时常见的&#xff1a;用户名不能为空。对不起&#xff0c;用户名已存在。等提示信息。 约束通常包括&#xff1a; 非空约束&#xff1a;not null检查约束&#xf…

解锁现代生活健康密码,开启养生新方式

在科技飞速发展的当下&#xff0c;我们享受着便捷生活&#xff0c;却也面临诸多健康隐患。想要维持良好状态&#xff0c;不妨从这些细节入手&#xff0c;解锁科学养生之道。​ 肠道是人体重要的消化器官&#xff0c;也是最大的免疫器官&#xff0c;养护肠道至关重要。日常可多…

在kali中安装AntSword(蚁剑)

步骤一、下载压缩包 源码&#xff1a;https://github.com/AntSwordProject/antSword&#xff0c;下载压缩包。 加载器&#xff1a;https://github.com/AntSwordProject/AntSword-Loader&#xff0c;根据系统选择压缩包&#xff08;kali选择AntSword-Loader-v4.0.3-linux-x64&…

GateWay与Consul知识点

这是一个涵盖客户端访问、网关处理、服务注册发现、业务服务及鉴权授权的系统架构图&#xff0c;各部分解析如下&#xff1a; 客户端层 App 端、Web 端&#xff1a;代表不同类型的客户端&#xff0c;涵盖手机 App、电脑 Web 页面等。用户通过这些客户端发起请求&#xff0c;访…

安宝特科技 | Vuzix Z100智能眼镜+AugmentOS:重新定义AI可穿戴设备的未来——从操作系统到硬件生态,如何掀起无感智能革命?

一、AugmentOS&#xff1a;AI可穿戴的“操作系统革命” 2025年2月3日&#xff0c;Vuzix与AI人机交互团队Mentra联合推出的AugmentOS&#xff0c;被业内视为智能眼镜领域的“iOS时刻”。这款全球首个专为智能眼镜设计的通用操作系统&#xff0c;通过三大突破重新定义了AI可穿戴…