el-table展开行主副表正反选联动

487 阅读4分钟

开发后台管理项目时,遇到了一个表格嵌套表格并且需要支持主副表多选联动的需求,因为项目的组件库是elementUI,在网上找到几篇文章,多半都有点bug,不过提供了一些思路,最后解决问题,所以整理了这么一篇文章。

Part1实现效果

Part2代码实现

1template模板部分

因为实际开发的时候,展开嵌入的表格往往比较复杂,这里就单独把嵌入的表格抽取成一个组件

主表模板部分代码:

<template>
  <div>
    <el-table
      :data="tableData"
      ref="tableSelect"
      :row-class-name="getRowClassName"
      :header-row-class-name="getHeaderRowClassName"
      @select-all="mainSelectAll"
      @select="mainSelect"
      @expand-change="handleExpandChange"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column type="expand">
        <template slot-scope="scope">
          <expand-table
            :data="scope.row.detailList"
            :ref="'sub' + scope.$index"
            @select="(selection) => subSelect(scope.$index, selection)"
            @select-all="(selection) => subSelectAll(scope.$index, selection)"
          />
        </template>
      </el-table-column>
      <el-table-column
        prop="name"
        label="姓名"
        width="350"
        show-overflow-tooltip
      >
        <template slot-scope="props">
          {{ props.row.name }}
        </template>
      </el-table-column>
      <el-table-column
        prop="age"
        label="年龄"
        width="350"
        show-overflow-tooltip
      >
        <template slot-scope="props">
          {{ props.row.age }}
        </template>
      </el-table-column>
      <el-table-column
        prop="address"
        label="地址"
        width="350"
        show-overflow-tooltip
      >
        <template slot-scope="props">
          {{ props.row.address }}
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

嵌入子表格代码:

<template>
  <el-table
    :data="data"
    border
    stripe
    size="small"
    ref="subTable"
    @select="handleSelect"
    @select-all="handleSelectAll"
  >
    <el-table-column type="selection" width="100" align="center" />
    <el-table-column
      prop="computerName"
      label="电脑名称"
      width="350"
      show-overflow-tooltip
    >
      <template slot-scope="props">
        {{ props.row.computerName }}
      </template>
    </el-table-column>
    <el-table-column
      prop="phoneName"
      label="手机名称"
      width="350"
      show-overflow-tooltip
    >
      <template slot-scope="props">
        {{ props.row.phoneName }}
      </template>
    </el-table-column>
    <el-table-column
      prop="carName"
      label="座驾"
      width="350"
      show-overflow-tooltip
    >
      <template slot-scope="props">
        {{ props.row.carName }}
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  props: {
    dataArray
  },
  methods: {
    /**
     * 处理子表单行选中事件。
     * 向父组件派发选中事件。
     * @param {Arrayselection - 子表中选中的行。
     */
    handleSelect (selection) {
      this.$emit('select', selection)
    },

    /**
     * 处理子表全选事件。
     * 向父组件派发全选事件。
     * @param {Arrayselection - 子表中选中的行。
     */
    handleSelectAll (selection) {
      this.$emit('select-all', selection)
    },

    /**
     * 获取子表的实际el-table实例。
     * @returns {Objectel-table实例。
     */
    getTableRef () {
      return this.$refs.subTable
    }
  }
}
</script>

2data部分数据格式以及选中数据的存储

data () {
    return {
      tableData: [
        {
          id'1',
          name'王小虎',
          age'18',
          address'上海市普陀区金沙江路 1518 弄',
          detailList: [
            {
              id'11',
              computerName'MacBook Pro',
              phoneName'iPhone 15 PRO MAX',
              carName'Audi A8'
            },
            {
              id'12',
              computerName'HUAWEI MateBook X Pro',
              phoneName'HUAWEI MATE 60 PRO',
              carName'AITO M7 ULTRA'
            }
          ]
        },
        {
          id'2',
          name'王小马',
          age'25',
          address'北京市海淀区万柳书院1号',
          detailList: [
            {
              id'21',
              computerName'MacBook Pro',
              phoneName'iPhone 15 PRO MAX',
              carName'Audi A8'
            },
            {
              id'22',
              computerName'XIAOMI NOTEBOOK PRO',
              phoneName'XIAOMI 14 ULTRA',
              carName'XIAOMI SU7 MAX'
            }
          ]
        }
      ],
      selectedData: [] // 选中的数据
    }
  },

3methods具体实现方法

1、首先是格式化数据,给数据的每一条插入isChecked属性标识当前数据是否选中,一是方便最终过滤出选中的数据,二是方便展开时候的回显。

/**
     * tableData
     * @returns {Array} 初始化后的数据列表。
     */
    initialize () {
      this.tableData = this.tableData.map(item => ({
        ...item,
        isCheckedfalse,
        detailList: item.detailList.map(i => ({
          ...i,
          isCheckedfalse
        }))
      }))
    },

2、处理主表全选

主表的选中有一个注意的点,也是参考的几个文章都有的bug,就是当子表没展开时,获取子表的ref会是undefined,就设置不了子表的选中态,所以就通过方法给自动展开,然后就能通过ref操作到子表,进行选中。其实也可以先从数据层面上先改变选中的标识,再在展开时操作,不过没有尝试,只是按照自动展开实现。

/**
     * 处理主表全选事件。
     * 展开所有子表,并选中所有子表中的行。
     * 更新selectedList以反映当前选中状态。
     * @param {Arrayselected - 主表中选中的行。
     */
    mainSelectAll (selected) {
      this.tableData.forEach((item, index) => {
        this.$refs.tableSelect.toggleRowExpansion(item, true// 展开子表
        item.isChecked = selected.length === this.tableData.length // 判断是否全选
        const subTable = this.$refs[`sub${index}`]?.getTableRef()
        if (subTable) {
          subTable.clearSelection()
          // 判断是否全选
          if (selected.length === this.tableData.length) {
            subTable.toggleAllSelection()
            item.detailList.forEach(i => {
              i.isChecked = true
            })
          } else {
            item.detailList.forEach(item => {
              item.isChecked = false
            })
          }
        } else {
          item.detailList.forEach(item => {
            item.isChecked = selected.length === this.tableData.length
          })
        }
      })
      this.updateSelectedList()
    },

3、处理主表单行选中

/**
     * 处理主表单行选中事件。
     * 展开对应的子表,并选中子表中的行。
     * 更新selectedList以反映当前选中状态。
     * @param {Arrayselection - 主表中选中的行。
     * @param {Objectrow - 当前选中的行。
     */
    mainSelect (selection, row) {
      this.$refs.tableSelect.toggleRowExpansion(row, true// 展开子表
      row.isChecked = selection.includes(row)
      const subTable = this.$refs[`sub${this.tableData.indexOf(row)}`]?.getTableRef()
      if (subTable) {
        subTable.clearSelection()
        if (selection.includes(row)) {
          row.detailList.forEach(item => {
            item.isChecked = true
            subTable.toggleRowSelection(item, true)
          })
        } else {
          row.detailList.forEach(item => {
            item.isChecked = false
          })
        }
      } else {
        row.detailList.forEach(item => {
          item.isChecked = selection.includes(row)
        })
      }
      this.updateSelectedList()
    },

4、处理子表全选事件

/**
     * 处理子表全选事件。
     * 如果子表有选中行,则主表对应行被选中。
     * 更新selectedList以反映当前选中状态。
     * @param {numberindex - 子表的索引。
     * @param {Arrayselection - 子表中选中的行。
     */
    subSelectAll (index, selection) {
      const mainTable = this.$refs.tableSelect
      const mainItem = this.tableData[index]

      if (selection.length === mainItem.detailList.length) {
        mainItem.isChecked = true
      } else {
        mainItem.isChecked = false
      }
      mainItem.detailList.forEach(attachment => {
        attachment.isChecked = selection.includes(attachment)
      })
      mainTable.toggleRowSelection(mainItem, mainItem.isChecked)
      this.updateSelectedList()
    },

5、处理子表的单选

/**
     * 处理子表单行选中事件。
     * 如果子表选中行数等于数据长度,则全选,小于则半选,否则不选。
     * 更新selectedList以反映当前选中状态。
     * @param {numberindex - 子表的索引。
     * @param {Arrayselection - 子表中选中的行。
     */
    subSelect (index, selection) {
      const mainItem = this.tableData[index]
      if (selection.length > 0 && selection.length === mainItem.detailList.length) {
        mainItem.isChecked = true
      } else if (selection.length > 0 && selection.length < mainItem.detailList.length) {
        mainItem.isChecked = ''
      } else {
        mainItem.isChecked = false
        this.$refs.tableSelect.toggleRowExpansion(mainItem, false)
      }
      this.toggleRowSelection(mainItem, mainItem.isChecked)
      mainItem.detailList.forEach(attachment => {
        attachment.isChecked = selection.includes(attachment)
      })
      this.updateSelectedList()
    },

6、其他调用到的函数方法(展开子表,切换当前选中态、获取半选类名...)

/**
     * 处理主表展开或折叠事件。
     * 在主表展开行时,根据isChecked恢复子表的选中状态。
     * @param {Objectrow - 展开或折叠的行。
     * @param {ArrayexpandedRows - 当前展开的行数组。
     */
    handleExpandChange (row, expandedRows) {
      if (expandedRows.includes(row)) {
        this.$nextTick(() => {
          const subTable = this.$refs[`sub${this.tableData.indexOf(row)}`]?.getTableRef()
          if (subTable) {
            this.$nextTick(() => {
              row.detailList.forEach(item => {
                subTable.toggleRowSelection(item, item.isChecked === true)
              })
            })
          }
        })
      }
    },
    /**
     * 更新selectedList以反映当前选中状态。
     * 筛选出tableList中主表和子表都被选中的行。
     */
    updateSelectedList () {
      const selectedList = this.tableData
        .filter(item => item.isChecked === true || item.isChecked === '')
        .map(it => {
          return {
            ...it,
            detailList: it.detailList.filter(item => item.isChecked)
          }
        })
      this.selectedData = selectedList
      // 当没有选中的时候,解决不会自动去除表头半选bug,手动隐藏表头的选中状态(去除indeterminate的css类名)
      if (!this.selectedData.length) {
        const headerRow = document.querySelector('.main-table-header')
        headerRow && headerRow.classList.remove('indeterminate')
      }
    },
    // 设置当前行的选中态
    toggleRowSelection (row, flag) {
      if (row) {
        this.$nextTick(() => {
          this.$refs.tableSelect &&
            this.$refs.tableSelect.toggleRowSelection(row, flag)
        })
      }
    },
    // 表格行样式 当当前行的状态为不明确状态时,添加样式,使其复选框为不明确状态样式
    getRowClassName ({ row }) {
      if (row.isChecked === '') {
        return 'indeterminate'
      }
    },
    // 表格标题样式 当一级目录有为不明确状态时,添加样式,使其全选复选框为不明确状态样式
    getHeaderRowClassName ({ row }) {
      const isIndeterminate = this.tableData.some(item => item.isChecked === '')
      if (isIndeterminate) {
        return 'indeterminate main-table-header'
      }
      return 'main-table-header'
    }

7、半选选中样式部分代码

.indeterminate .el-checkbox__input .el-checkbox__inner {
    background-color#409eff !important;
    border-color#409eff !important;
    color#fff !important;
}

.indeterminate .el-checkbox__input.is-checked .el-checkbox__inner::after {
    transformscale(0.5);
}

.indeterminate .el-checkbox__input .el-checkbox__inner {
    background-color#f2f6fc;
    border-color#dcdfe6;
}

.indeterminate .el-checkbox__input .el-checkbox__inner::after {
    border-color#c0c4cc !important;
    background-color#c0c4cc;
}

.indeterminate .el-checkbox__input .el-checkbox__inner::after {
    content'';
    position: absolute;
    display: block;
    background-color#fff;
    height2px;
    transformscale(0.5);
    left0;
    right0;
    top5px;
    width: auto !important;
}

以上就是最终实现的方法,代码还有不少可以优化的地方,可以更精简的判断选中,以及更简单的操作表格的选中等,以及选中主表,子表自动展开这个操作是否合理,以及子表全部取消选中,主表状态的切换,目前也只是很粗暴的操作类名去改变样式,思路是大概没问题,实现还有可以优化,有精力的伙伴们可以去优化,可以在评论提醒我去学习,没精力去优化,着急解决问题,把效果做出来的伙伴们,直接复制去用应该也能解决问题。