自定义实现 vxe-table 展开子表格的树结构复选框

在实际业务开发中,我们经常遇到主表格嵌套子表格,且需要在主表和子表上都提供复选框功能,并实现三态联动(全选、半选、未选)。本文将基于 Vue 3 + vxe-table 组件库,手把手教你实现一个支持父子表复选框联动的完整示例。

主表每行都有一个复选框,点击可同时控制其下所有子行;子表内部复选框单独控制,并能反向影响父行状态;表头提供全选/全不选按钮,支持全局批量操作

效果

  1. 主表行复选框 → 勾选时,该行下所有子行自动勾选;取消时,子行全部取消。
  2. 子表行复选框 → 勾选/取消时,自动更新所属父行的选中状态(全选/半选/未选)。
  3. 表头全选 → 一键选中/取消所有主行及其所有子行。
  4. 半选状态 → 当某个主行下只有部分子行被选中时,该行复选框显示半选(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

https://vxetable.cn

原文链接:自定义实现 vxe-table 展开子表格的树结构复选框

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容