Vue(十九):ElementUI 扩展实现树形结构表格组件的勾父选子、半勾选、过滤出半勾选节点功能

时间:2025-04-02 15:51:59
<template> <el-row :gutter="10"> <el-col :span="12"> <div class="flex"> <el-card class="card"> <el-scrollbar> <p>初始节点数量: {{ }}</p> <pre>{{ (d => (d)) }}</pre> </el-scrollbar> </el-card> <el-card class="card"> <el-scrollbar> <p>子节点总数量: {{ computed(() => { let count = 0; for (const key in childrenNodes) { count += childrenNodes[key].length; } return count; }) }}</p> <pre>{{ computed(() => { let obj = {}; for (const key in childrenNodes) { obj[key] = [ `子节点数量: ${(childrenNodes[key] || []).length}`, ...(childrenNodes[key] || []).map(d => (d)) ]; } return obj; }) }}</pre> </el-scrollbar> </el-card> <el-card class="card"> <el-scrollbar> <p>勾选节点数量(包含半勾选和全勾选): {{ }}</p> <pre>{{ (d => (d)) }}</pre> </el-scrollbar> </el-card> </div> </el-col> <el-col :span="12"> <el-table ref="treeTableRef" height="calc(100vh - 16px)" size="large" stripe border :data="tableData" row-key="id" lazy :load="load" @selection-change="selectionChange" @select="select" @select-all="selectAll" :header-row-class-name="headerRowClassName" :row-class-name="rowClassName"> <el-table-column type="selection" align="center"/> <el-table-column prop="id"> <template #header> <el-button type="primary" @click="getHalfSelectedNodes">获取半勾选节点</el-button> </template> </el-table-column> </el-table> </el-col> </el-row> </template> <script setup> import {computed, nextTick, reactive, ref} from 'vue'; import {tree} from "./"; // 默认节点 const tableData = reactive([ {id: '节点1', hasChildren: true}, {id: '节点2', hasChildren: true}, {id: '节点3', hasChildren: true}, {id: '节点4', hasChildren: true}, ]); const treeTableRef = ref(); // 表格实例 const selections = ref([]); // 勾选节点 const childrenNodes = reactive({}); // 全部子节点 const isSelectedAll = ref(false); // 是否勾选全部 /** * 表格头选中状态 * @type {ComputedRef<string>} 样式选择器名称 */ const headerRowClassName = computed(() => { let count = tableData.length; for (const key in childrenNodes) { count += (childrenNodes[key] || []).length; } return (count === selections.value.length || selections.value.length === 0) ? '' : 'half-checked'; }) /** * 勾选子节点 * @param id * @param selected */ const selectedChildren = (id, selected) => { (childrenNodes[id] || []).forEach(row => { treeTableRef.value.toggleRowSelection(row, selected); if (id !== row.id) selectedChildren(row.id, selected); }); } /** * 加载子节点 * @param row 当前节点 * @param treeNode 节点状态 * @param resolve 渲染子集函数 */ const load = (row, treeNode, resolve) => { setTimeout(() => { childrenNodes[row.id] = (tree[row.id] || []).map(d => ({...d, parentId: row.id})); resolve(childrenNodes[row.id]); // 判断当前节点是否选中,选中则自动勾选子节点 nextTick(() => select(selections.value, row)); }, 100); } /** * 监听节点选择事件 * @param selection 选中节点集合 */ const selectionChange = (selection) => { selections.value = selection; // 勾选的节点数量为0时,设置勾选全部的状态为false nextTick(() => ((selection.length === 0) && (isSelectedAll.value = false))); } /** * 单个节点勾选 * @param selection 选中节点集合 * @param row 当前节点 */ const select = (selection, row) => { nextTick(() => { // 是否勾选当前节点下全部子节点 selectedChildren(row.id, selection.some(d => d.id === row.id)); }); } /** * 全选节点勾选 * @param selection 选中节点集合 */ const selectAll = (selection) => { isSelectedAll.value = !isSelectedAll.value; treeTableRef.value.data.forEach(row => { // 默认数据的勾选 treeTableRef.value.toggleRowSelection(row, isSelectedAll.value); // 是否勾选全部节点下全部子节点 selectedChildren(row.id, isSelectedAll.value); }); } /** * 定义表格行样式选择器 * @param row 当前节点 * @param rowIndex 当前节点索引 * @returns {string} 样式选择器名称 */ const rowClassName = ({row, rowIndex}) => { if (selections.value.length === 0) return ''; const selectedNodeFlags = []; // 存储节点的状态 /** * 筛选子节点勾选状态 * @param item 当前节点 */ const filterSelectedChildrenNodeFlags = (item) => { (childrenNodes[item.id] || []).forEach(node => { // 避免死循环 if (item.id !== node.id) { // 当前节点子节点总数量 const childrenCount = childrenNodes[item.id].length; // 当前节点的子节点勾选数量 const selectedCount = selections.value.filter(d => d.parentId === item.id).length; if (childrenCount === selectedCount) { // 当 总数量 === 勾选数量 对此节点进行勾选,并设置为勾选默认样式 treeTableRef.value.toggleRowSelection(item, true); selectedNodeFlags.push(''); } else if (selectedCount > 0 && selectedCount < childrenCount) { // 当 勾选数量 > 0 && 勾选数量 < 总数量 对此节点进行勾选,并设置为半勾选样式 treeTableRef.value.toggleRowSelection(item, true); selectedNodeFlags.push('half-checked'); } else if (selectedCount === 0) { // 当 勾选数量 === 0 对此节点不进行勾选,并设置为不勾选默认样式 // 延迟执行,避免与load函数请求过程中发生冲突 nextTick(() => treeTableRef.value.toggleRowSelection(item, false)); selectedNodeFlags.push(''); } else { // 其他情况不处理 selectedNodeFlags.push(''); } filterSelectedChildrenNodeFlags(node); } }) } filterSelectedChildrenNodeFlags(row); return selectedNodeFlags.every(flag => flag === '') ? '' : 'half-checked'; } /** * 获取半勾节点 */ const getHalfSelectedNodes = () => { const halfSelectedNodesData = {}; // 半勾选节点(去重存储) /** * 筛选半勾选节点(注意:半勾选实际为勾选状态,只是样式上为半勾选); 如果该节点的下一层子节点全都勾选了(包含全勾选和半勾选),那么此节点将不会被列为半勾选节点数据 * @param item 当前节点 */ const filterHalfSelectedNodes = (item) => { // 当前节点子节点总数量 const childrenCount = (childrenNodes[item.id] || []).length; // 当前节点的子节点勾选数量 const selectedCount = selections.value.filter(d => d.parentId === item.id).length; // 当 勾选数量 > 0 && 勾选数量 < 总数量 则此为半勾选 if (selectedCount > 0 && selectedCount < childrenCount) { halfSelectedNodesData[item.id] = item; } (childrenNodes[item.id] || []).forEach(node => { if (item.id !== node.id) { filterHalfSelectedNodes(node); } }); } treeTableRef.value.data.forEach(row => filterHalfSelectedNodes(row)); // 全部节点 let allNodes = [...treeTableRef.value.data]; Object.values(childrenNodes).forEach(nodes => allNodes = [...allNodes, ...nodes]); // 过滤全部半勾选节点数据,向上递归(子节点为半勾选,父勾选必为半勾选) let filterHalfSelectedParentNodes = (parentId) => { allNodes.forEach(node => { if (node.id === parentId) { halfSelectedNodesData[node.id] = node; filterHalfSelectedParentNodes(node.parentId); } }); } Object.values(halfSelectedNodesData).forEach(row => filterHalfSelectedParentNodes(row.parentId)); console.log(Object.keys(halfSelectedNodesData), Object.values(halfSelectedNodesData)); } </script> <style scoped> .flex { gap: 10px; display: flex; flex-direction: column; height: calc(100vh - 16px); } .flex .card { flex: 1; } ::v-global(.flex .card .el-card__body) { height: calc(100% - 40px); } ::v-global(.half-checked .el-table-column--selection .el-checkbox .el-checkbox__inner::after) { display: none; } ::v-global(.half-checked .el-table-column--selection .el-checkbox .el-checkbox__inner) { background-color: orange; border-color: orange; } ::v-global(.half-checked .el-table-column--selection .el-checkbox .el-checkbox__inner::before) { content: ""; position: absolute; display: block; background-color: var(--el-checkbox-checked-icon-color); height: 2px; transform: scale(.5); left: 0; right: 0; top: 5px; } </style>