开发后台管理项目时,遇到了一个表格嵌套表格并且需要支持主副表多选联动的需求,因为项目的组件库是elementUI,在网上找到几篇文章,多半都有点bug,不过提供了一些思路,最后解决问题,所以整理了这么一篇文章。
Part1实现效果
Part2代码实现
1template模板部分
因为实际开发的时候,展开嵌入的表格往往比较复杂,这里就单独把嵌入的表格抽取成一个组件
主表模板部分代码:
<template>
<div>
<el-table
:data="tableData"
ref="tableSelect"
:row-class-name="getRowClassName"
:header-row-class-name="getHeaderRowClassName"
@select-all="mainSelectAll"
@select="mainSelect"
@expand-change="handleExpandChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="expand">
<template slot-scope="scope">
<expand-table
:data="scope.row.detailList"
:ref="'sub' + scope.$index"
@select="(selection) => subSelect(scope.$index, selection)"
@select-all="(selection) => subSelectAll(scope.$index, selection)"
/>
</template>
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="350"
show-overflow-tooltip
>
<template slot-scope="props">
{{ props.row.name }}
</template>
</el-table-column>
<el-table-column
prop="age"
label="年龄"
width="350"
show-overflow-tooltip
>
<template slot-scope="props">
{{ props.row.age }}
</template>
</el-table-column>
<el-table-column
prop="address"
label="地址"
width="350"
show-overflow-tooltip
>
<template slot-scope="props">
{{ props.row.address }}
</template>
</el-table-column>
</el-table>
</div>
</template>
嵌入子表格代码:
<template>
<el-table
:data="data"
border
stripe
size="small"
ref="subTable"
@select="handleSelect"
@select-all="handleSelectAll"
>
<el-table-column type="selection" width="100" align="center" />
<el-table-column
prop="computerName"
label="电脑名称"
width="350"
show-overflow-tooltip
>
<template slot-scope="props">
{{ props.row.computerName }}
</template>
</el-table-column>
<el-table-column
prop="phoneName"
label="手机名称"
width="350"
show-overflow-tooltip
>
<template slot-scope="props">
{{ props.row.phoneName }}
</template>
</el-table-column>
<el-table-column
prop="carName"
label="座驾"
width="350"
show-overflow-tooltip
>
<template slot-scope="props">
{{ props.row.carName }}
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
props: {
data: Array
},
methods: {
/**
* 处理子表单行选中事件。
* 向父组件派发选中事件。
* @param {Array} selection - 子表中选中的行。
*/
handleSelect (selection) {
this.$emit('select', selection)
},
/**
* 处理子表全选事件。
* 向父组件派发全选事件。
* @param {Array} selection - 子表中选中的行。
*/
handleSelectAll (selection) {
this.$emit('select-all', selection)
},
/**
* 获取子表的实际el-table实例。
* @returns {Object} el-table实例。
*/
getTableRef () {
return this.$refs.subTable
}
}
}
</script>
2data部分数据格式以及选中数据的存储
data () {
return {
tableData: [
{
id: '1',
name: '王小虎',
age: '18',
address: '上海市普陀区金沙江路 1518 弄',
detailList: [
{
id: '11',
computerName: 'MacBook Pro',
phoneName: 'iPhone 15 PRO MAX',
carName: 'Audi A8'
},
{
id: '12',
computerName: 'HUAWEI MateBook X Pro',
phoneName: 'HUAWEI MATE 60 PRO',
carName: 'AITO M7 ULTRA'
}
]
},
{
id: '2',
name: '王小马',
age: '25',
address: '北京市海淀区万柳书院1号',
detailList: [
{
id: '21',
computerName: 'MacBook Pro',
phoneName: 'iPhone 15 PRO MAX',
carName: 'Audi A8'
},
{
id: '22',
computerName: 'XIAOMI NOTEBOOK PRO',
phoneName: 'XIAOMI 14 ULTRA',
carName: 'XIAOMI SU7 MAX'
}
]
}
],
selectedData: [] // 选中的数据
}
},
3methods具体实现方法
1、首先是格式化数据,给数据的每一条插入isChecked属性标识当前数据是否选中,一是方便最终过滤出选中的数据,二是方便展开时候的回显。
/**
* tableData
* @returns {Array} 初始化后的数据列表。
*/
initialize () {
this.tableData = this.tableData.map(item => ({
...item,
isChecked: false,
detailList: item.detailList.map(i => ({
...i,
isChecked: false
}))
}))
},
2、处理主表全选
主表的选中有一个注意的点,也是参考的几个文章都有的bug,就是当子表没展开时,获取子表的ref会是undefined,就设置不了子表的选中态,所以就通过方法给自动展开,然后就能通过ref操作到子表,进行选中。其实也可以先从数据层面上先改变选中的标识,再在展开时操作,不过没有尝试,只是按照自动展开实现。
/**
* 处理主表全选事件。
* 展开所有子表,并选中所有子表中的行。
* 更新selectedList以反映当前选中状态。
* @param {Array} selected - 主表中选中的行。
*/
mainSelectAll (selected) {
this.tableData.forEach((item, index) => {
this.$refs.tableSelect.toggleRowExpansion(item, true) // 展开子表
item.isChecked = selected.length === this.tableData.length // 判断是否全选
const subTable = this.$refs[`sub${index}`]?.getTableRef()
if (subTable) {
subTable.clearSelection()
// 判断是否全选
if (selected.length === this.tableData.length) {
subTable.toggleAllSelection()
item.detailList.forEach(i => {
i.isChecked = true
})
} else {
item.detailList.forEach(item => {
item.isChecked = false
})
}
} else {
item.detailList.forEach(item => {
item.isChecked = selected.length === this.tableData.length
})
}
})
this.updateSelectedList()
},
3、处理主表单行选中
/**
* 处理主表单行选中事件。
* 展开对应的子表,并选中子表中的行。
* 更新selectedList以反映当前选中状态。
* @param {Array} selection - 主表中选中的行。
* @param {Object} row - 当前选中的行。
*/
mainSelect (selection, row) {
this.$refs.tableSelect.toggleRowExpansion(row, true) // 展开子表
row.isChecked = selection.includes(row)
const subTable = this.$refs[`sub${this.tableData.indexOf(row)}`]?.getTableRef()
if (subTable) {
subTable.clearSelection()
if (selection.includes(row)) {
row.detailList.forEach(item => {
item.isChecked = true
subTable.toggleRowSelection(item, true)
})
} else {
row.detailList.forEach(item => {
item.isChecked = false
})
}
} else {
row.detailList.forEach(item => {
item.isChecked = selection.includes(row)
})
}
this.updateSelectedList()
},
4、处理子表全选事件
/**
* 处理子表全选事件。
* 如果子表有选中行,则主表对应行被选中。
* 更新selectedList以反映当前选中状态。
* @param {number} index - 子表的索引。
* @param {Array} selection - 子表中选中的行。
*/
subSelectAll (index, selection) {
const mainTable = this.$refs.tableSelect
const mainItem = this.tableData[index]
if (selection.length === mainItem.detailList.length) {
mainItem.isChecked = true
} else {
mainItem.isChecked = false
}
mainItem.detailList.forEach(attachment => {
attachment.isChecked = selection.includes(attachment)
})
mainTable.toggleRowSelection(mainItem, mainItem.isChecked)
this.updateSelectedList()
},
5、处理子表的单选
/**
* 处理子表单行选中事件。
* 如果子表选中行数等于数据长度,则全选,小于则半选,否则不选。
* 更新selectedList以反映当前选中状态。
* @param {number} index - 子表的索引。
* @param {Array} selection - 子表中选中的行。
*/
subSelect (index, selection) {
const mainItem = this.tableData[index]
if (selection.length > 0 && selection.length === mainItem.detailList.length) {
mainItem.isChecked = true
} else if (selection.length > 0 && selection.length < mainItem.detailList.length) {
mainItem.isChecked = ''
} else {
mainItem.isChecked = false
this.$refs.tableSelect.toggleRowExpansion(mainItem, false)
}
this.toggleRowSelection(mainItem, mainItem.isChecked)
mainItem.detailList.forEach(attachment => {
attachment.isChecked = selection.includes(attachment)
})
this.updateSelectedList()
},
6、其他调用到的函数方法(展开子表,切换当前选中态、获取半选类名...)
/**
* 处理主表展开或折叠事件。
* 在主表展开行时,根据isChecked恢复子表的选中状态。
* @param {Object} row - 展开或折叠的行。
* @param {Array} expandedRows - 当前展开的行数组。
*/
handleExpandChange (row, expandedRows) {
if (expandedRows.includes(row)) {
this.$nextTick(() => {
const subTable = this.$refs[`sub${this.tableData.indexOf(row)}`]?.getTableRef()
if (subTable) {
this.$nextTick(() => {
row.detailList.forEach(item => {
subTable.toggleRowSelection(item, item.isChecked === true)
})
})
}
})
}
},
/**
* 更新selectedList以反映当前选中状态。
* 筛选出tableList中主表和子表都被选中的行。
*/
updateSelectedList () {
const selectedList = this.tableData
.filter(item => item.isChecked === true || item.isChecked === '')
.map(it => {
return {
...it,
detailList: it.detailList.filter(item => item.isChecked)
}
})
this.selectedData = selectedList
// 当没有选中的时候,解决不会自动去除表头半选bug,手动隐藏表头的选中状态(去除indeterminate的css类名)
if (!this.selectedData.length) {
const headerRow = document.querySelector('.main-table-header')
headerRow && headerRow.classList.remove('indeterminate')
}
},
// 设置当前行的选中态
toggleRowSelection (row, flag) {
if (row) {
this.$nextTick(() => {
this.$refs.tableSelect &&
this.$refs.tableSelect.toggleRowSelection(row, flag)
})
}
},
// 表格行样式 当当前行的状态为不明确状态时,添加样式,使其复选框为不明确状态样式
getRowClassName ({ row }) {
if (row.isChecked === '') {
return 'indeterminate'
}
},
// 表格标题样式 当一级目录有为不明确状态时,添加样式,使其全选复选框为不明确状态样式
getHeaderRowClassName ({ row }) {
const isIndeterminate = this.tableData.some(item => item.isChecked === '')
if (isIndeterminate) {
return 'indeterminate main-table-header'
}
return 'main-table-header'
}
7、半选选中样式部分代码
.indeterminate .el-checkbox__input .el-checkbox__inner {
background-color: #409eff !important;
border-color: #409eff !important;
color: #fff !important;
}
.indeterminate .el-checkbox__input.is-checked .el-checkbox__inner::after {
transform: scale(0.5);
}
.indeterminate .el-checkbox__input .el-checkbox__inner {
background-color: #f2f6fc;
border-color: #dcdfe6;
}
.indeterminate .el-checkbox__input .el-checkbox__inner::after {
border-color: #c0c4cc !important;
background-color: #c0c4cc;
}
.indeterminate .el-checkbox__input .el-checkbox__inner::after {
content: '';
position: absolute;
display: block;
background-color: #fff;
height: 2px;
transform: scale(0.5);
left: 0;
right: 0;
top: 5px;
width: auto !important;
}
以上就是最终实现的方法,代码还有不少可以优化的地方,可以更精简的判断选中,以及更简单的操作表格的选中等,以及选中主表,子表自动展开这个操作是否合理,以及子表全部取消选中,主表状态的切换,目前也只是很粗暴的操作类名去改变样式,思路是大概没问题,实现还有可以优化,有精力的伙伴们可以去优化,可以在评论提醒我去学习,没精力去优化,着急解决问题,把效果做出来的伙伴们,直接复制去用应该也能解决问题。