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
二维码
打赏