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