内容简介:
通过使用Element-ui 表格组件,实现主表子表在同一页面展示,同时支持折叠和主子表的Checkbox。
需求场景:
在项目开发中可能会遇到主表和子表需要同时展示的情况,同时用户需要选中主表和子表中的数据对这些数据进行不同的操作。
比较常见的场景是购物车还有一些不同层级展示交互的场景,笔者本次遇到的是用户对子表操作完成后才会对主表的内容进行操作,且需要展示在同一页面,便于用户进行操作,最大程度增加页面信息熵。
最终需要实现如下效果:
实现方案:
版本:
node:v14.17.6
vue:2.6.10
1.首先我们需要确定主表子表结构Element-ui的组件能否支持,在Element的官网中找到表格支持展开行。
<template>
<el-table
:data="tableData"
style="width: 100%">
<el-table-column type="expand">
<template slot-scope="props">
<el-form label-position="left" inline class="demo-table-expand">
<el-form-item label="商品名称">
<span>{{ props.row.name }}</span>
</el-form-item>
<el-form-item label="所属店铺">
<span>{{ props.row.shop }}</span>
</el-form-item>
<el-form-item label="商品 ID">
<span>{{ props.row.id }}</span>
</el-form-item>
<el-form-item label="店铺 ID">
<span>{{ props.row.shopId }}</span>
</el-form-item>
<el-form-item label="商品分类">
<span>{{ props.row.category }}</span>
</el-form-item>
<el-form-item label="店铺地址">
<span>{{ props.row.address }}</span>
</el-form-item>
<el-form-item label="商品描述">
<span>{{ props.row.desc }}</span>
</el-form-item>
</el-form>
</template>
</el-table-column>
<el-table-column
label="商品 ID"
prop="id">
</el-table-column>
<el-table-column
label="商品名称"
prop="name">
</el-table-column>
<el-table-column
label="描述"
prop="desc">
</el-table-column>
</el-table>
</template>
<style>
.demo-table-expand {
font-size: 0;
}
.demo-table-expand label {
width: 90px;
color: #99a9bf;
}
.demo-table-expand .el-form-item {
margin-right: 0;
margin-bottom: 0;
width: 50%;
}
</style>
<script>
export default {
data() {
return {
tableData: [{
id: '12987122',
name: '好滋好味鸡蛋仔',
category: '江浙小吃、小吃零食',
desc: '荷兰优质淡奶,奶香浓而不腻',
address: '上海市普陀区真北路',
shop: '王小虎夫妻店',
shopId: '10333'
}, {
id: '12987123',
name: '好滋好味鸡蛋仔',
category: '江浙小吃、小吃零食',
desc: '荷兰优质淡奶,奶香浓而不腻',
address: '上海市普陀区真北路',
shop: '王小虎夫妻店',
shopId: '10333'
}, {
id: '12987125',
name: '好滋好味鸡蛋仔',
category: '江浙小吃、小吃零食',
desc: '荷兰优质淡奶,奶香浓而不腻',
address: '上海市普陀区真北路',
shop: '王小虎夫妻店',
shopId: '10333'
}, {
id: '12987126',
name: '好滋好味鸡蛋仔',
category: '江浙小吃、小吃零食',
desc: '荷兰优质淡奶,奶香浓而不腻',
address: '上海市普陀区真北路',
shop: '王小虎夫妻店',
shopId: '10333'
}]
}
}
}
</script>
通过代码可以知道使用<template slot-scope="props">可以插入子表,该组件满足要求。
2.展开可以满足之后,还需要调查一下是否有方法可以满足我们对勾选框的需求:
toggleSelection(rows) {
if (rows) {
rows.forEach(row => {
this.$refs.multipleTable.toggleRowSelection(row);
});
} else {
this.$refs.multipleTable.clearSelection();
}
}
通过以上例子与函数说明,可以判断需求应该是可以通过该组件实现的。
3.接下来开始设计数据结构,为了简便期间暂不实现子表的异步获取,主表与子表数据放在同一个数组中;同时设计两个数组,接收主表和子表CheckBox选中的数据。
data() {
return {
open: true,
tableData: [{
id: '12987122',
name: '张三',
status: '审批中',
childData: [
{
selfId: 1,
productId: 120001,
productName: '苹果',
productQuantity: 10,
id: 12987122
},
{
selfId: 2,
productId: 120002,
productName: '香蕉',
productQuantity: 15,
id: 12987122
},
{
selfId: 3,
productId: 120003,
productName: '梨子',
productQuantity: 20,
id: 12987122
}
]
}, {
id: '12987123',
name: '赵四',
status: '审批中',
childData: [
{
selfId: 4,
productId: 120004,
productName: '桃子',
productQuantity: 8,
id: 12987123
},
{
selfId: 5,
productId: 120005,
productName: '芒果',
productQuantity: 88,
id: 12987123
}
]
}, {
id: '12987125',
name: '王五',
status: '已完成',
childData: [
{
selfId: 6,
productId: 120006,
productName: '李子',
productQuantity: 888,
id: 12987125
},
{
selfId: 7,
productId: 120007,
productName: '葡萄',
productQuantity: 666,
id: 12987125
}
]
}],
mainSelections: [],
childSelections: []
}
}
mainSelections和childSelections分别用于接收主表选中数据和子表选中数据,为了理解方便id统一指代主表id,selfId统一指代子表id(实际开发中尽量不要这样命名)。
4.设计好数据结构后,开始画界面:
<template>
<div>
<el-table
:data="tableData"
ref="mainTable"
:default-expand-all="open"
@expand-change="expandChange"
@selection-change="handleMainSelectionChange"
style="width: 100%">
<el-table-column
type="selection"
width="55"/>
<el-table-column type="expand">
<template slot-scope="props">
<el-table
:key="'childTable'+props.row.id"
header-cell-class-name="selectAllbtnDis"
:ref="'childTable'+props.row.id"
@select="handleChildSelect"
:data="props.row.childData"
style="width: 100%;margin-left: 25px">
<el-table-column
type="selection"
width="55"/>
<el-table-column
label="产品号"
prop="productId">
</el-table-column>
<el-table-column
label="产品名称"
prop="productName">
</el-table-column>
<el-table-column
label="产品数量"
prop="productQuantity">
</el-table-column>
<el-table-column
label="单据号"
prop="id">
</el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column
label="单据号 ID"
prop="id">
</el-table-column>
<el-table-column
label="处理人"
prop="name">
</el-table-column>
<el-table-column
label="处理状态"
prop="status">
</el-table-column>
</el-table>
</div>
</template>
5.接下来需要实现最关键的CheckBox主子表联动以及选中数据获取:
<el-table
:data="tableData"
ref="mainTable"
:default-expand-all="open"
@expand-change="expandChange"
@selection-change="handleMainSelectionChange"
style="width: 100%">
handleMainSelectionChange(selection) {
if (selection.length > this.mainSelections.length) {
let diffifentElements = this.findDiffifent(selection, this.mainSelections)
this.toggleRowTrue(diffifentElements);
} else {
let diffifentElements = this.findDiffifent(this.mainSelections, selection)
this.toggleRowFalse(diffifentElements)
}
this.mainSelections = [...selection]
}
handleMainSelectionChange函数主要用于处理主表的CheckBox选中变化,因为最终我们将主表选中的数据存放在mainSelections中,我们将比对当前选中项和我们存下来的数据之间的变化,由此来判断到底是勾中CheckBox还是取消CheckBox,如果selection的长度大于mainSelections的长度则操作是勾中一个新选项,如果小于则操作为取消一个选项。
findDiffifent(externalArray, internalArray) {
let diffifentElements = []
for (const exterElement of externalArray) {
let found = false
for (const internalElement of internalArray) {
if (internalElement.id == exterElement.id) {
found = true
break
}
}
if (!found) {
diffifentElements.push(exterElement)
}
}
return diffifentElements;
},
findDiffifent函数主要用于找出数组中的不同项
toggleRowTrue(selections) {
for (const selection of selections) {
if (selection.childData != null) {
const _this = this
selection.childData.forEach(row => {
if (!(_this.childSelections.find(item => item.selfId == row.selfId))) {
this.childSelections.push(row)
let ref = eval('this.$refs.childTable' + row.id)
if (ref) {
ref.toggleRowSelection(row, true);
}
}
})
}
}
},
toggleRowFalse(selections) {
for (const selection of selections) {
if (selection.childData != null) {
let childSelect = this.childSelections.filter(item => item.id == selection.id)
let mainSelect = this.tableData.find(item => item.id == selection.id)
if (childSelect.length >= mainSelect.childData.length) {
selection.childData.forEach(row => {
this.childSelections = this.childSelections.filter(item => item != row)
let ref = eval('this.$refs.childTable' + row.id)
if (ref) {
ref.toggleRowSelection(row, false);
}
})
}
}
}
}
toggleRowTrue与toggleRowFalse分别对应选中和取消两种状态,因为childSelections在选中和取消时会有不同操作,所以分为了两个函数。
<el-table
:key="'childTable'+props.row.id"
header-cell-class-name="selectAllbtnDis"
:ref="'childTable'+props.row.id"
@select="handleChildSelect"
:data="props.row.childData"
style="width: 100%;margin-left: 25px">
因为主表的每条数据都对应一个子表,需注意ref的设置方式,这里采用了childTable作为前缀,主表的ID作为后缀,确定唯一一张子表
let ref = eval('this.$refs.childTable' + row.id)
if (ref) {
ref.toggleRowSelection(row, true);
}
需要注意,Element-ui表格展开的dom只有在展开时才会渲染,所以需要判断一下ref是否存在。
完成以上函数就可以实现如下效果:
6.现在我们剩下的问题是选中子表全部选项对应的主表的也应该被选中,类似某宝购物车的状态
handleChildSelect(selection, row) {
let appear = false
for (const selectionElement of selection) {
if (row.selfId == selectionElement.selfId) {
this.childSelections.push(selectionElement)
appear = true
break
}
}
if (!appear) {
this.childSelections = this.childSelections.filter(item => item != row)
}
let select = this.childSelections.filter(item => item.id == row.id)
let mainSelect = this.tableData.find(item => item.id == row.id)
if (select.length < mainSelect.childData.length) {
this.$refs.mainTable.toggleRowSelection(mainSelect, false)
} else {
this.mainSelections.push(mainSelect)
this.$refs.mainTable.toggleRowSelection(mainSelect, true)
}
}
首先通过循环判断子表选中的元素到底是选中还是取消,如果selection中存在容row则代表是选中状态,如果不是则代表取消状态,childSelections中要添加或删除对应的元素。
接下来判断操作的子表数据是否全部选中,如果全部选中则对应的主表数据也应该选中,如果不是则主表数据不应该被选中。这里没有再判断dom是否存在,一般来说能选中CheckBox元素是肯定渲染好了的。
最终实现如下效果:
7.到这里主体CheckBox相关的功能基本实现了,但是还有一些小问题,之前说过如果不展开dom是不会渲染的,将会出现如下现象:
子表的CheckBox状态会随着折叠而消失,解决这个问题需要在主表做折叠时做某些处理:
<el-table
:data="tableData"
ref="mainTable"
:default-expand-all="open"
@expand-change="expandChange"
@selection-change="handleMainSelectionChange"
style="width: 100%">
expandChange(row, expanded) {
if (expanded.find(item => item == row)) {
row.childData.forEach(child => {
if (this.childSelections.find(item => item.selfId == child.selfId)) {
const _this = this
//在DOM渲染完毕之后立即执行
this.$nextTick(() => {
let ref = eval('_this.$refs.childTable' + child.id)
ref.toggleRowSelection(child, true);
})
}
})
}
}
expandChange函数就是为了解决这个问题设计的,通过expanded.find(item => item == row)判断是展开操作还是折叠操作,如果折叠操作我们不做处理;当组件展开时,判断当前主表行中的子表数据列是否存在childSelections(前面已经将所有选中的子表数据存入该数组中)当中,如果存在则将其设置为勾选状态。
注意:设置勾选状态时要在DOM渲染后再执行,需使用this.$nextTick(() =>{})进行操作
最终呈现如下效果:
全部代码,仅供参考:
<template>
<div>
<el-table
:data="tableData"
ref="mainTable"
:default-expand-all="open"
@expand-change="expandChange"
@selection-change="handleMainSelectionChange"
style="width: 100%">
<el-table-column
type="selection"
width="55"/>
<el-table-column type="expand">
<template slot-scope="props">
<el-table
:key="'childTable'+props.row.id"
header-cell-class-name="selectAllbtnDis"
:ref="'childTable'+props.row.id"
@select="handleChildSelect"
:data="props.row.childData"
style="width: 100%;margin-left: 25px">
<el-table-column
type="selection"
width="55"/>
<el-table-column
label="产品号"
prop="productId">
</el-table-column>
<el-table-column
label="产品名称"
prop="productName">
</el-table-column>
<el-table-column
label="产品数量"
prop="productQuantity">
</el-table-column>
<el-table-column
label="单据号"
prop="id">
</el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column
label="单据号 ID"
prop="id">
</el-table-column>
<el-table-column
label="处理人"
prop="name">
</el-table-column>
<el-table-column
label="处理状态"
prop="status">
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'index',
data() {
return {
open: true,
tableData: [{
id: '12987122',
name: '张三',
status: '审批中',
childData: [
{
selfId: 1,
productId: 120001,
productName: '苹果',
productQuantity: 10,
id: 12987122
},
{
selfId: 2,
productId: 120002,
productName: '香蕉',
productQuantity: 15,
id: 12987122
},
{
selfId: 3,
productId: 120003,
productName: '梨子',
productQuantity: 20,
id: 12987122
}
]
}, {
id: '12987123',
name: '赵四',
status: '审批中',
childData: [
{
selfId: 4,
productId: 120004,
productName: '桃子',
productQuantity: 8,
id: 12987123
},
{
selfId: 5,
productId: 120005,
productName: '芒果',
productQuantity: 88,
id: 12987123
}
]
}, {
id: '12987125',
name: '王五',
status: '已完成',
childData: [
{
selfId: 6,
productId: 120006,
productName: '李子',
productQuantity: 888,
id: 12987125
},
{
selfId: 7,
productId: 120007,
productName: '葡萄',
productQuantity: 666,
id: 12987125
}
]
}],
mainSelections: [],
childSelections: []
}
},
watch: {
mainSelections() {
console.log('main', this.mainSelections)
},
childSelections() {
console.log('child', this.childSelections)
}
},
methods: {
toggleRowTrue(selections) {
for (const selection of selections) {
if (selection.childData != null) {
const _this = this
selection.childData.forEach(row => {
if (!(_this.childSelections.find(item => item.selfId == row.selfId))) {
this.childSelections.push(row)
let ref = eval('this.$refs.childTable' + row.id)
if (ref) {
ref.toggleRowSelection(row, true);
}
}
})
}
}
},
toggleRowFalse(selections) {
for (const selection of selections) {
if (selection.childData != null) {
let childSelect = this.childSelections.filter(item => item.id == selection.id)
let mainSelect = this.tableData.find(item => item.id == selection.id)
if (childSelect.length >= mainSelect.childData.length) {
selection.childData.forEach(row => {
this.childSelections = this.childSelections.filter(item => item != row)
let ref = eval('this.$refs.childTable' + row.id)
if (ref) {
ref.toggleRowSelection(row, false);
}
})
}
}
}
},
findDiffifent(externalArray, internalArray) {
let diffifentElements = []
for (const exterElement of externalArray) {
let found = false
for (const internalElement of internalArray) {
if (internalElement.id == exterElement.id) {
found = true
break
}
}
if (!found) {
diffifentElements.push(exterElement)
}
}
return diffifentElements;
},
handleMainSelectionChange(selection) {
if (selection.length > this.mainSelections.length) {
let diffifentElements = this.findDiffifent(selection, this.mainSelections)
this.toggleRowTrue(diffifentElements);
} else {
let diffifentElements = this.findDiffifent(this.mainSelections, selection)
this.toggleRowFalse(diffifentElements)
}
this.mainSelections = [...selection]
},
handleChildSelect(selection, row) {
let appear = false
for (const selectionElement of selection) {
if (row.selfId == selectionElement.selfId) {
this.childSelections.push(selectionElement)
appear = true
break
}
}
if (!appear) {
this.childSelections = this.childSelections.filter(item => item != row)
}
let select = this.childSelections.filter(item => item.id == row.id)
let mainSelect = this.tableData.find(item => item.id == row.id)
if (select.length < mainSelect.childData.length) {
this.$refs.mainTable.toggleRowSelection(mainSelect, false)
} else {
this.mainSelections.push(mainSelect)
this.$refs.mainTable.toggleRowSelection(mainSelect, true)
}
},
expandChange(row, expanded) {
if (expanded.find(item => item == row)) {
row.childData.forEach(child => {
if (this.childSelections.find(item => item.selfId == child.selfId)) {
console.log(this.$refs)
const _this = this
//在DOM渲染完毕之后立即执行
this.$nextTick(() => {
let ref = eval('_this.$refs.childTable' + child.id)
ref.toggleRowSelection(child, true);
})
}
})
}
}
}
}
</script>
<style scoped>
::v-deep .selectAllbtnDis .cell .el-checkbox__inner {
display: none;
}
</style>