TreeTable代码实现多级菜单表格展示

 

<!-- 使用方法-->
<TreeTable :tree-data="tableData" :checked-ids.sync="checkedIds">
            </TreeTable>
<template>
  <div class="menu-table-container">
    <div class="table-toolbar" style="margin-bottom: 16px;">
      <el-button type="text" @click="handleSelectAll" style="margin-left: 16px;">
        {{ isAllSelected ? '取消全选' : '全选' }}
      </el-button>
    </div>
    <el-table
      :data="tableData"
      :span-method="objectSpanMethod"
      border
      style="width: 100%;"
      :show-header="showHeader"
      :header-cell-style="{ 'text-align': 'center' }"
      :cell-style="{ 'text-align': 'center', 'vertical-align': 'middle' }"
      ref="table"
    >
      <!-- 动态生成菜单列(修复勾选状态同步) -->
      <el-table-column
        v-for="(level, index) in maxLevel"
        :key="index"
        :label="`${index+1}级菜单`"
      >
        <template slot-scope="scope">
          <el-checkbox
            v-if="scope.row[`level${index+1}`] !== '-'"
            class="cell-checkbox"
            :label="getCurrentNodeId(scope.row, index)"
            v-model="selectedNodeIds"
            :indeterminate="isNodeIndeterminate(getCurrentNodeId(scope.row, index))"
            :disabled="isDisabledCheckbox(scope.row, index)"
            @change="(val) => handleCheckboxChange(getCurrentNodeId(scope.row, index), val)"
          >
            {{ scope.row[`level${index+1}`] }}
          </el-checkbox>
          <span v-else>-</span>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  name: 'TreeTable',
  props: {
    treeData: {
      type: Array,
      required: true
    },
    showHeader: {
      type: Boolean,
      default: true
    },
    checkedIds: {
      type: Array,
      default: () => []
    }
  },
  watch: {
    checkedIds: {
      immediate: true,
      deep: true,
      handler(val) {
        // 外部传入什么,就以什么为准
        this.selectedNodeIds = (val || []).map(id => String(id))
        // 处理父子联动(补全父节点)
        this.$nextTick(() => {
          this.syncParentStatusFromChecked()
        })
      }
    },
    treeData: {
      immediate: true,
      deep: true,
      handler() {
        this.initTable()
        // tree 构建完后,重新应用勾选
        this.$nextTick(() => {
          this.syncParentStatusFromChecked()
        })
      }
    }
  },
  data() {
    return {
      tableData: [], // 扁平化后的表格数据
      maxLevel: 0, // 树形数据的最大层级
      spanConfig: [], // 合并配置(索引对应层级,值为合并数数组)
      selectedNodeIds: [], // 已选择的节点ID数组(v-model绑定核心)
      isSingleSelect: false, // 是否单选模式
      treeNodeMap: {}, // 节点ID映射(key: 节点id,value: 节点对象)
      allNodeIds: [] // 所有节点ID数组(用于全选)
    }
  },
  computed: {
    // 是否全选
    isAllSelected() {
      // 判断是否所有叶子节点被选中
      return this.allNodeIds.every(id => this.selectedNodeIds.includes(id))
    }
  },
  mounted() {
    this.initTable()
  },
  methods: {
    emitChange() {
      this.$emit('update:checkedIds', [...this.selectedNodeIds])
    },
    /**
     * 初始化表格:构建节点映射 → 计算最大层级 → 扁平化数据 → 计算合并配置 → 收集所有节点ID
     */
    initTable() {
      // 重置数据
      this.treeNodeMap = {}
      this.tableData = []
      this.spanConfig = []

      // 1. 构建节点映射(遍历所有根节点)
      this.treeData.forEach(rootNode => {
        this.buildTreeNodeMap(rootNode)
      })

      // 2. 计算最大层级(遍历所有根节点)
      let maxLevel = 0
      this.treeData.forEach(rootNode => {
        maxLevel = Math.max(maxLevel, this.calcMaxLevel(rootNode))
      })
      this.maxLevel = maxLevel

      // 3. 扁平化数据(遍历所有根节点)
      this.treeData.forEach(rootNode => {
        this.flattenTree(rootNode, [], 1)
      })

      // 4. 计算合并配置
      this.calcSpanConfig()

      // 5. 收集所有节点ID
      this.allNodeIds = Object.values(this.treeNodeMap)
        .filter(node => !node.children || node.children.length === 0)
        .map(node => String(node.id))

    },

    /**
     * 构建节点ID映射表(包含所有节点,便于快速查找)
     */
    buildTreeNodeMap(node) {
      node.id = String(node.id)
      if (node.parentId) node.parentId = String(node.parentId)
      this.treeNodeMap[node.id] = node
      const children = node.children || []
      children.forEach(child => this.buildTreeNodeMap(child))
    },

    /**
     * 递归计算单个节点树的最大层级
     */
    calcMaxLevel(node, currentLevel = 0) {
      const children = node.children || []
      if (children.length === 0) return currentLevel
      let max = currentLevel
      children.forEach(child => {
        max = Math.max(max, this.calcMaxLevel(child, currentLevel + 1))
      })
      return max
    },

    /**
     * 扁平化单个节点树的树形数据(为每行存储各层级节点ID)
     */
    flattenTree(node, parentLabels, currentLevel) {
      const newParentLabels = [...parentLabels, node.label]
      const children = node.children || []

      const row = {
        nodeId: node.id,
        levelNodes: [] // [一级节点ID, 二级节点ID, ..., 当前层级节点ID]
      }

      // 回溯填充各层级节点ID
      let currentNode = node
      for (let i = currentLevel; i >= 1; i--) {
        row.levelNodes[i - 1] = String(currentNode.id)
        currentNode = this.treeNodeMap[currentNode.parentId] || currentNode
      }

      // 补全层级数据(修复:数组无id属性,空层级填"-")
      for (let i = 0; i < this.maxLevel; i++) {
        if (!row.levelNodes[i]) {
          row.levelNodes[i] = row.levelNodes[i - 1] || '' // 根节点空时填空字符串
        }
        row[`level${i+1}`] = newParentLabels[i] || "-"
      }

      // 叶子节点添加到表格
      if (children.length === 0) {
        this.tableData.push(row)
      } else {
        children.forEach(child => {
          this.flattenTree(child, newParentLabels, currentLevel + 1)
        })
      }
    },

    /**
     * 计算表格合并配置
     */
    calcSpanConfig() {
      this.spanConfig = Array.from({ length: this.maxLevel }, () => [])

      for (let level = 1; level <= this.maxLevel; level++) {
        const spanArr = []
        let lastRow = null
        let spanCount = 0

        this.tableData.forEach((row, rowIndex) => {
          let canMerge = true

          if (lastRow) {
            // 关键:检查当前层级及其之前的所有层级是否一致
            for (let l = 1; l <= level; l++) {
              if (row[`level${l}`] !== lastRow[`level${l}`]) {
                canMerge = false
                break
              }
            }
          }

          if (lastRow && canMerge) {
            spanCount++
            spanArr.push(0)
            spanArr[spanArr.length - spanCount] += 1
          } else {
            spanCount = 1
            spanArr.push(1)
          }

          lastRow = row
        })

        this.spanConfig[level - 1] = spanArr
      }
    },

    /**
     * 表格合并方法
     */
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
      const levelIndex = columnIndex
      if (levelIndex < this.maxLevel) {
        const rowspan = this.spanConfig[levelIndex][rowIndex]
        return { rowspan, colspan: 1 }
      }
      return { rowspan: 1, colspan: 1 }
    },

    /**
     * 获取当前单元格对应的节点ID
     */
    getCurrentNodeId(row, levelIndex) {
      const id = row.levelNodes[levelIndex] || ''
      return String(id)
    },

    /**
     * 判断节点是否半选(同时检查所有后代子节点)
     */
    isNodeIndeterminate(nodeId) {
      const node = this.treeNodeMap[nodeId]
      if (!node || !node.children || node.children.length === 0) return false

      // 获取所有后代子节点ID
      const allDescendantIds = this.getAllDescendantIds(nodeId)
      if (allDescendantIds.length === 0) return false

      // 已选中的后代子节点数量
      const selectedDescendantCount = allDescendantIds.filter(id =>
        this.selectedNodeIds.includes(id)
      ).length

      // 半选条件:部分子节点选中
      return selectedDescendantCount > 0 && selectedDescendantCount < allDescendantIds.length
    },

    /**
     * 递归获取节点的所有后代子节点ID(包含所有层级)
     */
    getAllDescendantIds(parentId) {
      const node = this.treeNodeMap[parentId]
      if (!node || !node.children || node.children.length === 0) return []

      let descendantIds = []
      node.children.forEach(child => {
        descendantIds.push(child.id)
        descendantIds = [...descendantIds, ...this.getAllDescendantIds(child.id)]
      })
      return descendantIds
    },

    /**
     * 选择框禁用逻辑
     */
    isDisabledCheckbox(row, levelIndex) {
      const nodeId = this.getCurrentNodeId(row, levelIndex)
      return !nodeId || !this.treeNodeMap[nodeId]
    },

    /**
     * 选择框变化事件(核心修复:状态同步)
     * @param {string} nodeId - 触发事件的节点ID
     * @param {boolean} isChecked - 是否勾选
     */
    handleCheckboxChange(nodeId, isChecked) {
      if (!nodeId || !this.treeNodeMap[nodeId]) return

      const allDescendantIds = this.getAllDescendantIds(nodeId)

      if (isChecked) {
        // 勾选:加入当前节点 + 所有后代节点
        this.selectedNodeIds = Array.from(new Set([...this.selectedNodeIds, nodeId, ...allDescendantIds]))
      } else {
        // 取消:移除当前节点 + 所有后代节点
        this.selectedNodeIds = this.selectedNodeIds.filter(id => id !== nodeId && !allDescendantIds.includes(id))
      }

      // 更新父节点状态(递归向上)
      this.updateParentStatus(nodeId)

      // 强制刷新表格
      this.$nextTick(() => {
        this.$refs.table.doLayout()
      })

      this.emitChange()
    },

    /**
     * 更新父节点状态:取消或半选逻辑
     */
    updateParentStatus(childId) {
      let node = this.treeNodeMap[childId]

      while (node && node.parentId && node.parentId !== 0) {
        const parent = this.treeNodeMap[node.parentId]
        if (!parent) break

        const directChildIds = parent.children.map(c => c.id)
        const selectedCount = directChildIds.filter(id => this.selectedNodeIds.includes(id)).length

        if (selectedCount === 0) {
          // 所有子节点未选中 → 移除父节点
          this.selectedNodeIds = this.selectedNodeIds.filter(id => id !== parent.id)
        } else {
          // 部分或全部子节点选中 → 保留父节点
          if (!this.selectedNodeIds.includes(parent.id)) {
            this.selectedNodeIds.push(parent.id)
          }
        }

        node = parent
      }

      // 去重
      this.selectedNodeIds = Array.from(new Set(this.selectedNodeIds))
    },

    syncParentStatusFromChecked() {
      const all = new Set(this.selectedNodeIds)
      this.selectedNodeIds.forEach(id => {
        let node = this.treeNodeMap[id]
        while (node && node.parentId && node.parentId !== 0) {
          all.add(node.parentId)
          node = this.treeNodeMap[node.parentId]
        }
      })
      this.selectedNodeIds = [...all]
    },

    /**
     * 全选/取消全选
     */
    handleSelectAll() {
      if (this.isAllSelected) {
        // 取消全选 → 直接清空 selectedNodeIds
        this.selectedNodeIds = []
      } else {
        // 全选 → 添加所有叶子节点 + 同步父节点
        this.selectedNodeIds = [...this.allNodeIds]
        this.syncParentStatusFromChecked()
      }
      this.emitChange();
    },
  }
}
</script>

<style scoped>
.menu-table-container {
  padding: 20px;
  background: #fff;
}

.el-table th {
  background-color: #f5f7fa !important;
  font-weight: 600 !important;
}

.el-table td, .el-table th {
  border-right: 1px solid #e6e6e6;
  border-bottom: 1px solid #e6e6e6;
}

.el-table--border th.el-table__cell:last-child {
  border-right: 1px solid #e6e6e6;
}

/* 适配多列横向滚动 */
.el-table {
  overflow-x: auto;
}

/* 选择框样式优化 */
.cell-checkbox {
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  width: 100%;
  height: 100%;
  padding: 8px 0;
}

.el-checkbox__label {
  margin-left: 8px !important;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 单元格居中 */
.el-table__cell {
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
}
</style>

 

实现样式:

版权声明:
作者:lhylwl
链接:https://ye-w.cn/2025/12/09/63.html
来源:小凡笔记-我的技术记录
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
< <上一篇
下一篇>>