el-table树形表格实现 父子联动勾选,子部分勾选时,父处于半勾选的状态

el-table树形表格实现 父子联动勾选,子部分勾选时,父处于半勾选的状态

需求背景

el-table,支持树形表格,但是多选框并没有自动支持父子联动勾选;

  1. 勾选全选,只有最外层的行被勾选;
  2. 勾选父,子级不会自动勾选;
  3. 勾选部分子,父级不会自动处于半勾选状态;
  4. 依次勾选全部的子级,父级不会自动勾选;

具体要求:

  1. 勾选全选按钮,表格所有层级 均被勾选,取消勾选全选按钮,表格所有层级 均被取消勾选;
  2. 父级勾选,子级(以及更深层级的子级)全部勾选,父级取消勾选,子级(以及更深层级的子级)全部取消勾选;
  3. 子级部分勾选 或 处于 半勾选的状态,则父级处于半勾选的状态
  4. 依次勾选全部的子级,父级会自动勾选;

开发分析

1、半勾选状态的实现

当前只有标题头的勾选框有半勾选状态,行上没有,需要手动实现

首先是半勾选的样式,需要从标题头勾选框的半勾选状态的样式复制;当父级处于半勾选状态时,去使用这个类的样式;当行的某一数据标识为true时,勾选框的列 的类名添加indeterminate,否则勾选框的列 的类名为空;

可以使用cell-class-name属性,进行实现< el-table :row-key="valueKey">< /el-table>

js 复制代码
  tableCellClassName ({row, column}) {
    let cellClassName = ''
    if (row.indeterminate && column.type === 'selection') {
      cellClassName = 'indeterminate'
    }
    return cellClassName
  }
css 复制代码
<style lang="scss" scoped>
::v-deep .lov_table {
  .indeterminate {
    .el-checkbox__input {
      .el-checkbox__inner {
        background-color: #5f4efd;
        border-color: #5f4efd;
        &::before {
          content: '';
          position: absolute;
          display: block;
          background-color: #fff;
          height: 2px;
          -webkit-transform: scale(.5);
          transform: scale(.5);
          left: 0;
          right: 0;
          top: 5px;
        }
      }
    }
  }
}
</style>

2、编辑初始化时,要给保存前的数据自动勾选上

获取到数据后,使用modalTableRef.toggleRowSelection(row, true) 进行勾选

同时给树形结构的数据初始化setTreeMetaData,使得每一个子节点都有parentId,方便后续的联动操作

js 复制代码
async getList() {
    this.loading = true
    try {
      const res = await this.lovRemote.request({
        ...this.remoteParams,
        ...this.queryForm,
        pageIndex: this.pageIndex,
        pageSize: this.pageSize
      })
      if (res?.data) {
        const remoteResult: any = this.$attrs.remoteResult
        const result = remoteResult ? remoteResult(res) : res.data.list || res.data
        setTreeMetaData(result, this.valueKey, 'children')
        this.list = result
        this.totalRows = res.data.total
        this.$nextTick(() => {
          this.initSelection()
        })
      }
    } catch (err) {
      this.$message.error(err.message)
    } finally {
      this.loading = false
    }
  }
  
initSelection() {
    const modalTableRef: any = this.$refs.modalTableRef
    modalTableRef.clearSelection(); //* 打扫干净屋子再请客
    if (this.cachedSelection.length) {
      const checkSelect = (data) => {
        data.forEach((item) => {
          this.cachedSelection.forEach((sel) => {
            if (item[this.valueKey] === sel[this.valueKey]) {
              if (this.multiple) {
                modalTableRef.toggleRowSelection(item, true)
                resetTableTreeSelection({record: item, userSelection: this.cachedSelection, list: this.list, tableRef: modalTableRef, toggleSub: false})
              } else {
                modalTableRef.setCurrentRow(item)
              }
            }
          })
          if (getType(item.children) === 'Array' && item.children.length) {
            checkSelect(item.children)
          }
        })
      }
      // 递归选中勾选缓存数据
      checkSelect(this.list)
    }
  }

3、父子联动勾选

使用selection-change事件(当选择项发生变化时会触发该事件)监听的话,只有一个selection参数,并不知道当前勾选的是哪一行,于是使用select(当用户手动勾选数据行的 Checkbox 时触发的事件)和select-all(当用户手动勾选全选 Checkbox 时触发的事件)

js 复制代码
/** 勾选全选操作(多选模式) */
selectAllOnClick() {
  const modalTableRef: any = this.$refs.modalTableRef
  const isAllSelected = modalTableRef?.store.states.isAllSelected; //? 是否全部勾选
  if (this.needRelativeSelect) {
    this.list.forEach(record => {
      toggleSubAll({record, toggleRowSelection: modalTableRef.toggleRowSelection, selected: isAllSelected})
    })
    const finalSelection = modalTableRef?.store.states.selection;
    this.cachedSelection = finalSelection;
    return
  }
}

/** 勾选单条操作(多选模式) */
selectOnClick(selection, row) {
  const modalTableRef: any = this.$refs.modalTableRef
  if (this.needRelativeSelect) {
    resetTableTreeSelection({record: row, userSelection: selection, list: this.list, tableRef: modalTableRef})
    const finalSelection = modalTableRef?.store.states.selection;
    this.cachedSelection = finalSelection;
    return
  }
}

util.js

js 复制代码
/**
 * todo 对树形结构的数据进行加工,直接子节设置parentValue
 * @param data 树形结构的数据
 * @param treeKey 树形结构的id字段
 * @param childrenKey 树形结构子级的字段
 * @param parentNode 上级节点
 */
const setTreeMetaData = (data, treeKey: string = 'id', childrenKey: string = 'children' ,parentNode?: any) => {
  data.forEach(item => {
    if (parentNode) item.parentValue = parentNode[treeKey];
    if (Array.isArray(item[childrenKey]) && item[childrenKey].length) {
      item[childrenKey].forEach(child => {
        child.parentValue = item[treeKey];
        if (Array.isArray(child[childrenKey]) && child[childrenKey].length) {
          setTreeMetaData(child[childrenKey], treeKey, childrenKey, child)
        }
      });
    }
  })
}
/**
 * todo 获取展开后的树形表格数据 深层 变成 一层
 * @param treeData 树形结构的数据
 * @param childrenKey 树形结构子级的字段
 * @returns 展开后的树形表格数据
 */
const getExpandedTreeData = (treeData, childrenKey: string = 'children') => {
  return treeData.reduce((accumulator, curItem) => {
    if (Array.isArray(curItem[childrenKey]) && curItem[childrenKey].length) {
      return accumulator.concat(curItem).concat(getExpandedTreeData(curItem[childrenKey]))
    }
    return accumulator.concat(curItem)
  }, [])
}
/**
 * todo 将当前行下的所有子级切换勾选状态
 * @param record 单前行
 * @param toggleRowSelection 切换行的勾选状态的内置方法
 * @param childrenKey 树形结构子级的字段
 * @param selected 统一设置的 勾选的状态
 */
const toggleSubAll = ({record, toggleRowSelection, childrenKey = 'children', selected = true}) => {
  if (Array.isArray(record[childrenKey]) && record[childrenKey].length) { //* 有子级
    record[childrenKey].forEach(subRecord => {
      toggleRowSelection(subRecord, selected); //* 调用el-table内置方法,进行勾选
      if (Array.isArray(subRecord[childrenKey]) && subRecord[childrenKey].length) { //* 子级还有下级
        toggleSubAll({record: subRecord, toggleRowSelection, childrenKey, selected})
      }
    })
    
  }
}
/**
 * todo 设置树形表格父级的勾选状态
 * @param parentValue 父级的id
 * @param expandedList 树形表格展开后的数据
 * @param userSelection 用户勾选的所有数据
 * @param tableRef 表格的ref
 * @param treeKey 树形结构的id字段
 */
const setTableTreeParentSelection = ({parentValue, expandedList, userSelection, tableRef, treeKey = 'id'}) => {
  const toggleRowSelection = tableRef.toggleRowSelection;
  const subList = expandedList.filter(item => item.parentValue === parentValue);
  const parentRecord = expandedList.find(item => item[treeKey] === parentValue)
  const selectedList = subList.filter(subRecord => userSelection.some(selectedRecord => selectedRecord[treeKey] === subRecord[treeKey]))
  const halfSelectedList = subList.filter(subRecord => subRecord.indeterminate === 'Y')
  parentRecord.indeterminate = undefined;
  if (subList.length === selectedList.length) { //* 所有子级全部勾选,父级勾选
    toggleRowSelection(parentRecord, true)
  } else if (!selectedList.length && !halfSelectedList.length) { //* 所有子级 全部没有勾选也没有半勾选
    toggleRowSelection(parentRecord, false)
  } else {
    //* 子级部分勾选,
    toggleRowSelection(parentRecord, false)
    parentRecord.indeterminate = 'Y'
  }
  const currentSelection = tableRef?.store.states.selection;
  if (parentRecord.parentValue) setTableTreeParentSelection({parentValue: parentRecord.parentValue, expandedList, userSelection: currentSelection, tableRef})
}
/**
 * todo 重置树形表格的勾选状态
 * @param record 用户勾选的单前行
 * @param userSelection 用户勾选的所有数据
 * @param list 当前页面的所有数据(树形结构)
 * @param childrenKey 树形结构子级的字段
 * @param tableRef 表格的ref
 */
const resetTableTreeSelection = ({record, userSelection, list, tableRef, treeKey = 'id', childrenKey = 'children', toggleSub = true}) => {
  const toggleRowSelection = tableRef.toggleRowSelection; //? 切换行的勾选状态的内置方法
  const isSelected = userSelection.some(item => item[treeKey] === record[treeKey]); //* 当前项被勾选
  record.indeterminate = undefined;
  toggleSub && toggleSubAll({record, toggleRowSelection, childrenKey, selected: isSelected})
  const expandedTreeData = getExpandedTreeData(list, childrenKey);
  if (record.parentValue) setTableTreeParentSelection({parentValue: record.parentValue, expandedList: expandedTreeData, userSelection, tableRef, treeKey})
}

设计缺点

不支持表格勾选状态还没有缓存就再次查询的场景,比如前端手动过滤,点击【查询】按钮进行后端精确查询,翻页等,也就是说,勾选状态没有缓存

这个设计方案只是为了简单的场景,只有新增-编辑-查询,没有查询,分页等复杂的场景;

有查询分页的,请看二次设计的方案 el-table树形表格实现 父子联动勾选 并查询时进行勾选缓存

完整代码

js 复制代码
<template>
  <el-dialog
    class="lov_modal"
    :width="`${width || '60vw'}`"
    :title="title"
    :visible="visible"
    @close="closeModal"
    destroy-on-close
    append-to-body
    :close-on-click-modal="false"
  >
    <base-query-form
      :defaultQuery="remoteParams"
      :fieldProps="queryItems"
      :loading="loading"
      @query="getQuery"
    />
    <el-table
      border
      :height="500"
      ref="modalTableRef"
      :data="list"
      class="lov_table"
      v-loading="loading"
      :row-key="valueKey"
      highlight-current-row
      :cell-class-name="tableCellClassName"
      @current-change="currentChange"
      @select="selectOnClick"
      @select-all="selectAllOnClick"
      @row-dblclick="rowOnDoubleClick"
    >
      <el-table-column
        v-if="multiple"
        type="selection"
        width="55"
        reserve-selection
        align="center"
      />
      <el-table-column
        :label="colLabel(column)"
        :prop="column.descKey || column.key"
        show-overflow-tooltip
        v-for="column in tableColumns"
        :key="column.key"
        :formatter="column.format"
        v-bind="column"
      >
      </el-table-column>
    </el-table>
    <el-pagination
      v-if="!$attrs.remoteResult"
      class="table_pagination"
      :disabled="loading"
      @size-change="listSizeOnChange"
      @current-change="listCurrentOnChange"
      :current-page="pageIndex"
      :page-sizes="[10, 20, 50, 100]"
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="totalRows"
    >
    </el-pagination>
    <span slot="footer" class="dialog-footer">
      <el-button @click="closeModal">取 消</el-button>
      <el-button type="primary" @click="confirmOnClick">确 定</el-button>
    </span>
  </el-dialog>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import BaseQueryForm from '@/components/BaseQueryForm/index.vue'
import { ColumnProp } from '@/utils/interface'
import * as LOV_CONFIG from './lovModalConfig'
import { getType, isNil } from '@/utils/util'
import { resetTableTreeSelection, setTreeMetaData, toggleSubAll } from '../BaseSearchTable/utils'

@Component({
  components: {
    BaseQueryForm
  },
  name: 'lovModal'
})
export default class extends Vue {
  @Prop({ required: true }) lovCode: string
  @Prop() remoteParams: any
  @Prop() valueKey: string
  @Prop() cached: any[]
  @Prop() width: string
  @Prop({ default: '弹窗' }) title: string
  @Prop({ default: false }) multiple: boolean; //? 是否开启多选
  /** 树形数据全部展开标识 */
  @Prop({ default: false }) expandAllFlag: boolean
  @Prop({ default: false }) needRelativeSelect: boolean; //? 树形表格,是否需要联动勾选

  list = []
  // selection = []
  cachedSelection = []
  currentRecord = null
  totalRows: number = 0
  loading: boolean = false
  queryForm: any = {}
  pageIndex = 1
  pageSize = 10
  visible = false

  get lovRemote() {
    return LOV_CONFIG[this.lovCode].lovRemote
  }

  get lovModalColumns(): ColumnProp[] {
    return LOV_CONFIG[this.lovCode].columns
  }

  get colLabel() {
    return (col) => {
      return col.i18nKey ? this.$t(`table.${col.i18nKey}`) : col.name
    }
  }


  get tableColumns() {
    return this.lovModalColumns.filter((item) => item.showInTable)
  }

  get queryItems() {
    return this.lovModalColumns.filter((item) => item.showInQuery)
  }

  getQuery(params) {
    this.queryForm = params
    this.initList()
  }

  initList() {
    this.pageIndex = 1
    this.totalRows = 0
    this.list = []
    this.getList()
  }

  initSelection() {
    const modalTableRef: any = this.$refs.modalTableRef
    modalTableRef.clearSelection(); //* 打扫干净屋子再请客
    if (this.cachedSelection.length) {
      const checkSelect = (data) => {
        data.forEach((item) => {
          this.cachedSelection.forEach((sel) => {
            if (item[this.valueKey] === sel[this.valueKey]) {
              if (this.multiple) {
                modalTableRef.toggleRowSelection(item, true)
                resetTableTreeSelection({record: item, userSelection: this.cachedSelection, list: this.list, tableRef: modalTableRef, toggleSub: false})
              } else {
                modalTableRef.setCurrentRow(item)
              }
            }
          })
          if (getType(item.children) === 'Array' && item.children.length) {
            checkSelect(item.children)
          }
        })
      }
      // 递归选中勾选缓存数据
      checkSelect(this.list)
    }
  }

  async getList() {
    this.loading = true
    try {
      const res = await this.lovRemote.request({
        ...this.remoteParams,
        ...this.queryForm,
        pageIndex: this.pageIndex,
        pageSize: this.pageSize
      })
      if (res?.data) {
        const remoteResult: any = this.$attrs.remoteResult
        const result = remoteResult ? remoteResult(res) : res.data.list || res.data
        setTreeMetaData(result, this.valueKey, 'children')
        this.list = result
        this.totalRows = res.data.total
        this.$nextTick(() => {
          this.initSelection()
        })
      }
    } catch (err) {
      this.$message.error(err.message)
    } finally {
      this.loading = false
    }
  }

  listSizeOnChange(val) {
    this.pageIndex = 1
    this.pageSize = val
    this.$nextTick(() => {
      this.initList()
    })
  }

  listCurrentOnChange(val) {
    this.pageIndex = val
    this.$nextTick(() => {
      this.getList()
    })
  }

  toggleRowExpanAll(isExpan) {
    this.toggleRowExpan(this.list, isExpan)
  }

  toggleRowExpan(data, isExpan) {
    const tree: any = this.$refs.modalTableRef
    data.forEach((item) => {
      tree.toggleRowExpansion(item, isExpan)
      if (!isNil(item.children) && item.children.length) {
        this.toggleRowExpan(item.children, isExpan)
      }
    })
  }

  async showModal() {
    this.visible = true
    this.cachedSelection = JSON.parse(JSON.stringify(this.cached))
    this.queryForm = { ...this.queryForm, ...this.remoteParams }
    await this.getList()
    this.expandAllFlag && this.toggleRowExpanAll(this.expandAllFlag)
  }

  closeModal() {
    this.pageIndex = 1
    this.pageSize = 10
    this.totalRows = 0
    this.visible = false
    this.cachedSelection = []
    this.queryForm = {}
    this.currentRecord = null
  }

  /** 点击单行操作 */
  currentChange(val) {
    this.currentRecord = val
  }
  tableCellClassName ({row, column}) {
    let cellClassName = ''
    if (row.indeterminate && column.type === 'selection') {
      cellClassName = 'indeterminate'
    }
    return cellClassName
  }
  /** 勾选全选操作(多选模式) */
  selectAllOnClick() {
    const modalTableRef: any = this.$refs.modalTableRef
    const isAllSelected = modalTableRef?.store.states.isAllSelected; //? 是否全部勾选
    if (this.needRelativeSelect) {
      this.list.forEach(record => {
        toggleSubAll({record, toggleRowSelection: modalTableRef.toggleRowSelection, selected: isAllSelected})
      })
      const finalSelection = modalTableRef?.store.states.selection;
      this.cachedSelection = finalSelection;
      return
    }
  }

  /** 勾选单条操作(多选模式) */
  selectOnClick(selection, row) {
    const modalTableRef: any = this.$refs.modalTableRef
    if (this.needRelativeSelect) {
      resetTableTreeSelection({record: row, userSelection: selection, list: this.list, tableRef: modalTableRef})
      const finalSelection = modalTableRef?.store.states.selection;
      this.cachedSelection = finalSelection;
      return
    }
  }


  /** 双击行 */
  rowOnDoubleClick(row) {
    this.currentChange(row)
    if (!this.multiple) { //* 单选时,双击行时执行【确认】操作
      this.$nextTick(() => {
        this.confirmOnClick()
      })
    }
  }

  /** 点击确认按钮 */
  confirmOnClick() {
    if (this.multiple) {
      if(this.cachedSelection.length == 0){
        let message=(this.$t('documentation.pleaseSelect') as any)+this.title
        this.$message({
          type:'warning',
          message
        })
      }
      this.$emit('onOk', this.cachedSelection)
    } else {
      if(!this.currentRecord) {
        let message=(this.$t('documentation.pleaseSelect') as any)+this.title
        this.$message({
          type:'error',
          message
        })
        return
      }
      this.$emit('onOk', this.currentRecord)
    }
  }
}
</script>

<style lang="scss" scoped>
::v-deep .lov_table {
  .indeterminate {
    .el-checkbox__input {
      .el-checkbox__inner {
        background-color: #5f4efd;
        border-color: #5f4efd;
        &::before {
          content: '';
          position: absolute;
          display: block;
          background-color: #fff;
          height: 2px;
          -webkit-transform: scale(.5);
          transform: scale(.5);
          left: 0;
          right: 0;
          top: 5px;
        }
      }
    }
  }
}
</style>
相关推荐
一 乐3 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
小御姐@stella3 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
万叶学编程6 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
积水成江9 小时前
关于Generator,async 和 await的介绍
前端·javascript·vue.js
计算机学姐10 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis
老华带你飞10 小时前
公寓管理系统|SprinBoot+vue夕阳红公寓管理系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·spring boot·课程设计
qbbmnnnnnn10 小时前
【WebGis开发 - Cesium】如何确保Cesium场景加载完毕
前端·javascript·vue.js·gis·cesium·webgis·三维可视化开发
杨荧12 小时前
【JAVA开源】基于Vue和SpringBoot的水果购物网站
java·开发语言·vue.js·spring boot·spring cloud·开源
霸王蟹13 小时前
Vue3 项目中为啥不需要根标签了?
前端·javascript·vue.js·笔记·学习
老章学编程i13 小时前
Vue工程化开发
开发语言·前端·javascript·vue.js·前端框架