element-ui 可复选树型表格

效果

在这里插入图片描述

思路

  • 自定义模板
  • 当点击某个行复选框时,其所有后代复选框都要同步状态,且其直系父辈状态需要根据所点击复选框的状态来修正
  • 点击全选复选框时,批量同步所有行内复选框状态

代码

非封装组件,按需自行改写
确保引入element-ui 的情况下直接新增如下代码的vue组件即可,部分数据检索和批处理存在优化空间。

<template>
  <div>
    <aside>树型表格复选框</aside>
    <el-table
        ref="table"
        :data="rows"
        row-key="id"
        border
        default-expand-all>
      <!--自定义复选框-start-->
      <el-table-column label="全选" width="160" align="center">
        <template slot="header" slot-scope="scope">
          <el-checkbox :indeterminate="isIndeterminate" v-model="isFullChecked" @change="checkAllChange"/>
        </template>
        <template slot-scope="{row}">
          <el-checkbox :indeterminate="row.isIndeterminate" :value="row.checked" @change="checkRowChange(row)"/>
        </template>
      </el-table-column>
      <!--自定义复选框-end-->
      <el-table-column label="子节点部分选取" prop="isIndeterminate" width="160" align="center">
        <template slot-scope="{row}">{{row.isIndeterminate?'true':'false'}}</template>
      </el-table-column>
      <el-table-column label="是否选中" prop="checked" width="160" align="center">
        <template slot-scope="{row}">{{row.checked?'true':'false'}}</template>
      </el-table-column>
      <el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
      <el-table-column prop="id" label="ID" align="center" width="160"></el-table-column>
      <el-table-column prop="parentId" label="PARENT_ID" align="center" width="160"></el-table-column>
    </el-table>
    <br>
    <!--如果涉及分页,新的数据会直接覆盖掉原有的,只需要额外的操作将全选复选框重置即可-->
    <el-button @click="getResource">刷新数据</el-button>
  </div>
</template>

<script>
export default {
  name: "tree-table-checkbox",
  data() {
    return {
      rows: [],
      isFullChecked: false,
      isIndeterminate: false,
    }
  },
  created() {
    this.getResource()
  },
  watch: {
    // 直接监听rows数据是否变化,重置全选框
    rows() {
      this.isFullChecked = false
      this.isIndeterminate = false
    }
  },
  methods: {
    /*
    *  同步更新子节点复选框
    *  isIndeterminate = false checked = false 表现为空    全部子节点取消勾选
    *  isIndeterminate = false checked = true  表现为选中  全部子节点勾选
    *  isIndeterminate = true   表现为半选(操作其子节点的结果),因此子节点要改为选中 不修改子节点状态  点击为全选中
    * */
    deepSetChildren(data) {
      const { isIndeterminate, checked } = data
      const recursionSetChecked = (arr, checked) => {
        if (arr && arr.length > 0) {
          arr.forEach(d => {
            d.checked = checked
            d.isIndeterminate = false
            recursionSetChecked(d.children, checked)
          })
        }
      }
      recursionSetChecked(data.children, isIndeterminate ? true : checked)
    },
    /**
     *根据checked自定义加载
     *1)判断当前checked。
     * checked ==true  当前节点 变为false  下级所有级别节点变为false
     * checked == false 当前节点 变为true 下级所有级别节点变为true
     *2)双向递归刷新所有节点checked
     */
    checkRowChange(data) {
      const vm = this;
      const { rows } = vm
      /*
       * tbody中的复选框不使用v-model,否则会出现 checked=true 需要点两次才会变成false的情况
       * 可参考 https://blog.csdn.net/weixin_41597344/article/details/103196688
       * 因此这里再事件监听中手动取反
       */
      data.checked = !data.checked

      //鼠标勾选/取消勾选 需要同步子节点状态和操作的行一致
      vm.deepSetChildren(data)
      /*
       * 递归修正
       * 再整个数据树中,操作节点的下层子分支状态由操作节点确定,而其父辈节点需要根据操作节点的情况进行判断。
       * 这里没有单独找出操作节点的分支,直接处理了整棵树
       * @returns {*}
       */
      const recursion = (node) => {
        if (node.children && node.children.length > 0) {
          // 向下递归
          node.children.forEach(d => recursion(d))
          // 获取子节点的情况(有多少勾选的,有多少半选的)
          const sumChecked = node.children.filter(d => d.checked).length
          node.isIndeterminate = sumChecked > 0 && sumChecked < node.children.length
          if (node.children.some(d => d.isIndeterminate)) {
            node.isIndeterminate = true
          }
          node.checked = sumChecked !== 0
        } else {
          // 没有子节点的由自身确定状态
          node.isIndeterminate = false
        }
        return node
      }
      rows.forEach(d => {
        recursion(d)
      })
      //因为全选按钮不在整个数据树中,需要单独判断数据树一层的情况
      //如果全部勾选则 【全选框】 勾选
      if (rows.every(d => d.checked)) {
        vm.isFullChecked = true
      }
      //如果全部勾选则 【全选框】 取消勾选
      if (rows.every(d => !d.checked)) {
        vm.isFullChecked = false
      }
      //如果是存在半选或存在未选且有已选或半选 则 【全选框】半选
      vm.isIndeterminate = rows.some(d => d.isIndeterminate) ? true : rows.some(d => !d.checked) && rows.some(d => d.checked)

    },
    /**
     * 初始化时设定isFullChecked = false,每次点击取反
     * if  isFullChecked == true
     *设置所有节点checked = true
     * if  isFullChecked == false
     * 设置所有节点checked = false
     */
    checkAllChange() {
      const vm = this
      const { rows } = vm
      const recursionSetChecked = (item, checked) => {
        item.checked = checked
        item.isIndeterminate = false
        if (item.children && item.children.length > 0) {
          item.children.forEach(d => recursionSetChecked(d, checked))
        }
      }
      this.isIndeterminate = false
      rows.forEach(d => recursionSetChecked(d, vm.isFullChecked))
    },
    //获取列表基础数据
    getResource() {
      const vm = this
      //测试用数据只有一个根节点 "0" 方便构建测试用数据树
      const data = [
        { id: '1', parentId: '0' },
        { id: '2', parentId: '0' },
        { id: '2-0', parentId: '2' },
        { id: '1-0', parentId: '1' },
        { id: '1-1', parentId: '1' },
        { id: '1-1-0', parentId: '1-1' },
        { id: '1-1-0-0', parentId: '1-1-0' },
        { id: '1-1-0-1', parentId: '1-1-0' },
        { id: '1-2', parentId: '1' },
        { id: '1-2-0', parentId: '1-2' },
        { id: '1-2-1', parentId: '1-2' },
      ]
      //1) 简单处理数据用于自定义渲染; checked:  indeterminate:
      data.forEach(d => {
        d.checked = false //是否选中
        d.isIndeterminate = false//是否是半选状态
      })
      vm.rows = vm.makeTree(data, 'id', 'parentId', '0')
    },
    /**
     * 构建树,与复选逻辑无关
     * @param data
     * @param idMark
     * @param pIdMark
     * @param rootId
     * @returns {*}
     */
    makeTree(data, idMark, pIdMark, rootId) {
      //转化为字典,id为键值,并添加根节点对象
      let nodeDict = {};
      (nodeDict[rootId] = { children: [] })[idMark] = rootId;
      data.forEach(n => {
        (nodeDict[n[idMark]] = n).children = [];
      });
      data.forEach(function (d) {
        let parentNode = nodeDict[d[pIdMark]];
        if (parentNode) {
          parentNode.children.push(d);
        }
      });
      return nodeDict[rootId].children;
    }
  }
}
</script>

<style scoped>

</style>

补充

复选逻辑完全可以抽离,之前一直没时间,所以补充下

/*
* 层级树组递归
* */
Object.defineProperty(Array.prototype, 'recursive', {
  enumerable: false,
  value: function(handler) {
    const vm = this
    const recursive = (current, parent, index, siblings, path) => {
      if (handler({ current, parent, index, siblings, path })) return true
      if (current.children && current.children.length > 0) {
        const nextPath = [...path, current.id]
        for (let i = 0, child; (child = current.children[i]) != null; i++) {
          if (recursive(child, current, i, current.children, nextPath)) {
            return true
          }
        }
      }
    }
    for (let i = 0, child; (child = vm[i]) != null; i++) {
      if (recursive(child, null, i, vm, [])) {
        return true
      }
    }
  }
})

export class CheckManager {
  rootId
  options = []
  nodeIterator = []
  nodeMap = {}
  parentMap = {}
  statusMap = {}

  constructor(rootId) {
    this.rootId = rootId != null ? rootId : Math.random()
    this.statusMap[0] = {
      checked: false,
      indeterminate: false
    }
  }

  setData(data) {
    const vm = this, { rootId } = vm
    vm.parentMap = {}
    //添加虚拟根节点
    vm.options = [{ id: rootId, parentId: null, children: data }]
    //记录节点选中状态
    let statusMapTemp = {}
    let nodeMapTemp = {}
    let nodeIteratorTemp = []
    vm.options.recursive(({ current, parent }) => {
      vm.parentMap[current.id] = parent
      nodeMapTemp[current.id] = current
      statusMapTemp[current.id] = { checked: false, indeterminate: false }
      if (current.id != rootId) {
        nodeIteratorTemp.push(current)
      }
    })
    vm.statusMap = statusMapTemp
    vm.nodeMap = nodeMapTemp
    vm.nodeIterator = nodeIteratorTemp
    return statusMapTemp
  }

  toggle(id) {
    const vm = this, { options, statusMap, parentMap } = vm
    if (id == null) {
      return
    }
    let node = vm.nodeMap[id]
    let currStatus = statusMap[node.id]

    let checked = currStatus.checked = currStatus.indeterminate ? true : !currStatus.checked
    currStatus.indeterminate = false

    //1)同步子节点状态和本节点一致
    if (node.children) node.children.recursive(({ current }) => {
      statusMap[current.id] = { checked, indeterminate: false }
    })

    //2) 向上计算父节点
    let parent = node
    while ((parent = parentMap[parent.id])) {
      vm.updateNodeStatus(parent)
    }
    return statusMap
  }

  /**
   * 如果有子节点,则本节点状态由子节点确定
   */
  setStatus(node) {
    const vm = this, { statusMap } = vm
    let status = statusMap[node.id]
    if (!status) {
      status = statusMap[node.id] = {
        checked: false,
        indeterminate: false,
      }
    }
    if (node.children && node.children.length > 0) {
      node.children.forEach(d => vm.setStatus(d))
      vm.updateNodeStatus(node)
    } else {
      // 没有子节点的由自身确定状态
      node.indeterminate = false
    }
  }

  /**
   * 依据子节点状态更新本节点
   * @param node
   */
  updateNodeStatus(node) {
    const vm = this, { options, statusMap, parentMap } = vm
    let checkedNum = 0, unCheckedNum = 0, status = statusMap[node.id], hasHalfChecked = false
    for (let i = 0, item; (item = node.children[i]) != null; i++) {
      //记录子节点不同选中状态的数量
      statusMap[item.id].checked ? checkedNum++ : unCheckedNum++

      //有半选子节点或者  既有未选中子节点也有选中子节点  则本节点半选
      if ((statusMap[item.id].indeterminate) || (checkedNum > 0 && unCheckedNum > 0)) {
        status.indeterminate = true
        status.checked = true
        hasHalfChecked = true
        break
      }
    }
    if (!hasHalfChecked) {
      //子节点全部为选中状态则本节点选中
      status.indeterminate = false
      status.checked = checkedNum > 0
    }
  }

  /**
   * 更新选中状态
   */
  updateStatus() {
    const vm = this
    for (let i = 0, node; (node = vm.options[i]) != null; i++) {
      vm.setStatus(node)
    }
    return vm.statusMap
  }

  /*
   * 节点变化后仅保留末级节点的选中状态且末级节点indeterminate = false
   */
  refresh(data) {
    const vm = this, { rootId } = vm
    vm.parentMap = {}
    vm.options = [{ id: rootId, parentId: null, children: data }]
    //重新组织父节点映射
    vm.options.recursive(({ current, parent }) => {
      vm.parentMap[current.id] = parent
      current.parentId = parent?.id
    })
    data.forEach(d => this.setStatus(d))
    return this.statusMap
  }

  setChecked(statusOps) {
    const vm = this, { statusMap } = vm;
    let prevStatus
    for (let i = 0, nextStatus; (nextStatus = statusOps[i]) != null; i++) {
      prevStatus = statusMap[nextStatus.id]
      if (nextStatus.checked != null) {
        prevStatus.checked = nextStatus.checked
      }
    }
    return this.updateStatus()
  }

  getChecked() {
    const vm = this, { nodeIterator, statusMap } = vm;
    let result = [], status
    for (let i = 0, item; (item = nodeIterator[i]) != null; i++) {
      status = statusMap[item.id]
      if (status.checked) {
        result.push(item.id)
      }
    }
    return result
  }

  getCheckedNode() {
    const vm = this, { nodeIterator, statusMap, nodeMap } = vm;
    let result = [], status
    for (let i = 0, item; (item = nodeIterator[i]) != null; i++) {
      status = statusMap[item.id]
      if (status.checked) {
        result.push(nodeMap[item.id])
      }
    }
    return result

  }

  getHalfChecked() {
    const vm = this, { nodeIterator, statusMap } = vm;
    let result = [], status
    for (let i = 0, item; (item = nodeIterator[i]) != null; i++) {
      status = statusMap[item.id]
      if (status.checked && status.indeterminate == true) {
        result.push(item.id)
      }
    }
    return result
  }

  getFullChecked() {
    const vm = this, { nodeIterator, statusMap } = vm;
    let result = [], status
    for (let i = 0, item; (item = nodeIterator[i]) != null; i++) {
      status = statusMap[item.id]
      if (status.checked && status.indeterminate == false) {
        result.push(item.id)
      }
    }
    return result
  }
}

使用

创建管理类实例 并指明根节点id

let checkManger = new CheckManager(0);

赋值通过字段 id parentId children 表示父子关系的数据

let data = [
  id:1,
  parentId:0,
  children:[
     id:1,
     parentId:1,
  ]
]
checkManger.setData(data)

获取计算后的 复选框状态集

checkStatusMap= checkManger.statusMap

渲染复选框并绑定切换事件

 <el-checkbox 
 :value="checkStatusMap[row.id].checked" 
 :indeterminate="checkStatusMap[row.id].indeterminate" 
 @change="toggle(row.id)">
</el-checkbox>

更新复选框状态集

toggle(data) {
   this.checkStatusMap = checkManger.toggle(data);
}
  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值