效果
思路
- 自定义模板
- 当点击某个行复选框时,其所有后代复选框都要同步状态,且其直系父辈状态需要根据所点击复选框的状态来修正
- 点击全选复选框时,批量同步所有行内复选框状态
代码
非封装组件,按需自行改写
确保引入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);
}