1,前言
最近这段时间在做一个新的模块,其中有一个三层的树结构,产品经理提出了一个很古怪的需求,整的我只能自己控制树的交互,写完之后,感觉对这个组件的用法有了不一样的了解,故而写下来。
2,需求
- 如果上级节点勾选了,则底下所有节点也勾选
- 如果是一个个勾选子级节点,直至勾选满所有子级,则该父级节点不能勾选,只能算选中状态
- 已勾选的节点不能展开,如果是展开了再勾选的,要自动收缩回去
遇见问题:
问题1:后端数据不友好,无唯一key
值(有重复key
),导致Tree
组件无唯一的key
问题2:后端数据不友好,第一层第二层的字段和第三层的字段不一致(第一层字段是dept_id
,子集字段是children
,第二层子集字段是porjs
,第三层字段又是porj_id
)
问题3:不能使用check-strictly
,也就是Tree
组件自带的父子关联,只能手动控制checkbox
的选中状态
问题4:提交给后端的数据,如果一级二级节点被勾选,则不用传递其下层结构,如果不是被勾选,则需要传递其下层结构
如图:
不过还好这个树结构只有三层,办法还是有的。(如果是未知层级就难了)
3,解决思路
问题1:无唯一key
值
这个好办,接口请求到数据之后,深拷贝一份,遍历一下,给id
手动添加字符来使它们变成唯一的,最后提交的时候去掉前面添加的字符
// 将所有id根据层级加上壹,贰,叁
handlePushLabel(data) {
try {
data.forEach(item1 => {
item1.dept_id += '壹'
if (item1.children && item1.children.length > 0) {
item1.children.forEach(item2 => {
item2.dept_id += '贰'
item2.parent_id += '壹'
if (item2.children.length > 0) {
item2.children.forEach(item3 => {
item3.dept_id += '叁'
item3.parent_id += '贰'
})
}
})
}
})
return data
} catch (error) {
console.warn(error)
}
}
// 将数据的key恢复为原来的
treeList.forEach(item1 => {
item1.dept_id = item1.dept_id.replace('壹', '')
if (item1.children.length > 0) {
item1.children.forEach(item2 => {
item2.dept_id = item2.dept_id.replace('贰', '')
item2.parent_id = item2.parent_id.replace('壹', '')
if (item2.children.length > 0) {
item2.children.forEach(item3 => {
item3.dept_id = item3.dept_id.replace('叁', '')
item3.parent_id = item3.parent_id.replace('贰', '')
})
}
})
}
})
问题2:第一层第二层的字段和第三层的字段不一致
这个也好办,最好的办法是后端调整成一样的,但是如果碰见博主这样的无法沟通的后端,只能前端自己转换字段了,这里采用的是forEach
遍历,然后使用map
替换对象键名。
// 将树数据的projs字段和proj_id和proj_name改名
handleChangeKey(data) {
try {
const tree = data
tree.forEach(item => {
if (item.children) {
const arr = item.children
// 将projs字段转为children
item.children = arr.map(item1 => {
if (item1.projs.length > 0) {
const obj = item1.projs
const parent_id = item1.dept_id
// 将proj_id字段转为dept_id 将proj_name字段转为dept_name
// 并添加depth深度和父节点id
item1.projs = obj.map(item2 => {
return {
dept_id: item2.proj_id,
dept_name: item2.proj_name,
depth: 3,
parent_id
}
})
}
return {
dept_id: item1.dept_id,
dept_name: item1.dept_name,
depth: item1.depth,
parent_id: item1.parent_id,
children: item1.projs
}
})
}
})
return this.handlePushLabel(tree)
} catch (error) {
console.warn(error)
}
}
问题3:不能使用check-strictly
这个就比较繁琐了,不能使用Tree
自带的勾选父子关联(原因看需求2),只能自己手写一二三级节点的勾选逻辑。这样的话,二级和三级节点需要有个parent_id
字段,也就是其父级的id
,且有一个depth
字段,代表其深度1,2,3
。
<el-tree
@check-change="handleTreeClick"
:data="treeList"
show-checkbox
:default-expand-all="false"
:check-strictly="true"
@node-expand="handleTreeOpen"
node-key="dept_id"
ref="tree"
highlight-current
:props="defaultProps"
/>
给Tree
组件加上ref
属性,设置check-strictly
为true
,利用@check-change
监听节点勾选,利用@node-expand
监听节点展开收起,设置node-key
为每个节点的id
。
思路是:通过@check-change
的回调,拿到第一个参数data
,这个data
里包含该节点的数据,通过这个数据可以拿到depth
判断他是第几层节点,还可以拿到parent_id
找到它的上级节点。根据这个区分一二三级节点,然后通过获取到的id,使用this.$refs.tree.getNode(id)
可以获取到节点Node
。设置节点Node
的checked
为true
,则该节点会变成勾选状态。设置它的indeterminate
为true
,则会变成选中状态,设置expanded
为true
,则是展开状态。也可以通过this.$refs.tree.setChecked(id, true)
来设置选中。
问题4:提交给后端的数据
这个就是坑了,需要先把之前改变的key
变回去,还有子级的键名改回去,然后根据是勾选还是只是单纯的选中来拼接数据。在这里用到了getCheckedNodes
来获取目前被选中的节点所组成的数组,也用到了getHalfCheckedNodes
获取半选中的节点所组成的数组。
4,完整代码
export default {
// 将树数据的projs字段和proj_id和proj_name改名
handleChangeKey(data) {
try {
const tree = data
tree.forEach(item => {
if (item.children) {
const arr = item.children
// 将projs字段转为children
item.children = arr.map(item1 => {
if (item1.projs.length > 0) {
const obj = item1.projs
const parent_id = item1.dept_id
// 将proj_id字段转为dept_id 将proj_name字段转为dept_name
// 并添加depth深度和父节点id
item1.projs = obj.map(item2 => {
return {
dept_id: item2.proj_id,
dept_name: item2.proj_name,
depth: 3,
parent_id
}
})
}
return {
dept_id: item1.dept_id,
dept_name: item1.dept_name,
depth: item1.depth,
parent_id: item1.parent_id,
children: item1.projs
}
})
}
})
return this.handlePushLabel(tree)
} catch (error) {
console.warn(error)
}
},
// 将所有id根据层级加上壹,贰,叁
handlePushLabel(data) {
try {
data.forEach(item1 => {
item1.dept_id += '壹'
if (item1.children && item1.children.length > 0) {
item1.children.forEach(item2 => {
item2.dept_id += '贰'
item2.parent_id += '壹'
if (item2.children.length > 0) {
item2.children.forEach(item3 => {
item3.dept_id += '叁'
item3.parent_id += '贰'
})
}
})
}
})
return data
} catch (error) {
console.warn(error)
}
},
/**
* 树的选中状态发生变化时
* @param {Object} data 该节点的数据
* @param {Object} on 节点本身是否被选中
* @param {Object} child 节点的子树中是否有被选中的节点
*/
handleTreeClick(data, on, child) {
try {
this.form.tree = data
if (data.depth === 1) {
this.handleOneNode(on, data)
} else if (data.depth === 2) {
this.handleTwoNode(on, data)
} else if (data.depth === 3) {
this.handleThreeNode(on, data)
}
} catch (error) {
console.warn(error)
}
},
/**
* 一级节点处理
* @param {Boolean} on 是否被选中
* @param {Object} data 当前节点的数据
*/
handleOneNode(on, data) {
try {
const tree = this.$refs.tree
// 如果当前节点未被选中且为半选状态
const node = tree.getNode(data.dept_id)
if (node.indeterminate && !node.checked) return
// 如果当前节点被选中则不能展开
if (node.checked && node.expanded) node.expanded = false
// 勾选所有下级
let arr = []
if (data.children.length > 0) {
data.children.forEach(item => {
// 筛选出所有的下级key
arr.push(item.dept_id)
if (item.children.length > 0) {
item.children.forEach(child => {
// 筛选出所有的下下级key
arr.push(child.dept_id)
})
}
})
}
// 选中or取消
if (on) {
arr.forEach(dept => {
tree.setChecked(dept, true)
})
} else {
arr.forEach(dept => {
tree.setChecked(dept, false)
})
}
} catch (error) {
console.warn(error)
}
},
/**
* 二级节点处理
* @param {Boolean} on 是否被选中
* @param {Object} data 当前节点的数据
*/
handleTwoNode(on, data) {
try {
const tree = this.$refs.tree
const node = tree.getNode(data.dept_id)
// 如果当前是半选
if (node.indeterminate && !node.checked) return
// 如果当前节点被选中则不能展开
if (node.checked && node.expanded) node.expanded = false
// 上级节点
const parentNode = tree.getNode(data.parent_id)
// 勾选所有下级
let arr = []
if (data.children.length > 0) {
data.children.forEach(item => {
// 筛选出所有的下级key
arr.push(item.dept_id)
})
}
// 选中or取消
if (on) {
arr.forEach(dept => {
tree.setChecked(dept, true)
})
// 如果上级节点不是被勾选则让上级节点半勾选
if (!parentNode.checked) {
parentNode.indeterminate = true
}
} else {
// 先取消所有下级勾选
arr.forEach(dept => {
tree.setChecked(dept, false)
})
// 如果上级节点被勾选则让上级节点半勾选
if (parentNode.checked) {
parentNode.indeterminate = true
// 如果上级是半选,则循环判断下级是否还存在勾选的,来决定上级是否需要去掉半选
} else if (parentNode.indeterminate) {
const parentData = parentNode.data || []
let bool = true
const children = parentData.children
const childArr = []
// 筛选出所有兄弟节点的key
if (children && children.length > 0) {
children.forEach(childItem => {
childArr.push(childItem.dept_id)
})
}
// 循环判断
if (childArr.length > 0) {
for (let i of childArr) {
let thisNode = tree.getNode(i)
// 如果有一个是勾选或者半选
if (thisNode.checked || thisNode.indeterminate) {
bool = false
}
}
}
if (bool) {
parentNode.indeterminate = false
}
}
}
} catch (error) {
console.warn(error)
}
},
/**
* 三级节点处理
* @param {Boolean} on 是否被选中
* @param {Object} data 当前节点的数据
*/
handleThreeNode(on, data) {
try {
// 1,如果勾选了,上级节点没选,则把上级节点和上上级改为半选
// 2,如果取消了,上级节点如果是勾选,则把上级节点和上上级改为半选
const tree = this.$refs.tree
// 上级节点
console.log(data)
const parentNode = tree.getNode(data.parent_id)
const forefathersKey = parentNode.data.parent_id
// 祖先节点
console.log(parentNode)
console.log(forefathersKey)
const forefathersNode = tree.getNode(forefathersKey)
console.log(forefathersNode)
// 如果当前节点被勾选
if (on) {
// 如果上级节点未被勾选,则让他半选
if (!parentNode.checked) {
parentNode.indeterminate = true
}
// 如果祖先节点未被勾选,则让他半选
if (!forefathersNode.checked) {
forefathersNode.indeterminate = true
}
// 如果当前节点是被取消勾选
} else {
const parentArr = []
const forefathersArr = []
const parentData = parentNode.data
const forefathersData = forefathersNode.data
let parentBool = true
let forefathersBool = true
// 筛选出所有兄弟key,如果有勾选的则代表上级不需要去除勾选
if (parentData.children.length > 0) {
parentData.children.forEach(parent => {
parentArr.push(parent.dept_id)
})
for (let i of parentArr) {
let thisNode = tree.getNode(i)
if (thisNode.checked) {
parentBool = false
}
}
}
// 为tree则代表没有三级节点被勾选,此时上级去除勾选
if (parentBool) {
parentNode.checked = false
parentNode.indeterminate = false
} else {
parentNode.indeterminate = true
}
// 筛选出所有上级的兄弟key,如果有勾选的则代表上级不需要去除勾选
if (forefathersData.children.length > 0) {
forefathersData.children.forEach(parent => {
forefathersArr.push(parent.dept_id)
})
for (let i of forefathersArr) {
let thisNode = tree.getNode(i)
if (thisNode.checked || thisNode.indeterminate) {
forefathersBool = false
}
}
}
if (forefathersBool) {
forefathersNode.indeterminate = false
}
}
} catch (error) {
console.warn(error)
}
},
/**
* 树被展开时
* @param {Object} data 该节点的数据
* @param {Object} node 节点对应的Node
* @param {Object} ref 节点组件
*/
handleTreeOpen(data, node) {
// 如果节点被选中则不让展开
if (node.checked) {
Tip.warn('当前层级已被全选,无法展开!')
node.expanded = false
}
},
// 拼接出需要的树数据
handleJoinTree() {
try {
const tree = this.$refs.tree
const treeList = _.cloneDeep(this.treeList)
// 被选中的节点
const onItem = tree.getCheckedNodes()
// 半选中的节点
const halfItem = tree.getHalfCheckedNodes()
const oneArr = []
const twoArr = []
const threeArr = []
const oneArr_ = []
const twoArr_ = []
const threeArr_ = []
// 节点分层
if (onItem.length > 0) {
onItem.forEach(item => {
switch (item.depth) {
case 1:
oneArr.push(item.dept_id)
break
case 2:
twoArr.push(item.dept_id)
break
case 3:
threeArr.push(item.dept_id)
break
}
})
}
if (halfItem.length > 0) {
halfItem.forEach(item => {
switch (item.depth) {
case 1:
oneArr_.push(item.dept_id)
break
case 2:
twoArr_.push(item.dept_id)
break
case 3:
threeArr_.push(item.dept_id)
break
}
})
}
const oneList = this.handlejoinOne(treeList, oneArr, oneArr_)
const twoList = this.handlejoinTwo(treeList, twoArr, twoArr_)
const threeList = this.handlejoinThree(treeList, threeArr, threeArr_)
// 将第二层拼进第一层
oneList.forEach(item => {
twoList.forEach(item2 => {
if (item2.parent_id === item.dept_id) {
if (!item.isOn) {
item.children.push(item2)
}
}
})
})
// 将第三层拼进第二层
oneList.forEach(child1 => {
if (child1.children.length > 0) {
child1.children.forEach(child2 => {
threeList.forEach(child3 => {
if (child3.parent_id === child2.dept_id) {
if (!child2.isOn) {
child2.children.push(child3)
}
}
})
})
}
})
return oneList
} catch (error) {
console.warn(error)
}
},
// 返回第一层
handlejoinOne(treeList, oneArr, oneArr_) {
try {
// 找出第一层节点
const oneList = []
treeList.forEach(item => {
for (let i of oneArr) {
if (item.dept_id === i) {
oneList.push({
dept_id: item.dept_id,
children: [],
isOn: true,
name: item.dept_name
})
}
}
for (let i of oneArr_) {
if (item.dept_id === i) {
oneList.push({
dept_id: item.dept_id,
children: [],
isOn: false,
name: item.dept_name
})
}
}
})
return oneList
} catch (error) {
console.warn(error)
}
},
// 返回第二层
handlejoinTwo(treeList, twoArr, twoArr_) {
try {
const twoList = []
treeList.forEach(item => {
if (item.children.length > 0) {
item.children.forEach(item2 => {
for (let i of twoArr) {
if (item2.dept_id === i) {
twoList.push({
dept_id: item2.dept_id,
children: [],
isOn: true,
parent_id: item2.parent_id,
name: item2.dept_name
})
}
}
for (let i of twoArr_) {
if (item2.dept_id === i) {
twoList.push({
dept_id: item2.dept_id,
children: [],
isOn: false,
parent_id: item2.parent_id,
name: item2.dept_name
})
}
}
})
}
})
return twoList
} catch (error) {
console.warn(error)
}
},
// 返回第三层
handlejoinThree(treeList, threeArr, threeArr_) {
try {
const threeList = []
treeList.forEach(item => {
if (item.children.length > 0) {
item.children.forEach(item2 => {
if (item2.children.length > 0) {
item2.children.forEach(item3 => {
for (let i of threeArr) {
if (item3.dept_id === i) {
threeList.push({
dept_id: item3.dept_id,
isOn: true,
parent_id: item3.parent_id,
name: item3.dept_name
})
}
}
for (let i of threeArr_) {
if (item3.dept_id === i) {
threeList.push({
dept_id: item3.dept_id,
isOn: false,
parent_id: item3.parent_id,
name: item3.dept_name
})
}
}
})
}
})
}
})
return threeList
} catch (error) {
console.warn(error)
}
},
// 将数据的key恢复为原来的
handleRestoreKey() {
try {
const treeList = this.handleJoinTree()
// 去掉id后面的壹 贰 叁
treeList.forEach(item1 => {
item1.dept_id = item1.dept_id.replace('壹', '')
if (item1.children.length > 0) {
item1.children.forEach(item2 => {
item2.dept_id = item2.dept_id.replace('贰', '')
item2.parent_id = item2.parent_id.replace('壹', '')
if (item2.children.length > 0) {
item2.children.forEach(item3 => {
item3.dept_id = item3.dept_id.replace('叁', '')
item3.parent_id = item3.parent_id.replace('贰', '')
})
}
})
}
})
// 将dept_id字段转为proj_id将dept_name字段转为proj_name,将children转为projs
treeList.forEach(child1 => {
if (child1.children.length > 0) {
const childObj = child1.children.map(item => {
let returnObj = {}
if (item.children.length > 0) {
const obj = item.children
obj.children = obj.map(child2 => {
return {
proj_id: child2.dept_id,
proj_name: child2.name
}
})
returnObj = {
dept_id: item.dept_id,
dept_name: item.name,
projs: obj.children
}
} else {
returnObj = {
projs: [],
dept_id: item.dept_id,
isOn: true,
name: item.name
}
}
return returnObj
})
child1.children = childObj
}
})
console.log(treeList)
return treeList
} catch (error) {
console.warn(error)
}
},
// 详情设置树勾选
handleSetTree(list) {
try {
console.log(list)
const one = []
const two = []
const three = []
if (list.length > 0) {
// 第一层
list.forEach(item => {
let child = item.children || ''
let obj = { id: item.dept_id + '壹', isOn: true }
if (child && child.length > 0) {
obj.isOn = false
}
one.push(obj)
})
// 第二层
list.forEach(item1 => {
let child1 = item1.children || ''
if (child1 && child1.length > 0) {
child1.forEach(item2 => {
let child2 = item2.projs || ''
let obj = { id: item2.dept_id + '贰', isOn: true }
if (child2 && child2.length > 0) {
obj.isOn = false
}
two.push(obj)
})
}
})
// 第二层
list.forEach(item1 => {
let child1 = item1.children || ''
if (child1 && child1.length > 0) {
child1.forEach(item2 => {
let child2 = item2.projs || ''
if (child2 && child2.length > 0) {
child2.forEach(item3 => {
let obj = { id: item3.proj_id + '叁', isOn: true }
three.push(obj)
})
}
})
}
})
const tree = this.$refs.tree
// 勾选第一层
if (one && one.length > 0) {
one.forEach(item => {
let node = tree.getNode(item.id)
if (item.isOn) {
node.checked = true
this.handleOneNode(true, node.data)
} else {
node.indeterminate = true
}
})
}
// 勾选第二层
if (two && two.length > 0) {
two.forEach(item => {
let node = tree.getNode(item.id)
if (item.isOn) {
node.checked = true
this.handleTwoNode(true, node.data)
} else {
node.indeterminate = true
}
})
}
// 勾选第三层
if (three && three.length > 0) {
three.forEach(item => {
let node = tree.getNode(item.id)
node.checked = true
})
}
}
} catch (error) {
console.warn(error)
}
}
}
获取转换后的结构:
this.treeList = this.handleChangeKey(data)
提交转换后的结构:
const treeList = this.handleRestoreKey()
5,总结
如果你有用到Tree组件,且产品出的需求不咋地,可以看看Tree常用这些方法技巧;
获取指定ID的节点
:this.$refs.tree.getNode(id)返回目前半选中的节点所组成的数组
:this.$refs.tree.getHalfCheckedNodes()返回目前被选中的节点所组成的数组
:this.$refs.tree.getCheckedNodes()通过 key / data 设置某个节点的勾选状态
:this.$refs.tree.setChecked(id, true)
本次分享就到这儿啦,我是@鹏多多,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~
PS:在本页按F12,在console中输入document.querySelectorAll('._2VdqdF')[0].click(),有惊喜哦
往期文章
- 超详细!Vuex手把手教程
- 使用nvm管理node.js版本以及更换npm淘宝镜像源
- 超详细!Vue-Router手把手教程
- vue中利用.env文件存储全局环境变量,以及配置vue启动和打包命令
- 微信小程序实现搜索关键词高亮
个人主页