vue 表格 vxe-table 展开行实现复选框联动

在业务场景中,我们经常遇到主表与子表的数据关联需求,例如一个订单包含多个商品、一个项目包含多个子任务。当用户需要同时选择主表和子表的数据进行批量操作时,复选框联动就成了关键需求。

本文展示如何利用 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 的工具函数,可以高效地处理任意层级的树形数据联动。

https://vxetable.cn