本次分享主要是针对A项目中使用到的异步树解决方案,并结合B项目中的收件人选择(异步树组件)进行比较,给出针对不同功能不同场景下使用异步树组件的解决方案。
在A项目中,使用到异步树组件解决的功能有:选择卡点、选择设备、选择收件人。三种功能的解决方案各不相同,下面进行一一阐述。
首先应该知道的是,异步树结构属于层级分明的树结构,常用到的组件库有ElementUI、Ant-Design-Vue等,这些组件库对应的异步树的API大体功能一致,只是用法稍有不同这里不对用法进行阐述,只阐述组件的功能逻辑,A我用的UI库是Ant-Design-Vue@2.x。
选择卡点:
-
视觉稿
-
功能实现
此异步树是针对建德市下属各个区县下的卡点设备进行选择,各区县总卡点设备总数并不是很多,大概不超过50个。需求:仅支持选择卡点,右侧支持卡点的删除和清空操作。
选择过程步骤如下:
- 点击选择卡点的按钮,触发事件展示弹窗,通过接口,获取第一层级的组织节点数据(一共两个层级,第一级是组织节点,第二级是卡点设备节点)
- 点击父节点展开对应区县下的卡点设备,然后通过勾选的方式进行卡点设备的选择,右侧支持删除操作
- 勾选后的设备进行右侧已选卡点列表展示,点击右侧的单个删除按钮,删除后,重新设置树结构的已勾选的节点key集合;点击清空按钮,则设置树的已选择节点key为空数组
回显过程步骤如下:
-
此时假设我们已选择了卡点,点击选择卡点按钮,展示弹窗
-
调用接口获取第一层级的组织节点,同时根据已选择的卡点设备的parentIndexCode,查询对应的父节点,并设置树的展开节点的key集合(一般是indexCode作为树的key),同时设置树组件已选中的节点key集合
<a-tree :expanded-keys="expandedKeys" :tree-data="treeData" @expand="onExpand" :load-data="onLoadData" v-model:checkedKeys="checkedKeys" checkable :selectable="false" ></a-tree> <script setup> const getTownList = () => { // 初始化树节点接口 http.get('url') .then(({ code, data, msg }) => { if (code === '0') { treeData.value = [...data].map(item => ({ title: item.regionName, key: item.id, isLeaf: false, disabled: true })); if (selectedPointPosition.value.length) { const res = [] const keysList = [] selectedPointPosition.value.map(ele => { keysList.push(ele.id) if (selectData.value.filter(item => item.regionName === ele.regionName).length) { res.push(selectData.value.find(item => item.regionName === ele.regionName).id) } }) checkedKeys.value = keysList; expandedKeys.value = Array.from(new Set(res)) } } else { message.error(msg); } }) } </script>
-
右侧列表则直接展示已选择的卡点设备集合即可,后续删除操作同选择过程一致
注意:1.选择卡点这里我设置了父节点不可勾选的操作,因为此功能只用于选择卡点,且不支持搜索功能;2.一般异步树选择卡点设备会结合地图选择同时存在,从地图选择切换树选择的逻辑和回显过程类似,不过多赘述。
选择设备:
- 视觉稿
-
功能实现
此异步树是针对建德市下各个组织下的人脸和车辆的抓拍设备选择,支持所有树节点的勾选。需求:1.可以选择单个人脸或者车辆的抓拍设备;2.当一个组织下面的抓拍设备都被选中后,右侧列表显示该组织的名称(替代该组织下的所有设备);3.左侧树结构支持根据抓拍设备名称进行搜索,右侧支持单一设备删除和清空功能。
非搜索过程步骤如下:
-
点击选择设备按钮,触发事件展示弹窗,调用接口,查询第一级组织节点(多级)
-
勾选设备节点,右侧回显已勾选的设备节点(通过树组件的选择节点方法获取到已勾选的节点),如果某一父节点下的抓拍设备节点(组织节点)均被选中,右侧展示对应的父节点
<a-tree v-if="treeData.length" v-model:expanded-keys="expandedKeys" :tree-data="treeData" @expand="onExpand" @check="selectNode" :load-data="onLoadData" v-model:checkedKeys="checkedKeys" checkable :selectable="false" > </a-tree> <script setup> const selectNode = (value, { checkedNodes, halfCheckedKeys }, extra) => { // 用来过滤已勾选的数据,只展示最高层级中已勾选的节点 const res = []; if (checkedNodes.length) { checkedNodes.forEach(ele => { const repeatNode = checkedNodes.filter(item => item.props.key === ele.props.parentIndexCode); if (repeatNode.length === 0) { res.push(ele); } }); } } </script>
搜索过程步骤如下:
-
输入框中输入关键字,点击搜索按钮,根据接口响应展示对应的树结构(展开的节点为所有父节点的key集合)
-
勾选目标抓拍设备节点,右侧进行顶部添加;此时要注意:如果在搜索状态下,取消已勾选的抓拍设备,要在树组件的选择节点方法中对当前点击的节点状态进行判断,然后进行右侧的抓拍设备新增/删除
// 选择树节点 const selectNode = (value, { checkedNodes, halfCheckedKeys }, extra) => { if (isInitStatus.value) { // isInitStatus.value为true,指初始状态(用户没有输入关键字进行查询) selectedPointPosition.value = res.map(ele => ({ ...ele.props, title: ele.props.name.includes('<') ? regFilterFont(ele.props.name) : ele.props.name, type: ele.props.isTreeNode ? 1 : 0, })); } else { // isInitStatus.value为false,指筛选状态(用户输入关键字进行查询操作) const _res = []; res.map(ele => { const length = selectedPointPosition.value.filter(item => item.indexCode === ele.props.indexCode).length; if (length === 0) _res.push({ ...ele.props, type: ele.props.isTreeNode ? 1 : 0, name: ele.props.name.includes('<') ? regFilterFont(ele.props.name) : ele.props.name, }); }); } }
-
当清空输入框中的内容点击搜索按钮时,要先设置树组件的节点数据为空数组,然后调用接口获取第一层级树节点,同时需要在树组件中添加v-if=“treeData.length”
<a-tree v-if="treeData.length" v-model:expanded-keys="expandedKeys" :tree-data="treeData" @expand="onExpand" @check="selectNode" :load-data="onLoadData" v-model:checkedKeys="checkedKeys" checkable :selectable="false" > </a-tree> <script setup> const onSearch = (val) => { if (val) { } else { // 清空关键字 expandedKeys.value = []; checkedKeys.value = []; treeData.value = []; // 初始化树(调用接口) } } </script>
回显过程步骤如下:
- 假如我们已经选择了设备节点或者组织节点,点击选择设备按钮,展示弹窗
- 调用接口获取第一层级树节点,此时我们有两种方案:1.直接根据已选设备/组织的indexCode(主键)进行设置树的已选择的节点(控制台会报错,不存在…节点,因为是异步树,可能还没有加载到当前已选择的抓拍设备/组织);2.在异步加载函数中判断,如果父节点展开后的子节点中含有已选择的抓拍设备,进行树已选择节点的设置(优势:控制台无报错;劣势:逻辑更复杂)。此处我采用的是第一种方案。
-
选择收件人:
- 交互稿
-
功能实现
此异步树是针对建德市下各个组织下的民警人员选择,支持所有树结点的勾选。需求:1.可以选择单个民警人员;2.可以通过勾选组织节点,从而选择该组织下属所有民警人员;3.支持根据民警人员名称进行搜索选择。
非搜索过程步骤如下:
-
点击收件人选择按钮,展示弹窗同时初始化第一层级的组织节点
-
点击父节点展开对应子节点,以此类推可以选择单个民警人员数据/组织节点,此处实现获取民警人员数据的方式:在树组件的check函数中获取到用户勾选的节点(组织节点和单个民警人员数据),这里要拿到最高级的组织节点和民警人员数据,然后通过调用接口的方式获取到最高级组织节点下的民警人员数据与同级民警人员(如果最高级组织节点和民警人员数据不是同一级,则民警人员数据会被包含在组织节点下)数据进行合并,然后再与已选择的民警人员数据进行合并展示,以此类推
<a-tree v-if="treeData.length" v-model:expanded-keys="expandedKeys" :tree-data="treeData" @expand="onExpand" @check="selectNode" :load-data="onLoadData" v-model:checkedKeys="checkedKeys" checkable :selectable="false" > </a-tree> <script setup> const selectNode = (value, { node, checkedNodes }, extra) => { // 非搜索状态:获取最高级节点的节点数据,并设置选中的key集合, // 调用接口(只传组织节点key)获取到筛选后的民警数据(同时与勾选的人员数据进行合并)用于显示 // 注意:如果有手动添加的数据,在勾选和取消勾选操作调用接口后要记得保留手动添加的数据 // 最高级节点数据 const highestNode = []; if (checkedNodes.length) { checkedNodes.forEach(ele => { const repeatNode = checkedNodes.filter(item => item.props.key === ele.props.parentIndexCode); if (repeatNode.length === 0) { highestNode.push({ ...ele.props }); } }); } // 过滤最高级节点数据(组织和人员) const personNode = highestNode .filter(ele => !ele.isDept) .map(ele => ({ ...ele, isEdit: false, isAddBySelf: false, })); const deptNode = highestNode.filter(ele => ele.isDept); throttle(function() { // 调用接口获取民警信息 getPeopleInfo(deptNode, personNode); }, 500); } </script>
-
进行右侧已选人员数据进行删除,每次删除都要设置树节点的已勾选数据,目的是保持右侧数据与树节点的联动显示效果
// 手动删除操作 const deleteBySelf = (val) => { selectedPersonList.value = selectedPersonList.value.filter(ele => ele.name !== val.name); checkedKeys.value = selectedPersonList.value.filter(ele => ele.indexCode).map(item => item.indexCode); }
搜索过程步骤如下:
-
用户输入关键字进行民警人员搜索,叶子节点一定是人员,不存在叶子节点是组织的情况
-
用户进行勾选,选择民警,此处只进行民警人员数据与右侧已选择人员的数据进行合并,对于勾选的组织节点不进行调用接口操作(因为搜索针对的是民警人员搜索)
-
用户取消已勾选的民警,此时要判断当前点击的节点勾选状态,然后判断进行右侧的人员数据删除/增加操作
const selectNode = (value, { node, checkedNodes }, extra) => { if (!isInitStatus.value) { // 搜索状态:精确到人员,只需要记录勾选的人员数据 // node.checked: 为false,表示选中;true表示取消选中 if (node.checked) { selectedPersonList.value = selectedPersonListCopy.value .filter(ele => ele.indexCode !== node.dataRef.indexCode); } else { selectedPersonList.value.unshift({ ...node.dataRef, isEdit: false, isAddBySelf: false, bySearch: 1, }); } } }
-
此方案存在问题:对收件人进行编辑操作,假设默认已选五个民警,此时如果勾选最高级的组织节点,那么右侧展示所有的民警;但是如果取消勾选后,就会存在问题,因为右侧已选民警是通过接口获取,此时树节点没有没有已勾选的节点,那么此时右侧的展示数据就会存在歧义:展示默认已选的五个民警还是不展示?同时前端逻辑过于复杂。推荐参考B项目中的收件人选择交互,如下:
功能实现:(基于elementUI实现)
<!--
@Description: 人员选择(三级)
@Author duanfanchao
@date 2022/1/1 00:00:00
@LastEditTime 2022-01-01 00:00:00
@LastEditors duanfanchao
-->
<template>
<div class="task-dispatch">
<div class="task-dispatch_div__content">
<div class="content_div__tree">
<div class="tree_div__header">人员所属组织</div>
<el-input v-model="treeKeyword" placeholder="搜索"></el-input>
<div class="tree_div__content">
<el-tree
:data="treeData"
:props="props"
ref="tree"
highlight-current
node-key="id"
@node-click="nodeClick"
:expand-on-click-node="true"
:filter-node-method="filterNode"
>
<div slot-scope="{ data }" style="display: flex;align-items: center;">
<img src="./image/department.png" style="width: 24px;height: 24px;margin-right: 4px;" alt="">
<span>{{ data.name }}</span>
</div>
</el-tree>
</div>
</div>
<!-- 待选择人员 -->
<div class="content_div__select">
<div class="select_div__header">
<span>待选择人员 ( {{selectCheckedPeopleTotal}}/{{selectPeopleTotal}} )</span>
<el-checkbox v-model="checked">包含下级人员</el-checkbox>
</div>
<el-input v-model="selectKeyword" placeholder="搜索">
<i slot="suffix" class="el-input__icon el-icon-search" @click="searchSelectPeople"></i>
</el-input>
<div class="select_div__name-list">
<el-divider direction="vertical"></el-divider>
<el-checkbox
v-model="selectAllChecked"
:disabled="selectPersonList.length === 0"
@change="selectCheckboxChange"
></el-checkbox>
<el-divider direction="vertical"></el-divider>
<span>姓名</span>
</div>
<div class="select_div__items" v-if="selectPersonList.length">
<div
class="items_div__detail"
v-for="item in selectPersonList"
:key="item.id"
>
<el-checkbox @change="(val) => clickSelectPerson(val, item)" :value="item.flag"></el-checkbox>
<span>{{ item.name }}</span>
</div>
</div>
<div class="select_div__no-data" v-else><span>暂无数据</span></div>
</div>
<div class="content_div__transfer">
<!-- 向右穿梭 -->
<el-button
icon="el-icon-arrow-right"
:disabled="selectPersonList.filter(ele => ele.flag).length === 0"
@click="rightTransfer"
></el-button>
<!-- 向左穿梭 -->
<el-button
icon="el-icon-arrow-left"
:disabled="selectedPersonList.filter(ele => ele.flag).length === 0"
@click="leftTransfer"
></el-button>
</div>
<!-- 已选择人员 -->
<div class="content_div__selected">
<div class="selected_div__header">
<span>已选择人员 ( {{selectedCheckedPeopleTotal}}/{{selectedPeopleTotal}} )</span>
</div>
<el-input v-model="selectedKeyword" placeholder="搜索">
<i slot="suffix" class="el-input__icon el-icon-search" @click="searchSelectedPeople"></i>
</el-input>
<div class="selected_div__name-list">
<el-divider direction="vertical"></el-divider>
<el-checkbox
v-model="selectedAllChecked"
:disabled="selectedPersonList.length === 0"
@change="selectedCheckboxChange"
></el-checkbox>
<el-divider direction="vertical"></el-divider>
<span>姓名</span>
</div>
<div class="selected_div__items" v-if="selectedPersonList.length">
<div class="items_div__detail" v-for="item in selectedPersonList" :key="item.id">
<el-checkbox
:value="item.flag"
@change="(val) => clickSelectedPerson(val, item)"
></el-checkbox>
<span>{{ item.name }}</span>
</div>
</div>
<div class="selected_div__no-data" v-else><span>暂无数据</span></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'taskDispatch',
components: {},
props: {},
computed: {
// 待选择人员总数
selectPeopleTotal: function(vm) {
return vm.selectPersonListCopy.length;
},
// 待选择人员-已勾选的人员总数
selectCheckedPeopleTotal: function(vm) {
return vm.selectPersonListCopy.filter(ele => ele.flag).length;
},
// 已选择人员总数
selectedPeopleTotal: function(vm) {
return vm.selectedPersonListCopy.length;
},
// 已选择人员-已勾选的人员总数
selectedCheckedPeopleTotal: function(vm) {
return vm.selectedPersonListCopy.filter(ele => ele.flag).length;
},
},
watch: {
treeKeyword(val) {
this.$refs.tree.filter(val);
}
},
data() {
return {
taskName: null, // 任务名称
treeKeyword: null, // 树关键字
selectKeyword: null, // 待选择人员关键字
selectedKeyword: null, // 已选择人员关键字
props: {
children: 'children',
label: 'name',
},
treeData: [],
checkedKey: null, // 被选中的节点key
selectAllChecked: false, // 待选择人员全选
selectedAllChecked: false, // 已选择人员全选
selectPersonList: [], // 待选择人员列表
selectPersonListCopy: [], // 待选择人员列表副本
selectedPersonList: [], // 已选择人员列表
selectedPersonListCopy: [], // 已选择人员列表副本
checked: false, // 包含下级人员
}
},
mounted() {
this.getOrgTreeData();
},
methods: {
// 获取部门树前两级的数据
getOrgTreeData() {
const data = [
{
name: '杭州市',
id: 'd1',
disabled: true,
children: [
{
name: '滨江区',
id: '2',
children: [],
},
{
name: '萧山区',
id: '3',
children: [],
},
]
}
]
this.treeData = data;
},
// 过滤节点
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
// 点击节点
nodeClick(val) {
if (val.id === 'd1') return;
if (val.id === this.checkedKey) {
this.checkedKey = null;
this.$refs.tree.setCurrentKey(null);
this.selectPersonListCopy = this.selectPersonList = [];
} else {
this.checkedKey = val.id;
// pid是用来证明当前人员关联的部门
if (val.id === '2') {
const data = [
{ name: '张三', id: '21', pid: '2' },
{ name: '李四', id: '22', pid: '2' },
{ name: '王五', id: '23', pid: '2' },
{ name: '赵六', id: '24', pid: '2' },
];
const _res = [];
data.map(ele => {
if (this.selectedPersonListCopy.filter(item => item.id === ele.id).length === 0) {
_res.push({ ...ele, flag: false });
}
});
this.selectPersonListCopy = _res;
this.selectPersonList = _res;
} else if (val.id === '3') {
const data = [
{ name: '张六', id: '31', pid: '3' },
{ name: '仵八', id: '32', pid: '3' },
{ name: '唐七', id: '33', pid: '3' },
{ name: '钱玖', id: '34', pid: '3' },
];
const _res = [];
data.map(ele => {
if (this.selectedPersonListCopy.filter(item => item.id === ele.id).length === 0) {
_res.push({ ...ele, flag: false });
}
});
this.selectPersonListCopy = _res;
this.selectPersonList = _res;
}
// this.$get(`${api.tree}?phoneFlag=true&parentIndexCode=${val.id}`).then(({ code, data, msg }) => {
// if (code === '0') {
// const _res = [];
// data.map(ele => {
// if (this.selectedPersonListCopy.filter(item => item.id === ele.id).length === 0) {
// _res.push({ ...ele, flag: false });
// }
// });
// this.selectPersonListCopy = _res;
// this.selectPersonList = _res;
// } else {
// this.$message.error(msg);
// }
// });
}
},
// 搜索待选择人员数据
searchSelectPeople() {
const _res = [...this.selectPersonListCopy];
this.selectPersonList = this.selectKeyword
? _res.filter(ele => ele.name.indexOf(this.selectKeyword) > -1)
: _res;
},
// 已选择人员的总复选框
selectedCheckboxChange(val) {
const _res = [...this.selectedPersonListCopy];
_res.map(ele => { ele.flag = val; });
},
// 点击待选择人员数据的复选框
clickSelectPerson(val, item) {
const _res = [...this.selectPersonListCopy];
const index = _res.findIndex(ele => ele.id === item.id);
_res[index].flag = val;
if (!this.selectKeyword) {
const arr = _res.filter(ele => ele.flag);
if (arr.length === _res.length) {
this.selectAllChecked = true;
} else {
if (this.selectAllChecked) this.selectAllChecked = false;
}
} else {
if (!val) this.selectAllChecked = false;
}
},
// 点击已选择人员数据的复选框
clickSelectedPerson(val, item) {
const _res = [...this.selectedPersonListCopy];
const index = _res.findIndex(ele => ele.name === item.name);
_res[index].flag = val;
if (!this.selectedKeyword) {
const arr = _res.filter(ele => ele.flag);
if (arr.length === _res.length) {
this.selectedAllChecked = true;
} else {
if (this.selectedAllChecked) this.selectedAllChecked = false;
}
} else {
if (!val) this.selectedAllChecked = false;
}
},
// 待选择人员的总复选框
selectCheckboxChange(val) {
const _res = [...this.selectPersonListCopy];
_res.map(ele => { ele.flag = val; });
},
// 搜索已选择人员数据
searchSelectedPeople() {
const _res = [...this.selectedPersonListCopy];
this.selectedPersonList = this.selectedKeyword
? _res.filter(ele => ele.name.indexOf(this.selectedKeyword) > -1)
: _res;
},
// 向左穿梭
leftTransfer() {
const _res = [...this.selectedPersonListCopy];
// 将已选择人员中的已勾选的人员数据向左穿梭到待选择人员
// 此时要判断向左穿梭的数据是否为当前树节点的数据
const _checkedData = _res.filter(ele => ele.flag).map(item => ({ ...item, flag: false }));
const _data = [];
_checkedData.map(ele => {
if (ele.pid === this.checkedKey) {
_data.push(ele);
}
});
this.selectPersonListCopy = this.selectPersonList = [..._data, ...this.selectPersonListCopy];
// 删除已选择人员中的已勾选的人员数据
this.selectedPersonListCopy = this.selectedPersonList = _res.filter(ele => !ele.flag);
if (this.selectedAllChecked) this.selectedAllChecked = false;
},
// 向右穿梭
rightTransfer() {
const _res = [...this.selectPersonListCopy];
// 将待选择人员中的已勾选的人员数据穿梭到已选择人员
const _data = _res.filter(ele => ele.flag).map(item => ({ ...item, flag: false }));
this.selectedPersonListCopy = this.selectedPersonList = [...this.selectedPersonListCopy, ..._data];
// 删除待选择人员中的已勾选的人员数据
this.selectPersonListCopy = this.selectPersonList = _res.filter(ele => !ele.flag);
if (this.selectAllChecked) this.selectAllChecked = false;
},
},
}
</script>
<style lang="scss" scoped>
.task-dispatch {
height: 90%;
width: 90%;
padding-left: 10%;
padding-top: 10%;
.task-dispatch_div__content {
height: 410px;
width: 100%;
display: flex;
.content_div__tree {
height: 408px;
border: 1px solid #e0e0e0;
width: calc(239px - 32px);
padding: 0 16px;
border-right: none;
border-radius: 2px 0 0 2px;
.tree_div__header {
height: 44px;
line-height: 44px;
width: 100%;
font-size: 14px;
color: rgba(0,0,0,0.90);
}
::v-deep .el-input__inner {
height: 32px!important;
line-height: 32px!important;
}
::v-deep .el-input__icon {
line-height: 32px!important;
}
::v-deep .el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
background-color: #dbeafe!important;
}
.tree_div__content {
height: 326px;
margin-top: 6px;
width: 100%;
overflow: auto;
.content_slot {
display: flex;
align-items: center;
}
}
}
.content_div__select {
height: 408px;
width: calc(310px - 32px);
border: 1px solid #e0e0e0;
border-radius: 0 2px 2px 0;
padding: 0 16px;
.select_div__header {
height: 44px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
span {
font-size: 14px;
color: rgba(0,0,0,0.90);
}
}
::v-deep .el-input__inner {
height: 32px!important;
line-height: 32px!important;
}
::v-deep .el-input__icon {
line-height: 32px!important;
}
.select_div__name-list {
height: 36px;
width: 100%;
display: flex;
align-items: center;
background-color: #ebebeb;
margin-top: 6px;
font-size: 14px;
color: rgba(0, 0, 0, 0.90);
.el-divider:nth-child(1) {
margin-left: 1px;
margin-right: 12px;
}
.el-divider:nth-child(3) {
margin: 0 12px;
}
}
.select_div__items {
height: calc(100% - 126px);
width: 100%;
margin-top: 6px;
overflow: auto;
.items_div__detail {
height: 35px;
width: 100%;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
font-size: 14px;
color: rgba(0,0,0,0.70);
cursor: pointer;
.el-checkbox {
margin: 0 15px;
}
}
}
.select_div__no-data {
height: calc(100% - 126px);
width: 100%;
margin-top: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: rgba(0,0,0,0.70);
}
}
.content_div__transfer {
width: 80px;
height: 410px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.el-button:nth-child(2) {
margin-left: 0;
margin-top: 12px;
}
}
.content_div__selected {
height: 408px;
width: calc(310px - 32px);
border: 1px solid #e0e0e0;
border-radius: 2px;
padding: 0 16px;
.selected_div__header {
height: 44px;
width: 100%;
display: flex;
align-items: center;
font-size: 14px;
color: rgba(0,0,0,0.90);
}
::v-deep .el-input__inner {
height: 32px!important;
line-height: 32px!important;
}
::v-deep .el-input__icon {
line-height: 32px!important;
}
.selected_div__name-list {
height: 36px;
width: 100%;
display: flex;
align-items: center;
background-color: #ebebeb;
margin-top: 6px;
font-size: 14px;
color: rgba(0, 0, 0, 0.90);
.el-divider:nth-child(1) {
margin-left: 1px;
margin-right: 12px;
}
.el-divider:nth-child(3) {
margin: 0 12px;
}
}
.selected_div__items {
height: calc(100% - 126px);
width: 100%;
margin-top: 6px;
overflow: auto;
.items_div__detail {
height: 35px;
width: 100%;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
font-size: 14px;
color: rgba(0,0,0,0.70);
cursor: pointer;
.el-checkbox {
margin: 0 15px;
}
}
}
.selected_div__no-data {
height: calc(100% - 126px);
width: 100%;
margin-top: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: rgba(0,0,0,0.70);
}
}
}
}
</style>
在A项目中对于使用到异步树组件的业务逻辑,存在的问题是:当子节点数量较多时,点击树节点复选框,复选框显示为勾选状态存在延迟卡顿(从点击操作到显示勾选)。推荐使用ant-design-vue@4.x版本,在tree组件中添加height属性,设置虚拟加载。
// 在异步树的搜索功能中,根据关键字搜索,接口响应的数据中叶子节点的名称中包含<font style="color: red">关键字</font>,此时需要使用正则将font标签替换掉,在tree组件中通过自定义的方式高亮关键字
/**
* 正则去除字符串中的font标签
*/
function regFilterFont(str) {
const pattern = /<font color="red">([^<]+)<\/font>/g;
const replacement = '$1';
return str.replace(pattern, replacement);
}
// '123231<font style="color: red">2222<font>3333' --> '12323122223333'