目录
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)
如果看了觉得有帮助的,我是@鹏多多,欢迎 点赞 关注 评论;END
PS:在本页按F12,在console中输入document.querySelectorAll('.diggit')[0].click(),有惊喜哦
公众号
往期文章
- 使用nvm管理node.js版本以及更换npm淘宝镜像源
- 超详细!Vue-Router手把手教程
- vue中利用.env文件存储全局环境变量,以及配置vue启动和打包命令
- 微信小程序实现搜索关键词高亮
- 超详细!Vue的九种通信方式
- 超详细!Vuex手把手教程
个人主页