Vue+elementUI的el-table组件,合并单元格,勾选之后,复制、新增、删除、批量复制、批量删除功能
'# Vue+elementUI的el-table组件,合并单元格,勾选之后,复制、新增、删除、批量复制、批量删除功能
一、背景与问题
在企业级应用开发中,el-table组件是数据展示的核心组件之一。当需要处理复杂数据展示需求时,常会遇到以下问题:
- 合并单元格需求:需要将同一组数据中的相同字段合并展示,例如订单列表中同一客户的多条订单合并展示
- 多选操作需求:需要支持勾选行后执行复制、删除等操作
- 批量操作需求:需要支持批量复制、批量删除等操作
- 数据一致性需求:需要保证操作后的数据状态与UI同步
传统开发中,这些问题需要结合el-table的row-class-name、cell-class-name、scope属性等特性进行组合实现,但容易出现性能问题、状态不一致等问题。
二、基本原理
1. 合并单元格原理
elementUI的el-table通过row-class-name和cell-class-name控制单元格样式,但真正的合并需要通过rowspan和colspan属性实现。其核心原理是:
- 通过遍历数据,计算每个单元格的合并行数和列数
- 通过
rowspan和colspan属性控制单元格的合并行为 - 需要维护一个标记数组,记录哪些单元格需要合并
2. 多选操作原理
elementUI的el-table支持多选功能,通过type="selection"列实现。其核心原理是:
- 通过
@selection-change事件获取选中行数据 - 通过
ref获取表格实例,调用toggleRowSelection方法控制行的选中状态 - 需要维护一个选中行的集合,保证数据状态与UI同步
3. 批量操作原理
批量操作需要结合Vue的响应式系统和数组的变异方法,其核心原理是:
- 通过
v-model绑定选中行数据 - 使用
filter、map等数组方法处理数据 - 需要处理数据的深拷贝,避免引用污染
三、环境准备
确保项目中已安装以下依赖:
npm install element-plus在Vue3项目中需要引入:
import { ElTable, ElTableColumn, ElSelect, ElOption, ElInput } from 'element-plus'四、核心实现
1. 合并单元格实现
<template>
<el-table
:data="tableData"
border
:row-class-name="getRowClassName"
:cell-class-name="getCellClassName"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="80"></el-table-column>
<el-table-column prop="client" label="客户名称" width="150"></el-table-column>
<el-table-column prop="product" label="产品名称" width="150"></el-table-column>
<el-table-column prop="quantity" label="数量" width="100"></el-table-column>
<el-table-column prop="price" label="单价" width="100"></el-table-column>
<el-table-column prop="amount" label="金额" width="100"></el-table-column>
</el-table>
</template>
<script>
export default {
data() {
return {
tableData: [
{ id: 1, client: '客户A', product: '产品1', quantity: 2, price: 100, amount: 200, type: 'detail' },
{ id: 2, client: '客户A', product: '产品2', quantity: 3, price: 200, amount: 600, type: 'detail' },
{ id: 3, client: '客户B', product: '产品3', quantity: 1, price: 300, amount: 300, type: 'detail' },
{ id: 4, client: '客户B', product: '产品4', quantity: 4, price: 400, amount: 1600, type: 'detail' },
{ id: 5, client: '客户C', product: '产品5', quantity: 5, price: 500, amount: 2500, type: 'detail' }
],
mergedRows: []
}
},
methods: {
getRowClassName({ row, index }) {
if (row.type === 'header') {
return 'header-row'
}
return ''
},
getCellClassName({ row, column, rowIndex, columnIndex }) {
const client = row.client
const cellIndex = columnIndex
// 处理客户名称列的合并
if (column.property === 'client') {
const isHeader = this.mergedRows.includes(row.id)
if (isHeader) {
return 'merged-cell'
}
return ''
}
// 处理其他列的合并
if (column.property !== 'client' && this.mergedRows.includes(row.id)) {
return 'merged-cell'
}
return ''
}
}
}
</script>
<style scoped>
.header-row {
background-color: #f5f7fa;
}
.merged-cell {
background-color: #e4e7ed;
}
</style>关键代码解释:
getRowClassName方法用于设置表头行的样式getCellClassName方法用于设置合并单元格的样式- 通过遍历数据计算需要合并的行
- 使用
mergedRows数组记录需要合并的行ID
2. 多选操作实现
<template>
<el-table
ref="tableRef"
:data="tableData"
border
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="client" label="客户名称" width="150"></el-table-column>
<el-table-column prop="product" label="产品名称" width="150"></el-table-column>
<el-table-column prop="quantity" label="数量" width="100"></el-table-column>
<el-table-column prop="price" label="单价" width="100"></el-table-column>
<el-table-column prop="amount" label="金额" width="100"></el-table-column>
</el-table>
<div style="margin-top: 20px">
<el-button @click="copySelected">复制选中</el-button>
<el-button @click="deleteSelected">删除选中</el-button>
</div>
</template>
<script>
export default {
data() {
return {
tableData: [
{ id: 1, client: '客户A', product: '产品1', quantity: 2, price: 100, amount: 200 },
{ id: 2, client: '客户A', product: '产品2', quantity: 3, price: 200, amount: 600 },
{ id: 3, client: '客户B', product: '产品3', quantity: 1, price: 300, amount: 300 },
{ id: 4, client: '客户B', product: '产品4', quantity: 4, price: 400, amount: 1600 },
{ id: 5, client: '客户C', product: '产品5', quantity: 5, price: 500, amount: 2500 }
],
selectedRows: []
}
},
methods: {
handleSelectionChange(rows) {
this.selectedRows = rows
},
copySelected() {
const copiedRows = this.selectedRows.map(row => ({ ...row }))
this.tableData = [...this.tableData, ...copiedRows]
this.$refs.tableRef.toggleRowSelection(this.selectedRows)
},
deleteSelected() {
this.tableData = this.tableData.filter(row => !this.selectedRows.includes(row))
}
}
}
</script>关键代码解释:
- 使用
@selection-change事件获取选中行数据 toggleRowSelection方法用于控制行的选中状态- 使用
map方法进行深拷贝,避免引用污染 - 使用
filter方法删除选中行
3. 批量操作实现
<template>
<el-table
ref="tableRef"
:data="tableData"
border
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="client" label="客户名称" width="150"></el-table-column>
<el-table-column prop="product" label="产品名称" width="150"></el-table-column>
<el-table-column prop="quantity" label="数量" width="100"></el-table-column>
<el-table-column prop="price" label="单价" width="100"></el-table-column>
<el-table-column prop="amount" label="金额" width="100"></el-table-column>
</el-table>
<div style="margin-top: 20px">
<el-button @click="batchCopy">批量复制</el-button>
<el-button @click="batchDelete">批量删除</el-button>
</div>
</template>
<script>
export default {
data() {
return {
tableData: [
{ id: 1, client: '客户A', product: '产品1', quantity: 2, price: 100, amount: 200 },
{ id: 2, client: '客户A', product: '产品2', quantity: 3, price: 200, amount: 600 },
{ id: 3, client: '客户B', product: '产品3', quantity: 1, price: 300, amount: 300 },
{ id: 4, client: '客户B', product: '产品4', quantity: 4, price: 400, amount: 1600 },
{ id: 5, client: '客户C', product: '产品5', quantity: 5, price: 500, amount: 2500 }
],
selectedRows: []
}
},
methods: {
handleSelectionChange(rows) {
this.selectedRows = rows
},
batchCopy() {
const copiedRows = this.selectedRows.map(row => ({ ...row }))
this.tableData = [...this.tableData, ...copiedRows]
this.$refs.tableRef.toggleRowSelection(this.selectedRows)
},
batchDelete() {
this.tableData = this.tableData.filter(row => !this.selectedRows.includes(row))
}
}
}
</script>关键代码解释:
- 使用
toggleRowSelection方法控制行的选中状态 - 使用
map方法进行深拷贝,避免引用污染 - 使用
filter方法删除选中行 - 批量操作需要处理大量数据时,注意性能优化
五、完整案例
1. 案例需求
实现一个订单管理界面,包含:
- 合并客户名称列
- 支持多选操作
- 支持复制、删除、批量复制、批量删除
- 支持新增行
2. 完整代码
<template>
<div style="padding: 20px">
<el-button type="primary" @click="addRow">新增行</el-button>
<el-button type="success" @click="batchCopy">批量复制</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-table
ref="tableRef"
:data="tableData"
border
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
style="width: 100%; margin-top: 20px"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="client" label="客户名称" width="150"></el-table-column>
<el-table-column prop="product" label="产品名称" width="150"></el-table-column>
<el-table-column prop="quantity" label="数量" width="100"></el-table-column>
<el-table-column prop="price" label="单价" width="100"></el-table-column>
<el-table-column prop="amount" label="金额" width="100"></el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
tableData: [
{ id: 1, client: '客户A', product: '产品1', quantity: 2, price: 100, amount: 200, type: 'detail' },
{ id: 2, client: '客户A', product: '产品2', quantity: 3, price: 200, amount: 600, type: 'detail' },
{ id: 3, client: '客户B', product: '产品3', quantity: 1, price: 300, amount: 300, type: 'detail' },
{ id: 4, client: '客户B', product: '产品4', quantity: 4, price: 400, amount: 1600, type: 'detail' },
{ id: 5, client: '客户C', product: '产品5', quantity: 5, price: 500, amount: 2500, type: 'detail' }
],
selectedRows: [],
mergedRows: []
}
},
mounted() {
this.calculateMergedRows()
},
methods: {
calculateMergedRows() {
this.mergedRows = []
const clients = []
const rowIds = []
this.tableData.forEach((row, index) => {
if (row.type === 'detail') {
if (clients.length > 0) {
const lastClient = clients[clients.length - 1]
if (lastClient.client === row.client) {
rowIds[rowIds.length - 1] = row.id
} else {
clients.push(row)
rowIds.push(row.id)
}
} else {
clients.push(row)
rowIds.push(row.id)
}
}
})
this.mergedRows = rowIds
},
handleSelectionChange(rows) {
this.selectedRows = rows
},
handleRowClick(row) {
if (row.type === 'header') {
this.$refs.tableRef.toggleRowSelection(row)
}
},
addRow() {
const newId = this.tableData.length + 1
const newClient = this.tableData[this.tableData.length - 1].client
this.tableData.push({
id: newId,
client: newClient,
product: '新产品',
quantity: 1,
price: 100,
amount: 100,
type: 'detail'
})
this.calculateMergedRows()
},
batchCopy() {
const copiedRows = this.selectedRows.map(row => ({ ...row }))
this.tableData = [...this.tableData, ...copiedRows]
this.$refs.tableRef.toggleRowSelection(this.selectedRows)
},
batchDelete() {
this.tableData = this.tableData.filter(row => !this.selectedRows.includes(row))
this.calculateMergedRows()
}
}
}
</script>
<style scoped>
.el-table {
border: 1px solid #ebeef5;
}
</style>关键代码解释:
calculateMergedRows方法计算需要合并的行handleRowClick方法处理行点击事件,支持头行和明细行的切换addRow方法新增行时重新计算合并行- 批量操作后需要重新计算合并行
六、源码解析
1. 合并单元格实现细节
calculateMergedRows() {
this.mergedRows = []
const clients = []
const rowIds = []
this.tableData.forEach((row, index) => {
if (row.type === 'detail') {
if (clients.length > 0) {
const lastClient = clients[clients.length - 1]
if (lastClient.client === row.client) {
rowIds[rowIds.length - 1] = row.id
} else {
clients.push(row)
rowIds.push(row.id)
}
} else {
clients.push(row)
rowIds.push(row.id)
}
}
})
this.mergedRows = rowIds
}这个方法遍历所有行,计算需要合并的行ID。通过维护一个客户端列表和行ID列表,当遇到相同客户名称时,将当前行ID加入到前一个客户的行ID列表中。最终生成的mergedRows数组包含所有需要合并的行ID。
2. 多选操作实现细节
handleSelectionChange(rows) {
this.selectedRows = rows
}这个方法通过@selection-change事件获取选中行,将选中行存储在selectedRows中,用于后续的复制和删除操作。
3. 批量操作实现细节
batchCopy() {
const copiedRows = this.selectedRows.map(row => ({ ...row }))
this.tableData = [...this.tableData, ...copiedRows]
this.$refs.tableRef.toggleRowSelection(this.selectedRows)
}这个方法通过map创建深拷贝,然后将复制的行添加到数据源中,最后调用toggleRowSelection方法保持选中状态。
七、进阶使用
1. 动态合并单元格
calculateMergedRows() {
this.mergedRows = []
const clients = []
const rowIds = []
this.tableData.forEach((row, index) => {
if (row.type === 'detail') {
if (clients.length > 0) {
const lastClient = clients[clients.length - 1]
if (lastClient.client === row.client) {
rowIds[rowIds.length - 1] = row.id
} else {
clients.push(row)
rowIds.push(row.id)
}
} else {
clients.push(row)
rowIds.push(row.id)
}
}
})
this.mergedRows = rowIds
}这个方法可以动态计算需要合并的行,当数据变化时重新计算。
2. 新增行时自动合并
addRow() {
const newId = this.tableData.length + 1
const newClient = this.tableData[this.tableData.length - 1].client
this.tableData.push({
id: newId,
client: newClient,
product: '新产品',
quantity: 1,
price: 100,
amount: 100,
type: 'detail'
})
this.calculateMergedRows()
}新增行后立即重新计算合并行,确保合并状态正确。
3. 批量操作时的状态保持
batchDelete() {
this.tableData = this.tableData.filter(row => !this.selectedRows.includes(row))
this.calculateMergedRows()
}删除行后重新计算合并行,确保合并状态正确。
八、性能与工程实践
1. 性能优化
- 虚拟滚动:对于大数据量的表格,使用虚拟滚动技术减少DOM节点数量
- 懒加载:对大量数据进行分页加载,避免一次性加载过多数据
- 防抖处理:对于频繁触发的批量操作,使用防抖函数减少不必要的计算
2. 状态管理
- 使用
ref获取表格实例,避免直接操作DOM - 使用
v-model绑定选中行数据,确保数据一致性 - 对关键数据进行深拷贝,避免引用污染
3. 异常处理
- 对数据进行校验,确保数据完整性
- 对操作进行防重处理,避免重复操作
- 对错误进行捕获,防止程序崩溃
九、常见问题与踩坑
1. 合并单元格错误
问题:合并单元格时出现错位
原因:合并行的计算逻辑错误,导致行索引不正确
解决方法:仔细检查合并行的计算逻辑,确保索引正确
2. 多选操作异常
问题:多选操作后数据状态不一致
原因:未正确处理数据的深拷贝
解决方法:使用map创建深拷贝,避免引用污染
3. 批量操作性能问题
问题:大数据量下批量操作导致页面卡顿
原因:频繁操作DOM节点导致性能问题
解决方法:使用虚拟滚动、分页等技术优化性能
十、最佳实践
1. 合并单元格的最佳实践
- 使用
rowspan和colspan属性实现合并 - 通过遍历数据计算需要合并的行
- 使用标记数组记录需要合并的行
2. 多选操作的最佳实践
- 使用
@selection-change事件获取选中行 - 使用
toggleRowSelection方法控制行的选中状态 - 使用
map创建深拷贝,避免引用污染
3. 批量操作的最佳实践
- 使用数组的变异方法处理数据
- 对关键数据进行深拷贝
- 确保操作后的数据状态与UI同步
十一、总结
通过本文的深入分析,我们了解了在Vue+elementUI的el-table组件中实现合并单元格、多选操作、批量操作的原理和实现方法。在实际开发中,需要注意以下几点:
- 合并单元格需要通过
rowspan和colspan属性实现,需要仔细计算合并行 - 多选操作需要使用
@selection-change事件获取选中行,并通过toggleRowSelection方法控制状态 - 批量操作需要使用数组的变异方法处理数据,并注意深拷贝避免引用污染
- 性能优化需要考虑虚拟滚动、分页等技术,避免大数据量导致的性能问题
- 异常处理需要对数据进行校验,确保数据完整性
在实际项目中,这种方案适用于需要展示分组数据的表格,但需要注意在大数据量场景下进行性能优化。通过合理使用这些技术,可以创建出功能完善、性能良好的数据展示组件。
评论已关闭