背景:在此之前已经实现过单层级的增删改查,现在需求升级成希望无限往下加 ,所以又来更新一下,之前写过这样一篇文章的地址
首先肯定是要针对组织架构树进行二次封装 具体方法可以看这个网站
首先看效果图
树结构
新增下级
编辑当前
删除
首先肯定是要针对框架进行二次封装,写成公共组件,代码里面已经写的很清楚,每一个方法和变量都是什么
template内容
<a-tree
v-if="showTree"
:tree-data="treeData"
:checkedKeys="checkedKeys"
:expandedKeys="expandedKeys"
checkable
:auto-expand-parent="true"
:defaultExpandAll="true"
:defaultExpandParent="true"
:replace-fields="replaceFields"
:load-data="onLoadData"
@expand="onExpand"
@select="onSelect"
@check="getSelectedKeys"
>
<template slot="custom" slot-scope="item">
<!-- 非新增条目 -->
<span v-if="!item.isNewItem">
<!-- 非编辑 -->
<span
v-if="!item.isEdit"
class="node-title"
:class="emptyActive && item.active ? 'active' : ''"
>{{ item.title }}
</span>
<!-- 编辑 -->
<div v-else class="add-input-div">
<input
v-model="item.title"
type="text"
class="editInput"
autofocus
:maxLength="12"
:class="item.empty ? 'red' : ''"
@focus="item.empty = false"
/>
<span
class="tree-save_icon edit-require_icon"
@click="saveEditNode(item)"
>
<a-icon type="check-circle" />
</span>
<span
class="tree-cancle_icon edit-require_icon"
@click="cancelEdit(item)"
>
<a-icon type="close-circle" />
</span>
</div>
<!-- 非编辑状态 -->
<span v-if="!item.isEdit">
<!-- 删除按钮 -->
<a-tooltip
placement="top"
v-if="item.parentId != 0 && isShowDeleteIcon"
>
<template slot="title">
<span>删除部门</span>
</template>
<span class="icon-wrap" @click="deleteNode(item)">
<icon name="delete-opt" />
</span>
</a-tooltip>
<!-- 编辑按钮 -->
<template v-if="item.parentId != 0 && isShowEditIcon">
<a-tooltip placement="top">
<template slot="title">
<span>编辑部门名称</span>
</template>
<span class="icon-wrap" @click="editNode(item, $event)">
<icon name="edit-opt" />
</span>
</a-tooltip>
</template>
<!-- 添加按钮 -->
<a-tooltip v-if="isShowAddIcon && item.depth < 5" placement="top">
<template slot="title">
<span>新增下级部门</span>
</template>
<span class="icon-wrap" @click="addNewNode(item)">
<icon name="plus" />
</span>
</a-tooltip>
</span>
</span>
<!-- 新增条目 -->
<div v-else class="add-input-div">
<input
v-model="item.name"
type="text"
class="editInput"
autofocus
:maxLength="12"
placeholder="请输入部门名称"
/>
<span
class="tree-save_icon edit-require_icon"
@click="saveAddNode(item)"
>
<a-icon type="check-circle" />
</span>
<span
class="tree-cancle_icon edit-require_icon"
@click="cancelAdd(item)"
>
<a-icon type="close-circle" />
</span>
</div>
</template>
</a-tree>
data 变量
data() {
return {
busy: false,
expandedKeys: [],
autoExpandParent: true,
replaceFields: { title: "name",key:'key' },
emptyActive: true,
backupsExpandedKeys: [],
tempTable: [],
treeCopyData: [],
props: {
label: "name",
children: "children",
},
selectedKeys:[],
checkedKeys:[],
showTree:false
};
},
接下来肯定是梳理方法了,因为不希望每一更新增加和删除都去刷新页面,想要静默更新,所以我们在和后端接口进行交互之后,将对新增、修改、删除的操作只针对本地数据进行处理,不进行重新请求接口数据,只有在页面手动刷新的时候才去重新加载数据
组件新增fun
如果当前业务不是公共的,可在组件内进行直接编写代码,如果有公共的,那就$emit 到父组件处理,由于规范子组件最好不要更改父组件的数据,所以我把数据在父组件进行处理
// 增加节点
addNewNode(node) {
this.$emit('addNewNode',node)
},
父组件新增节点fun
this.newNode = {
addNode: true,
id: Math.ceil(Math.random() * 10000),
key: Math.ceil(Math.random() * 10000),
isEdit: false,
name: "",
isNewItem: true,
title: "",
depth: node.depth + 1,
parentId: node.id,
parentName: node.title,
scopedSlots: { title: "custom" },
children: [],
};
this.$nextTick(() => {
this.handelParentData(node)
});
首先创建我们需要新增的节点,按照我们的业务处理增加上必须有的字段,然后节点数据构建好以后要对treeDate 进行处理,handelParentData 方法内便是将构建好的新增节点塞到treeData 里面
因为我们的顶级可能存在多个的情况,所以要去循环遍历treeData ,将当前选中节点增加下一级新增节点,然后对数组进行更新,节点出现在最前用unshift ,尾部出现则直接push
// 处理父级数据
handelParentData(node){
// node为当前节点
for (let i = 0; i < this.treeData.length; i++) {
if (this.treeData[i].id === node.id) {
// 处理掉多余的输入框
this.deleteAddInput();
// 在当前节点下一级点添加输入框
this.treeData[i].children.unshift(this.newNode);
break;
} else {
// 否则查询子集是否满足
this.findChildren(node, this.treeData[i].children, this.newNode);
}
}
},
// 增加子节点
findChildren(node, data) {
if (!data.length) return;
for (let j = 0; j < data.length; j++) {
if (data[j].id === node.id) {
this.deleteAddInput();
data[j].children.unshift(this.newNode);
break;
}else{
this.findChildren(node, data[j].children, this.newNode);
}
}
},
然后就会出现如图所示的效果 编辑好输入框之后提交新增的数据,可根据自己的业务逻辑自行调整,我只需要后端给我返回当前新增数据的id 即可,然后进行静默更新。
请求成功后端接口,需要更新掉我们新增节点的id ,否则新增下一级的时候找不到id会直接报错,并且将相关需要更新的信息进行更新,把标志性数据addNode 进行一个删除,让它变成正常渲染数据
// 新增树节点
saveAddNodeFun(node) {
addOrg({
name: node.name,
parentId: this.newNode.parentId,
type: 1
}).then((res) => {
if (res.errorCode === "00000") {
this.$message.success("部门新增成功");
this.newNode.id = res.data
this.newNode.key = res.data
this.newNode.isNewItem = false
this.newNode.name = node.name
this.newNode.title = node.name
delete this.newNode['addNode']
this.handelUpdateNewItem(this.treeData,this.newNode);
} else {
this.$message.error(res.message);
}
});
},
上述代码可以观察到我有一个fun handelUpdateNewItem ,这个是用来处理数据更新之后,对treeData 进行数据更新的操作,否则我们和后端交互的数据不能正常的和前端数据进行统一的话,下一次新增就会出现问题
所以我们需要针对我们编辑程功之后的数据进行更新,因为treeData是多层级的,所以必要的时候子节点和父节点都要遍历,以便于我们准确的找到当前新增的节点所在位置,和编辑当前节点的位置,并进行数据更新
// 新增成功/编辑成功数据更新节点
handelUpdateNewItem(newItem) {
if (!data.length) return;
for (let j = 0; j < data.length; j++) {
if (data[j].id == newItem.parentId) {
this.$set(data[j], j, newItem);
} else {
// 否则查询子集是否满足
this.handelUpdateChildNewItem(data[j].children, newItem)
}
}
},
组件编辑
相对于新增来说,编辑就简单许多了,直接可以子组件内进行编辑节点
// 编辑节点
editNode(node, e) {
e.stopPropagation();
this.$nextTick(() => {
for (let i = 0; i < this.treeData.length; i++) {
if (this.treeData[i].id === node.id) {
this.treeData[i].isEdit = true;
} else {
this.treeData[i].isEdit = false;
}
this.editChild(node, this.treeData[i].children);
}
});
},
// 编辑子节点
editChild(node, data) {
if (!data.length) return;
for (let j = 0; j < data.length; j++) {
if (data[j].id === node.id) {
data[j].isEdit = true;
} else {
data[j].isEdit = false;
}
this.editChild(node, data[j].children);
}
},
同样的提交成功之后需要静默更新数据,同样的去父级组件更新数据
// 保存编辑
saveEditNode(item) {
item.empty = true;
if (!item.title) return;
this.cancelEdit(item);
this.$emit("saveEditNode", item);
},
// 编辑树节点
saveEditNodeFun(item) {
editOrg({
id: item.id,
name: item.title,
parentId: item.parentId,
}).then((res) => {
if (res.errorCode === "00000") {
this.$message.success("部门编辑成功");
item.isEdit = false
this.handelUpdateNewItem(item)
} else {
this.$message.error(res.message);
}
});
},
由于在新增和编辑成功之后处理逻辑一样,所以合并成一个方法handelUpdateNewItem
删除和批量删除fun
因为这次增加了删除和批量删除,所以就在父组件操作,在子组件里面选择的id集合抛给父组件,然后进行删除,删除之后更新本地数据
// 获取选中数据
getSelectedKeys(checkedKeys, e) {
this.checkedKeys = checkedKeys
this.$emit("selectedKeysFun", checkedKeys, e);
},
// 选择数据
selectedKeysFun(idArr) {
this.selectedRowKeysList = idArr;
},
```bash
// 删除树节点
deleteNodeFun(node) {
this.tipsModal = true
this.deleteData = node
this.selectedRowKeysList = []
},
// 提交删除
submitDelete(){
let params = {ids:[]}
if(this.selectedRowKeysList.length >0 ){
params.ids.push(this.selectedRowKeysList.toString())
}else{
params.ids.push(this.deleteData.id)
}
deleteOrg(params).then((res) => {
if (res.errorCode === "00000") {
this.$message.success("部门删除成功");
// 调用组件方法删除本地数据
this.handelDeleteData(this.treeData,this.deleteData);
this.tipsModal = false
} else {
this.$message.error(res.message);
}
});
},
后面把方法都简化了一下,所以回头来把删除也简化了
// 删除本地节点
handelDeleteData(data,item) {
if (!data.length) return;
for (let j = 0; j < data.length; j++) {
if (data[j].id == item.id) {
data.splice(j, 1);
} else {
this.handelDeleteData(data[j].children, item);
}
}
},
这方法最后因为把新增抽成了一个弹窗之后,废弃了,不抽出来弹窗继续用,只是按需加载的话不能直接展开下层节点,能接受也可以,我们产品小姐姐不接受呢…
// 删除新增输入框
deleteAddInput() {
for (let i = 0; i < this.treeData.length; i++) {
if (this.treeData[i].addNode) {
this.treeData.splice(i, 1);
} else {
this.deleteChildrenAddInput(this.treeData[i].children);
}
}
},
// 删除子集新增输入框
deleteChildrenAddInput(data) {
if (!data.length) return;
for (let j = 0; j < data.length; j++) {
if (data[j].addNode) {
data.splice(j, 1);
} else {
this.deleteChildrenAddInput(data[j].children);
}
}
},
注意事项
代码写到这里其实差不多实现了,可是等我和产品沟通的时候才发现,城市套路好深啊,我想回农村…最初其实我们是造了个轮子的,虚拟加载只不过需要后端提供全部数据,可是后端告诉我,不能,要分页说数据量太大…
产品最初的目的就是希望无感知的去刷新组织架构,增删改查也好,滑动加载也罢,就是不能接口查询费时间,好了,我们把压力对准前端…
等重新梳理一篇需求之后发现,上述实现的不能满足产品所需的业务场景,所以,修修改改修修改改,继续精进。
需求
树结构支持分页加载,异步加载、远程搜索、增删改查、批量删除、批量选择等等…
调研和不断试验时候发现,andt-design-vue 的1.x 版本有很大的局限性,按需加载的时候不支持树节点控制展开,需要你去点一下,让子节点存在之后才能展开,当然不影响功能,但是很影响体验,我自己用都emmmm 觉得很无语。。。。。
也就是上述我们addNode 的时候,塞进节点的input 框,必须是要你加载过当前节点的子集才能够出现,不然你还得点一下,很难受。
所以,开发时间比较紧,我强烈建议把新增抽成一个modal弹窗来新增,然后静默更新当前tree ,编辑保持不变,依旧把当前node变成输入框,修改之后提交,提交成功更新当前tree
限制层级最多只能五级,代码里做了限制,如果不需要限制,可以放开 基于之前的 把新增抽出来,
// 增加节点
addNewNode(node) {
this.$emit('addNewNode',node)
},
addNewNode(node) {
this.addOrgModal = true;
this.parentNode = node
this.newNode = {
addNode: true,
id: Math.ceil(Math.random() * 10000),
key: Math.ceil(Math.random() * 10000),
isEdit: false,
name: "",
isNewItem: true,
title: "",
depth: node.depth + 1,
parentId: node.id,
parentName: node.title,
scopedSlots: { title: "custom" },
parentOrgPath: node.orgPath,
orgPath: node.orgPath + '-' + 0,
children: [],
};
},
<add-org-com
v-if="addOrgModal"
ref="addOrg"
@saveAddNodeFun="saveAddNodeFun"
@closeAddOrg="addOrgModal = false"
:parentNode="parentNode"
></add-org-com>
保存新增
// 保存新增节点
saveAddNode(node) {
if (!node.name) return;
this.$emit("saveAddNode", node);
},
为了不用刷新借口请求数据,让用户直白的看见新增的组织架构,由于有其他操作需要字段,所以我添加成功之后让后端把当前新增成功的整条数据都给我返回来,如果业务中不需要,可以直接返回id,只需要替换id即可
// 保存新增树节点
saveAddNodeFun(node) {
addOrg({
name: node.name,
parentId: this.newNode.parentId,
type: node.type ? node.type : 1,
}).then((res) => {
if (res.errorCode === "00000") {
console.log(this.isSearch)
this.$message.success("部门新增成功");
this.$refs.addOrg.loading = false
this.addOrgModal = false;
this.newNode.key = res.data.id;
this.newNode.isNewItem = false;
this.newNode = Object.assign(this.newNode,res.data);
delete this.newNode["addNode"];
this.newNode.title = node.name;
this.handelUpdateNewItem(this.newNode);
} else {
this.$message.error(res.message);
}
}).catch(err =>{
if (err && this.$refs.addOrg){
this.$refs.addOrg.loading = false
}
});
},
handelUpdateNewItem 和之前的一致,不在多做赘述
如此,新增成功。由于是新增的,所以我们直接unshift是可以的,但是编辑的时候不行,编辑的时候只需要把当前条的title替换就可以了,所以需要处理一下数据
treeList 是源数据,业务中我需要,所以备份了一下,treeData 是组装之后的树,所以编辑之后需要把源数据和treeData 都更新一下
upDataItem(data, item) {
if (!data.length) return;
for (let j = 0; j < data.length; j++) {
if (data[j].id == item.id) {
this.$set(data[j],'title',item.title)
this.$set(data[j],'name',item.title)
} else {
// 否则查询子集是否满足
this.upDataItem(data[j].children, item);
}
}
this.treeList.forEach(ele => {
if(ele.id == item.id){
this.$set(ele,'title',item.title)
this.$set(ele,'name',item.title)
}
})
},
刚才一瞬间我想能不能直接更新源数据,然后组装一下树呢,是可以的,这几天搞这个东西数据绕的有点懵,这样直接,就这么干,顺便回头把新增方法简化了。
编辑节点优化
一次仅能支持一个修改,如果有批量的需求,那么需要另外写扩展,我后续闲下来补充一下
// 编辑节点
editNode(node, e) {
e.stopPropagation();
this.$nextTick(() => {
this.editChild(node,this.treeData)
});
},
// 编辑子节点
editChild(node, data) {
if (!data.length) return;
for (let j = 0; j < data.length; j++) {
if (data[j].id === node.id) {
this.$set(data[j],'isEdit',true)
} else {
this.$set(data[j],'isEdit',false)
this.editChild(node, data[j].children)
}
}
},
远程搜索
框架现有的不支持远程搜索,远程搜索的话需要自己去组装一下,都要自己写。目前的业务场景是部门和人员不同级 的时候可以重名。模糊搜索就会出现返回来的数据同名但不同层级,而且后台不不处理,不知道父级的情况下我是没办法渲染成树结构的,所以就把搜索的时候做成list
同样的,这个是可以支持增删改查的 由于也是需要静默更新,不请求接口,所以在提交成功之后,我们需要更新本地数据。搜索的时候list显示的是当前部门的层级结构,让你清楚知道,但是修改只能修改当前层级
所以,修改成功之后需要拼上去
这块需要注意一下,如果修改之后的不包含关键词,则这条数据应该消失,如果包含,则留下
修改一个不包含的,提交成功之后应该是这样的 取消搜索之后依旧显示的是树结构的数据
滑动加载
<div
class="tree-list"
@scroll="scroll"
:infinite-scroll-disabled="busy"
ref="treeList"
:infinite-scroll-distance="0"
:style="{ height: height ? height+ 'px': height+'vh' }"
>
//包裹的树
<tree />
</div>
// 部门树滑动加载
scroll() {
this.$nextTick(() => {
const el = this.$refs.treeList;
const offsetHeight = el.offsetHeight;
const scrollTop = el.scrollTop;
const scrollHeight = el.scrollHeight;
this.$emit('scroll',offsetHeight,scrollTop,scrollHeight)
});
}
// 滑动加载
scroll(offsetHeight, scrollTop, scrollHeight) {
this.searchData.pageSize = 30;
if (
offsetHeight + scrollTop - scrollHeight >= 0 &&
this.searchData.page * this.searchData.pageSize < this.total
) {
this.searchData.page++;
if(this.isSearch){
this.getSearchResult()
}else{
this.getList();
}
}
}
更新 批量删除+++++++
刚刚测出来,批量删除有问题,所以优化了下代码,批量删除之后涉及到一个问题,查询之后删除的 ,那么树肯定是要更新的。还有一个问题,树批量删除之后,是需要静默删除的,所以涉及到操作数据。由于查询和非查询结构不一样,所以需要分开操作数据
查询只需要更新searchList 即可,但是树不一样,如果是树的,更新源数据之后涉及到一个选中问题,会报如下的错误
所以为了解决这个问题呢,就是删除源数据的时候把选中的数据一起删除,并且setTimeout 一下,最终方法如下
// 批量删除更新本地数据
updateListByDelete(arr) {
// 查询批量删除
if(this.isSearch){
for (let j = 0; j < arr.length; j++) {
for (let i = 0; i < this.searchList.length; i++) {
if (this.searchList[i].id == arr[j]) {
this.searchList.splice(i, 1);
}
}
}
this.deleteIds = []
}else{
for (let j = 0; j < arr.length;j = 0) {
for (let i = 0; i < this.treeList.length; i++) {
if (this.treeList[i].id == arr[j]) {
this.treeList.splice(i, 1);
this.deleteIds.splice(j, 1)
setTimeout(()=> {
this.treeData = transformData(this.treeList)
j = 0
},500)
}
}
}
}
},
|