在业务场景中,我们经常遇到主表与子表的数据关联需求,例如一个订单包含多个商品、一个项目包含多个子任务。当用户需要同时选择主表和子表的数据进行批量操作时,复选框联动就成了关键需求。
本文展示如何利用 vxe-table 的展开行(expand) 功能,配合自定义插槽和复选框组件,实现主表与子表之间的复选框联动效果------勾选主行时自动全选其所有子行,子行全选时主行自动选中,部分选中时主行显示半选状态。
适用场景:当树形表格(tree-config)无法满足复杂的展开内容布局时,使用 type: 'expand' 列配合自定义内容更加灵活。
代码

html
<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>
通过 vxe-table 的展开行 + 插槽机制,我们可以灵活实现主表与子表的复选框联动。这种方式相比内置的 tree-config,优势在于子表可以拥有完全独立的列定义、工具栏和交互逻辑,非常适合订单-商品、项目-任务等一对多的业务场景。
核心代码围绕 isChecked 和 isHalf 两个字段展开,配合 xe-utils 的工具函数,可以高效地处理任意层级的树形数据联动。