Vue3+el-table-v2虚拟表格大数据量多选功能详细教程
本教程基于 Element Plus 组件库的
el-table-v2
(假设你使用虚拟滚动表格),实现大数据量场景下的多选功能,并包含了全选、反选、已选行展示、清除选择等完整交互。
一、项目背景与需求
- 需要展示 数千至上万行数据,并且保证表格渲染性能
- 支持每行数据的单独多选功能
- 支持表头复选框,实现当前页全选/取消全选
- 显示已选中行的简要信息及数量
- 支持一键清除所有选择
- 可通过输入框动态控制生成数据的数量
二、核心思路
- 数据源
tableData
是所有当前页显示的行数据,带有_selected
字段来表示当前行是否被选中 - 选中行使用独立响应式数组
selectedRows
来存储完整对象 - 表头复选框绑定
isAllSelected
计算属性,实现全选与取消全选的切换 - 自定义列渲染器插入复选框组件,单独控制每行选中状态,双向绑定
_selected
- 已选行信息展示区同步展示
selectedRows
内容 - 动态数据生成时重置选中状态,保证数据和选中状态同步一致
三、完整代码详解
1. 模板部分(template)
<template>
<div class="app-container">
<!-- 操作控制面板 -->
<div class="control-panel mb-4">
<el-row :gutter="20">
<el-col :span="6">
<el-input-number v-model="rowCount" :min="100" :max="100000" :step="1000" @change="generateData" placeholder="行数量" />
</el-col>
<el-col :span="6">
<el-button type="primary" @click="generateData">重新生成数据</el-button>
</el-col>
<el-col :span="6">
<el-tag type="info">当前数据:{{ tableData.length }}行</el-tag>
</el-col>
<el-col :span="6">
<el-tag type="success" v-if="selectedRows.length">已选择:{{ selectedRows.length }}行</el-tag>
</el-col>
</el-row>
</div>
<!-- 虚拟化表格 -->
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>虚拟化表格 - 高效处理大数据量</span>
<div>
<el-button size="small" type="primary" @click="toggleAllSelection">
{{ isAllSelected ? '取消全选' : '全选' }}
</el-button>
</div>
</div>
</template>
<el-table-v2
:columns="columns"
:data="tableData"
:width="tableWidth"
:height="500"
:fixed="true"
:row-height="40"
:loading="loading"
/>
</el-card>
<!-- 选中行信息展示 -->
<el-card class="box-card mt-4" v-if="selectedRows.length > 0">
<template #header>
<div class="card-header">
<span>已选中行信息 ({{ selectedRows.length }})</span>
<el-button size="small" type="danger" @click="clearSelection">清除选择</el-button>
</div>
</template>
<div class="selected-rows-info">
<div v-for="(row, index) in selectedRows.slice(0, 5)" :key="row.id" class="selected-row">
<p><strong>行 #{{ row.id }}</strong>: {{ row.name }} - {{ row.job }} - {{ row.department }}</p>
</div>
<div v-if="selectedRows.length > 5" class="more-info">
还有 {{ selectedRows.length - 5 }} 行未显示...
</div>
</div>
</el-card>
</div>
</template>
2. 脚本逻辑(script setup)
import { ref, computed, h, onMounted } from 'vue'
import { ElCheckbox } from 'element-plus'
// 表格数据 & 配置
const tableData = ref([])
const rowCount = ref(5000)
const loading = ref(false)
// 选中行列表
const selectedRows = ref([])
// 判断是否全选(所有行都选中)
const isAllSelected = computed(() => {
return selectedRows.value.length > 0 && selectedRows.value.length === tableData.value.length
})
// 切换全选/取消全选
const toggleAllSelection = () => {
if (isAllSelected.value) {
// 取消全选
selectedRows.value = []
tableData.value.forEach(item => (item._selected = false))
} else {
// 全选所有行
selectedRows.value = [...tableData.value]
tableData.value.forEach(item => (item._selected = true))
}
}
// 清除所有选中项
const clearSelection = () => {
selectedRows.value = []
tableData.value.forEach(item => (item._selected = false))
}
// 切换单行选中状态(配合checkboxRenderer)
const toggleRowSelection = (row) => {
if (row._selected) {
// 选中,加入selectedRows
selectedRows.value.push(row)
handleRowSelected(row)
} else {
// 取消选中,从selectedRows移除
const index = selectedRows.value.findIndex(item => item.id === row.id)
if (index !== -1) {
selectedRows.value.splice(index, 1)
handleRowDeselected(row)
}
}
}
// 选中行回调
const handleRowSelected = (row) => {
console.log('行被选中:', row)
}
// 取消选中行回调
const handleRowDeselected = (row) => {
console.log('行被取消选中:', row)
}
// 自定义复选框渲染器 - 单元格
const checkboxRenderer = ({ rowData }) => {
return h(ElCheckbox, {
modelValue: rowData._selected,
'onUpdate:modelValue': (value) => {
rowData._selected = value
toggleRowSelection(rowData)
}
})
}
// 自定义表头复选框渲染器
const headerCheckboxRenderer = () => {
return h(ElCheckbox, {
modelValue: isAllSelected.value,
'onUpdate:modelValue': toggleAllSelection,
indeterminate: selectedRows.value.length > 0 && selectedRows.value.length < tableData.value.length,
})
}
// 表格列配置
const columns = [
{
key: 'selection',
dataKey: 'selection',
title: '',
width: 50,
cellRenderer: checkboxRenderer,
headerCellRenderer: headerCheckboxRenderer,
},
{ key: 'index', dataKey: 'index', title: '序号', width: 60, cellRenderer: ({ rowIndex }) => rowIndex + 1 },
{ key: 'id', dataKey: 'id', title: 'ID', width: 100 },
{ key: 'name', dataKey: 'name', title: '姓名', width: 120 },
{ key: 'age', dataKey: 'age', title: '年龄', width: 80 },
{ key: 'address', dataKey: 'address', title: '地址', width: 200 },
{ key: 'phone', dataKey: 'phone', title: '电话', width: 150 },
{ key: 'email', dataKey: 'email', title: '邮箱', width: 200 },
{ key: 'job', dataKey: 'job', title: '职业', width: 120 },
{ key: 'salary', dataKey: 'salary', title: '薪资', width: 120 },
{ key: 'department', dataKey: 'department', title: '部门', width: 150 },
{ key: 'joinDate', dataKey: 'joinDate', title: '入职日期', width: 150 },
]
// 计算表格宽度
const tableWidth = computed(() => columns.reduce((total, col) => total + col.width, 0))
// 生成模拟数据
const generateData = () => {
loading.value = true
selectedRows.value = []
setTimeout(() => {
const data = []
const nameList = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
const jobs = ['工程师', '设计师', '产品经理', '运营', '市场', '销售', '财务', '人力资源']
const departments = ['技术部', '产品部', '设计部', '市场部', '销售部', '财务部', '人力资源部']
for (let i = 0; i < rowCount.value; i++) {
const nameIndex = Math.floor(Math.random() * nameList.length)
const jobIndex = Math.floor(Math.random() * jobs.length)
const deptIndex = Math.floor(Math.random() * departments.length)
data.push({
id: i + 1,
name: nameList[nameIndex] + (i % 100),
age: Math.floor(Math.random() * 40) + 20,
address: `北京市朝阳区某某路${Math.floor(Math.random() * 100) + 1}号`,
phone: `1${Math.floor(Math.random() * 9) + 1}${Math.random().toString().slice(2, 10)}`,
email: `user${i}@example.com`,
job: jobs[jobIndex],
salary: Math.floor(Math.random() * 20000) + 5000,
department: departments[deptIndex],
joinDate: `202${Math.floor(Math.random() * 4)}-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
_selected: false,
})
}
tableData.value = data
loading.value = false
}, 300)
}
// 组件挂载时初始化数据
onMounted(() => {
generateData()
})
3. 样式(style)
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.mb-4 {
margin-bottom: 16px;
}
.mt-4 {
margin-top: 16px;
}
.selected-rows-info {
.selected-row {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.more-info {
margin-top: 10px;
color: #909399;
font-style: italic;
}
}
四、功能总结与优化建议
功能点 | 说明 | 优化建议 |
---|---|---|
大数据量渲染 | 采用虚拟滚动表格减少渲染压力 | 使用真正虚拟滚动方案或分页 |
多选状态维护 | 通过 _selected 字段与 selectedRows 双向绑定 |
使用 Map 维护选中状态,避免性能问题 |
表头全选 | 绑定计算属性控制全选和半选状态 | 结合分页和跨页时需要额外管理 |
选中项展示 | 显示部分已选行的摘要信息 | 提供导出选中 ID / 数据功能 |
数据动态生成 | 支持动态指定数据量重新生成数据 | 支持接口分页拉取更大数据量 |
性能优化 | 延迟模拟数据生成,避免阻塞界面 | 使用 Web Worker 或接口分页处理 |
五、总结
- 本教程演示了大数据量情况下,如何高效使用 Vue3 + Element Plus 虚拟表格实现多选
- 通过单独维护选中列表,确保数据和 UI 状态同步
- 实现了全选、反选、清除选中和已选行展示功能
- 适用于后台管理系统、ERP等大量数据表格场景
如果你需要,我还能帮你将本例升级成带有跨页多选管理、服务端分页整合、性能优化方案的完整版教程,欢迎随时告诉我!