在实际业务开发中,我们经常遇到主表格嵌套子表格,且需要在主表和子表上都提供复选框功能,并实现三态联动(全选、半选、未选)。本文将基于 Vue 3 + vxe-table 组件库,手把手教你实现一个支持父子表复选框联动的完整示例。
主表每行都有一个复选框,点击可同时控制其下所有子行;子表内部复选框单独控制,并能反向影响父行状态;表头提供全选/全不选按钮,支持全局批量操作
效果
- 主表行复选框 → 勾选时,该行下所有子行自动勾选;取消时,子行全部取消。
- 子表行复选框 → 勾选/取消时,自动更新所属父行的选中状态(全选/半选/未选)。
- 表头全选 → 一键选中/取消所有主行及其所有子行。
- 半选状态 → 当某个主行下只有部分子行被选中时,该行复选框显示半选(indeterminate)状态;表头同理。
实现思路
vxe-table 提供了强大的模板插槽能力,我们利用三个插槽来实现自定义复选框:
- cell_one_checkbox – 主表每行的复选框插槽
- head_one_checkbox – 表头的全选/半选插槽
- expand_content – 展开行内容,内部嵌套子表格,子表格的复选框使用 cell_two_checkbox 插槽
父子联动逻辑通过以下四个核心函数完成: - selectAllEvent():表头全选/全不选
- changeOneSelect(row):主表某行复选框变化
- changeTwoSelect(childRow, parentRow):子表某行复选框变化
- checkOneSelectStatus():更新表头的全选/半选状态
代码
<template>
<div>
<vxe-grid v-bind="gridOptions">
<template #expand_content="{ row }">
<div class="expand-wrapper">
<vxe-grid v-bind="childGridOptions" :data="row.childList">
<template #cell_two_checkbox="{ row: childRow }">
<vxe-checkbox
v-model="childRow.isChecked"
:indeterminate="childRow.isHalf"
@change="changeTwoSelect(childRow, row)"
></vxe-checkbox>
</template>
</vxe-grid>
</div>
</template>
<template #head_one_checkbox>
<vxe-checkbox
v-model="isAllChecked"
:indeterminate="isAllHalf"
@change="selectAllEvent"
></vxe-checkbox>
</template>
<template #cell_one_checkbox="{ row }">
<vxe-checkbox
v-model="row.isChecked"
:indeterminate="row.isHalf"
@change="changeOneSelect(row)"
></vxe-checkbox>
</template>
</vxe-grid>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import XEUtils from 'xe-utils'
const isAllChecked = ref(false)
const isAllHalf = ref(false)
const gridOptions = reactive({
border: true,
height: 600,
rowConfig: {
keyField: 'id'
},
columns: [
{ type: 'seq', width: 70 },
{
field: 'checkbox',
width: 60,
slots: { default: 'cell_one_checkbox', header: 'head_one_checkbox' }
},
{ type: 'expand', width: 60, slots: { content: 'expand_content' } },
{ field: 'name', title: 'Name' },
{ field: 'sex', title: 'Sex' },
{ field: 'age', title: 'Age' }
],
data: [
{
id: 10001,
name: 'Test1',
role: 'Develop',
sex: 'Man',
age: 28,
address: 'test abc',
isChecked: false,
isHalf: false,
childList: [
{
id: 10011,
name: 'Test112',
role: 'Develop',
sex: 'Man',
age: 28,
address: 'test abc',
isChecked: false,
isHalf: false
},
{
id: 10012,
name: 'Test134 134134134134134134134134',
role: 'Test',
sex: 'Women',
age: 22,
address: 'Guangzhou',
isChecked: false,
isHalf: false
}
]
},
{
id: 10002,
name: 'Test2',
role: 'Test',
sex: 'Women',
age: 22,
address: 'Guangzhou',
isChecked: false,
isHalf: false,
childList: [
{
id: 10021,
name: 'Test233 233233233233233',
role: 'Designer',
sex: 'Man',
age: 34,
address: 'test 234324',
isChecked: false,
isHalf: false
}
]
},
{
id: 10003,
name: 'Test3',
role: 'PM',
sex: 'Man',
age: 32,
address: 'Shanghai',
isChecked: false,
isHalf: false,
childList: [
{
id: 10031,
name: 'Test366 366 366 366366366366',
role: 'Test',
sex: 'Man',
age: 76,
address: 'test rtyty',
isChecked: false,
isHalf: false
},
{
id: 10032,
name: 'Test345',
role: 'Develop',
sex: 'Women',
age: 56,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10032,
name: 'Test361 361 361361361361361361361361361361361361',
role: 'Test',
sex: 'Women',
age: 21,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10033,
name: 'Test367',
role: 'Develop',
sex: 'Women',
age: 28,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10034,
name: 'Test3213',
role: 'Test',
sex: 'Man',
age: 35,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10035,
name: 'Test3214',
role: 'Develop',
sex: 'Women',
age: 49,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10036,
name: 'Test3216',
role: 'Test',
sex: 'Man',
age: 58,
address: 'Guangzhou',
isChecked: false,
isHalf: false
}
]
},
{
id: 10004,
name: 'Test4',
role: 'Designer',
sex: 'Women',
age: 24,
address: 'Shanghai',
isChecked: false,
isHalf: false,
childList: [
{
id: 10041,
name: 'Test456456 456456456456456456',
role: 'Designer',
sex: 'Man',
age: 19,
address: 'test 3444444',
isChecked: false,
isHalf: false
},
{
id: 10042,
name: 'Test457',
role: 'Test',
sex: 'Women',
age: 29,
address: 'rtyty sdfsdf',
isChecked: false,
isHalf: false
}
]
},
{
id: 10005,
name: 'Test5',
role: 'PM',
sex: 'Man',
age: 62,
address: 'Guangzhou',
isChecked: false,
isHalf: false,
childList: [
{
id: 10031,
name: 'Test366 366 366 366366366366',
role: 'Test',
sex: 'Man',
age: 76,
address: 'test rtyty',
isChecked: false,
isHalf: false
},
{
id: 10032,
name: 'Test345',
role: 'Develop',
sex: 'Women',
age: 56,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10032,
name: 'Test361 361 361361361361361361361361361361361361',
role: 'Test',
sex: 'Women',
age: 21,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10033,
name: 'Test367',
role: 'Develop',
sex: 'Women',
age: 28,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10034,
name: 'Test3213',
role: 'Test',
sex: 'Man',
age: 35,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10035,
name: 'Test3214',
role: 'Develop',
sex: 'Women',
age: 49,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10036,
name: 'Test3216',
role: 'Test',
sex: 'Man',
age: 58,
address: 'Guangzhou',
isChecked: false,
isHalf: false
},
{
id: 10037,
name: 'Test3217',
role: 'Test',
sex: 'Man',
age: 49,
address: 'Guangzhou',
isChecked: false,
isHalf: false
}
]
}
]
})
const childGridOptions = reactive({
border: true,
height: 200,
columns: [
{ field: 'checkbox', width: 60, slots: { default: 'cell_two_checkbox' } },
{ field: 'name', title: 'Name' },
{ field: 'role', title: 'Role' },
{ field: 'address', title: 'Address' }
]
})
const selectAllEvent = () => {
const { data = [] } = gridOptions
isAllHalf.value = false
XEUtils.eachTree(
data,
(row) => {
row.isChecked = isAllChecked.value
row.isHalf = false
},
{ children: 'childList' }
)
}
const changeOneSelect = (row) => {
row.isHalf = false
XEUtils.eachTree(
row.childList,
(childRow) => {
childRow.isChecked = row.isChecked
},
{ children: 'childList' }
)
checkOneSelectStatus()
}
const checkOneSelectStatus = () => {
const { data = [] } = gridOptions
isAllChecked.value = XEUtils.every(data, (row) => row.isChecked)
isAllHalf.value = !isAllChecked.value && XEUtils.some(data, (row) => row.isChecked || row.isHalf)
}
const changeTwoSelect = (childRow, row) => {
const { childList = [] } = row
row.isChecked = XEUtils.every(childList, (row) => row.isChecked)
row.isHalf = !isAllChecked.value && XEUtils.some(childList, (row) => row.isChecked)
checkOneSelectStatus()
}
</script>
<style lang="scss" scoped>
.expand-wrapper {
padding: 16px;
}
</style>
数据字段设计
每行数据(主行和子行)都需要包含两个字段:
- isChecked:Boolean,是否选中
- isHalf:Boolean,是否半选状态(仅用于界面展示,vxe-checkbox 的 indeterminate 属性)
子行通过 childList 数组与父行关联。
数据状态
- 父行全选:所有子行 isChecked === true
- 父行半选:不是全选 且 至少有一个子行被选中
- 父行未选:所有子行 isChecked === false
© 版权声明
THE END


![表情[baoquan]-拾光赋](https://blogs.ink/wp-content/themes/zibll/img/smilies/baoquan.gif)


暂无评论内容