一、需求描述
vue2,基于elementUI,封装el-tree和el-select组件,形成公司组织架构树,部门-人员树。支持多选、树形结构搜索等功能;
二、数据结构
整体上是一个长度为1的[{}]
对象数组,内部包含了children、id、label(公司/部门名)、type。children也是对象数组,如果children下包含的是人员,则有departId、id、label(人名)、type(user)、userDisplayName(人中文名字)、userName(人英文名字)字段,如果children下包含的是部门,则有children、id、label(部门名)、type(depart)字段。层层嵌套~
结构示例
:
第一层:公司名
第二层:一级部门领导、二级部门1、二级部门2(children展开示例)
第三层:三级部门1、三级部门2、三级部门3、三级部门4(children展开示例)、三级部门5
第四层:四级部门1、2、3领导数据、四级部门1、四级部门2、四级部门3(children展开示例)
[{
children:[
{...},{
children:[{
departId:xxx,
id:'xxx_user',
label:'人名',// 一级部门领导
type:'user',
userDisplayName:'人名-中文名',
userName:'人名-英文名'
},
{
children:[{...}], // 省略二级部门名1的children的展示,具体看二级部门名2
id:'111_depart',
label:'二级部门名1',
type:'depart'
},
{
children:[{
children:[{...}],// 省略三级部门名1的chilren展示,具体看三级部门名4
id:'xxx_depart',
label:'三级部门名1',
type:'depart'
},
{
children:[{...}],// 省略
id:'xxx_depart',
label:'三级部门名2',
type:'depart'
},
{
children:[{...}],// 省略
id:'xxx_depart',
label:'三级部门名3',
type:'depart'
},
{
children:[{
departId:xxxx,
id:'xxx_user',
label:'人名',// 四级部门1、2、3领导
type:'user',
userDisplayName:'人名-中文名',
userName:'人名-英文名'
},
{...},// 四级部门1
{...},// 四级部门2
{
children:[{
departId:@@@@,
id:'xxxx_user',
label:'人名',// 四级部门成员
type:'user',
userDisplayName:'人名-中文名',
userName:'人名-英文名'
},{
departId:@@@@,
id:'xxxx_user',
label:'人名',// 四级部门成员
type:'user',
userDisplayName:'人名-中文名',
userName:'人名-英文名'
}],
id:'@@@@_depart',
label:'四级部门名3', // 四级部门3及其children数据
type:'depart'}
}],
id:'xxx_depart',
label:'三级部门名4',
type:'depart'
},
{
children:[{...}],// 省略
id:'xxx_depart',
label:'三级部门名5',
type:'depart'
}],
id:'2222_depart',
label:'二级部门名2',
type:'depart'
}]
id:'xxx_depart',
label:'一级部门名',
type:'depart'
},...,{...}
],
id:'1_depart',
label:'公司名',
type:'depart''
}]
三、实现
3.1 实现思路
step1:基础功能搭建
先搭建好el-select和el-tree结构,然后通过getTreeData()
获取树形结构数据,然后通过触发el-tree的@check
事件,将用户选择的树形结构数据传给父组件,结合vue2的父子数据传递方式props
和$emit
给传给子组件作为:value的属性赋值,可以实现el-select+el-tree的选中和回显的效果;
step2:进一步完善:
1.el-select添加filter-method
搜索功能,结合el-tree提供的filter
方法,完成搜索功能;
2.在el-select清除选中的时候,要通过@change
方法清除el-tree对应的数据,将变化emit给父组件,更新:value的值;
3.通过el-tree的node-click
事件让人员添加功能支持点击复选框外的文字也生效;
step3:优化:
通过el-select的focus触发焦点事件,改造树形控件展示结构;
3.2 解释
Props
- 接收来自父组件的属性如
placeholder
,disabled
,value
,collapseTags
, 和clearable
,这些用于控制组件的行为和样式
Data
treeData
: 存储树形结构数据。defaultTreeProps
: 配置el-tree
的属性映射。defaultKeys
: 默认展开的树节点键值。initTreeSelect
: 标记是否初始化过树选择数据。isFocus
: 标记el-select
是否处于聚焦状态。
Methods
filterTreeNode
: 过滤树节点的方法,用于在树中搜索。selectorFilter
: 控制el-select
的过滤行为,只有当el-select
聚焦时才进行过滤。getTreeData
: 从服务器获取树形结构数据,并处理初始数据的回显。onNodeClick
: 当树中的节点被点击并选中时触发,处理选中节点的数据。searchData
: 当选择数据发生变化时触发,更新树中选中节点的状态。setTreeCheckedNodes
: 设置树中选中节点的方法。onBlur
: 当el-select
失去焦点时触发。onFocus
: 当el-select
获得焦点时触发,同时进行一些初始化操作。handleNodeClick
: 当树中的节点被点击时触发,处理节点的选中状态。findUsers
: 递归搜索树中的用户节点,用于回显数据。transformData
: 根据 URL 中携带的值来刷新和回显树中的选中状态。
四、代码
3.1 子组件
<template>
<div>
<el-select
ref="userSelector"
:value="value"
value-key="id"
filterable
:multiple="true"
:disabled="disabled"
:filter-method="selectorFilter"
:placeholder="placeholder"
:clearable="clearable"
:collapse-tags="collapseTags"
style="width: 100%"
popper-class="user-depart-selector-popper"
@change="searchData"
@focus="onFocus"
@blur.capture.native="onBlur"
>
<el-option
style="width: 100%; height: auto; max-height: available; overflow: auto; background-color: #fff"
label="adminName"
value="adminName"
disabled
>
<el-tree
ref="projectTree"
:data="treeData"
:props="defaultTreeProps"
node-key="id"
:default-expanded-keys="defaultKeys"
:filter-node-method="filterTreeNode"
show-checkbox
@check="onNodeClick"
accordion
@node-click="handleNodeClick"
></el-tree>
</el-option>
<!-- 用于select展示选择数据用的-没有这部分代码el-select不会展示任何内容 -->
<el-option
v-show="false"
v-for="item in value"
:key="item.id"
style="width: 100%; height: auto; max-height: available; overflow: auto; background-color: #fff"
:label="item.userDisplayName"
:value="item"
></el-option>
</el-select>
</div>
</template>
<script>
import { CommonService } from '@/api/api';
export default {
name: 'userTreeSelectMutiple',
props: {
placeholder: {
default: '人员'
},
disabled: {
type: Boolean,
default: false
},
value: {
type: Array,
default: () => {
return [];
}
},
// 多选时是否将选中值按文字的形式展示
collapseTags: {
type: Boolean,
default: true
},
clearable: {
type: Boolean,
default: false
}
},
data() {
return {
treeData: null,
defaultTreeProps: {
children: 'children',
label: 'label'
},
defaultKeys: ['1_depart'], // 默认展开的部门-公司名展开-与顶层数据的id对应
initTreeSelect: false, // 初始化树选择数据
isFocus: false
};
},
created() {
this.getTreeData();
},
mounted() {},
methods: {
filterTreeNode(value, data) {
if (!value) return true;
if (data.userName == undefined) {
return data.label.indexOf(value) !== -1;
} else {
return data.userName.indexOf(value) !== -1 || data.label.indexOf(value) !== -1;
}
},
selectorFilter(value) {
if (this.isFocus) {
this.$refs.projectTree.filter(value);
}
},
getTreeData() {
CommonService.getAllDepartmentUsers().then((res) => {
if (res.status == 200) {
if (res.data.success) {
this.treeData = res.data.data;
} else {
this.$message({ message: res.data.message, type: 'error' });
}
} else {
this.$message({ message: '服务端请求异常', type: 'error' });
}
});
},
onNodeClick(item, { checkedNodes = [] }) {
// 过滤出部门节点-否则点击部门也会选中部门名
const filteredNodes = checkedNodes.filter((node) => node.type !== 'depart');
const list = filteredNodes.map((item) => {
return {
id: item.id,
userType: item.type,
userDisplayName: item.label,
userName: item.userName
};
});
this.$emit('change', list); // 把用户勾选的情况给父组件,让父组件去更新select的值进行填充
},
// 搜索清除的时候触发
searchData(data) {
const checkedKeys = data.map((item) => {
return {
id: item.id,
label: item.userDisplayName
};
});
this.$refs.projectTree.setCheckedNodes(checkedKeys);
this.$emit('change', data);
},
setTreeCheckedNodes(data) {
const checkedKeys = data.map((item) => {
return {
id: item.id,
label: item.userDisplayName
};
});
this.$refs.projectTree.setCheckedNodes(checkedKeys);
},
onBlur() {
this.isFocus = false;
},
onFocus() {
this.isFocus = true;
if (!this.initTreeSelect) {
this.setTreeCheckedNodes(this.value);
}
this.$refs.projectTree.filter();
// 因为搜索以后,树状结构只会保留筛选以后的数据,所以需要定位到当前人,展开当前人的所有父节点
const nodes = this.$refs.projectTree.store.nodesMap;
for (let item in nodes) {
nodes[item].expanded = false;
if (item === this.defaultKeys[0]) {
nodes[item].expanded = true;
}
}
},
// 点击节点选中
handleNodeClick(node, nodeData) {
// 人名都是叶子节点且未选中的情况下才传给父组件
if (nodeData.isLeaf && !nodeData.checked) {
this.$refs.projectTree.setChecked(nodeData, !node.checked);
const temp = this.$refs.projectTree.getCheckedNodes();
const filteredNodes = temp.filter((node) => node.type !== 'depart'); // 过滤部门全选后的depart节点
this.$emit('change', filteredNodes);
}
}
}
};
</script>
代码到这里就可以结束了,但是我有特别的需求,用户刷新浏览器后,要保留之前选中的数据回显,而且刷新后我能拿到的、传给子组件作为:value的数据是一个字符串数组,形如[‘Lucy’,'Bob','Lucky']
这样的人员英文名,所以需要去树结构中,也就是在treeData
里匹配出对应的节点数据,然后emit给父组件更:value的值~所以代码又有了部分改动~也一起贴出来
step1:
修改getTreeData
函数,如果传给子组件的this.$props.value有值,说明用户进行了刷新操作,且拿到了不为空的字符串数组,需要进行回显。此时,数据回显的匹配得在拿到treeData后才能进行。因为getTreeData
是在created
生命周期里被调用的,所以只有刷新的时候会执行一次,并不会影响后续父子组件的交互;
getTreeData() {
CommonService.getAllDepartmentUsers().then((res) => {
if (res.status == 200) {
if (res.data.success) {
this.treeData = res.data.data;
// 刷新后url中拿到的[‘’]字符串数组,回显到select中
if (this.$props.value.length > 0) {
// 根据url的英文名匹配对应树形结构的数据
this.transformData();
}
} else {
this.$message({ message: res.data.message, type: 'error' });
}
} else {
this.$message({ message: '服务端请求异常', type: 'error' });
}
});
},
step2:
补充transformData()
方法,
// 刷新回显
findUsers(treeData, usernames) {
const results = [];
function search(node) {
if (node.type === 'user' && usernames.includes(node.userName)) {
results.push(node);
}
if (node.children) {
node.children.forEach((child) => search(child));
}
}
// 首先确保treeData数组存在并且有内容,然后获取第一个元素
if (treeData.length > 0 && treeData[0].children) {
treeData[0].children.forEach((node) => search(node));
}
return results;
},
transformData() {
const temp = this.findUsers(this.treeData, this.$props.value);
// 匹配到的数结构emit给父组件,让父组建更新:value的值-回显
this.$emit('change', temp);
}
3.2 父组件使用
<user-tree-select-mutiple
:value="filters.createrUsers"
@change="createrUsersChange"
:clearable="true"
style="width: 140px">
</user-tree-select-mutiple>
data(){
return {
filters:{
createrUsers: [],
}
}
}
createrUsersChange(val) {
this.filters.createrUsers = val;// 赋值-更新子组件:value
this.queryEventHandle();// 传值给后端-省略
},
注意:此时拿到的val是整个节点的数据,给后端传值的时候,根据需求进行过滤,比如我需要的是人员英文名,选中多个人员,用逗号拼接。那我传给后端的数据应该转换成:
filters.createrUsers = this.filters.createrUsers.map((user) => user.userName).join(',');
四、资料
el-select相关 | 说明 |
---|---|
value-key | 作为 value 唯一标识的键名,绑定值为对象类型时必填 |
filter-method | 自定义搜索方法 |
collapse-tags | 多选时是否将选中值按文字的形式展示 |
el-tree相关 | 说明 |
---|---|
props | 配置选项 |
node-key | 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 |
default-expanded-keys | 默认展开的节点的 key 的数组 |
filter-node-method | 对树节点进行筛选时执行的方法,返回 true 表示这个节点可以显示,返回 false 则表示这个节点会被隐藏 |
@check | 当复选框被点击的时候触发 |
accordion | 是否每次只打开一个同级树节点展开 |
@node-click | 节点被点击时的回调:共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 |
filter | 对树节点进行筛选操作 |
setCheckedNodes | 设置目前勾选的节点,使用此方法必须设置 node-key 属性 |
setChecked | 通过 key / data 设置某个节点的勾选状态,使用此方法必须设置 node-key 属性 |
getCheckedNodes | 若节点可被选择(即 show-checkbox 为 true ),则返回目前被选中的节点所组成的数组 |
Plus:这次的需求是基于带我的小姐姐之前的相关代码改出来的,改出来后受益良多~