前言
多级选择器在许多项目的业务需求都需要用到,例如常见的省市区的选择、员工所属公司的具体机构等等,为此设计一个高效、好用的多级选择器非常有必要。本次多级选择器后端将使用递归的方式一次性将数据返回给前端,极大提升了系统的响应速度;后端使用了递归的方式处理数据,也意味着数据库的设计需要存在父子关系的结构;前端将引入element-ui的选择器组件,再加上一些逻辑判断,即可实现多次选择器。
数据库表设计
通常一个项目数据库表的设计,需要系统根据具体的业务逻辑来制定。由于多级选择器需要的数据在后端使用递归的方式实现,为此数据库表需要设计出父子关系的数据供后端使用,具体的数据库表设计如下代码所示。
DROP TABLE IF EXISTS `institution`;
CREATE TABLE `institution` (
`i_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '机构id',
`i_pid` int(11) NULL DEFAULT NULL COMMENT '父机构id',
`i_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '机构名称',
`gmt_create` datetime NULL DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime NULL DEFAULT NULL COMMENT '修改时间',
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`i_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
需要注意的一点,第一层级的数据,没有父数据,程序默认父数据为0时,便是第一层级的数据,创建的数据如下图所示。
后端实现
后端需要先拿到数据库表存储的所有机构数据,再调用建树方法,具体代码如下所示。
@Override
public List<Institution> queryAllInstitution() {
//查询所有机构
QueryWrapper<Institution> wrapper=new QueryWrapper<>();
wrapper.orderByDesc("i_id");
List<Institution> institutionList = baseMapper.selectList(wrapper);
//递归调用
List<Institution> result=build(institutionList);
return result;
}
上述build方法即是建树过程,需要先在所有数据中找到第一层数据,前面已经规定好了,父数据为0的就是第一层的数据,具体代码如下所示。
//使用递归方法把第一层树建立起来
public static List<Institution> build(List<Institution> treeNodes){
List<Institution> trees=new ArrayList<>();
for (Institution treeNode:treeNodes) {
if ("0".equals(treeNode.getiPid()+"")){
treeNode.setLevel(1);
trees.add(findChildren(treeNode,treeNodes));
break;
}
}
return trees;
}
找到第一层后,便可使用上述的findChildren方法,递归查找其子节点,查找子节点就是比对每个节点的父数据是否与该节点的id相同,若相同,便有父子关系,具体代码实现如下所示。
//递归查找子节点
private static Institution findChildren(Institution treeNode
, List<Institution> treeNodes) {
//设置子节点对象不为空
treeNode.setChildren(new ArrayList<Institution>());
//遍历找到它的孩子节点
for (Institution it: treeNodes) {
if ((it.getiPid()+"").equals(treeNode.getiId()+"")){//找到孩子(用整型==也行)
int level =treeNode.getLevel()+1;
it.setLevel(level);//层数加1
if (treeNode.getChildren()==null)
treeNode.setChildren(new ArrayList<>());
//关键代码
treeNode.getChildren().add(findChildren(it,treeNodes));
}
}
return treeNode;
}
上面所使用的实体类,除了需要引用数据库的字段,还需要额外添加几个字段,具体代码如下所示。
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import java.time.LocalDateTime;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* <p>
*
* </p>
*
* @author itfeng
* @since 2022-12-04
*/
public class Institution extends Model<Institution> {
private static final long serialVersionUID = 1L;
/**
* 机构id
*/
@TableId(value = "i_id", type = IdType.AUTO)
private Integer iId;
/**
* 父机构id
*/
private Integer iPid;
/**
* 机构名称
*/
private String iName;
@TableField(exist = false)
private String pName;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)//使用myabtis-plus的自动注入功能实现
private Date gmtCreate;
/**
* 修改时间
*/
@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)//使用myabtis-plus的自动注入功能实现
private Date gmtModified;
/**
* 是否删除
*/
@TableLogic
private Boolean isDeleted;
//以下的实体属性便是额外引入
@ApiModelProperty(value = "层级")
@TableField(exist = false)
private Integer level;
@ApiModelProperty(value = "下级")
@TableField(exist = false)
private List<Institution> children;
@ApiModelProperty(value = "是否选中")
@TableField(exist = false)
private boolean isSelect;
public String getpName() {
return pName;
}
public void setpName(String pName) {
this.pName = pName;
}
public Integer getLevel() {
return level;
}
public void setLevel(Integer level) {
this.level = level;
}
public List<Institution> getChildren() {
return children;
}
public void setChildren(List<Institution> children) {
this.children = children;
}
public boolean isSelect() {
return isSelect;
}
public void setSelect(boolean select) {
isSelect = select;
}
public Integer getiId() {
return iId;
}
public void setiId(Integer iId) {
this.iId = iId;
}
public Integer getiPid() {
return iPid;
}
public void setiPid(Integer iPid) {
this.iPid = iPid;
}
public String getiName() {
return iName;
}
public void setiName(String iName) {
this.iName = iName;
}
public Date getGmtCreate() {
return gmtCreate;
}
public void setGmtCreate(Date gmtCreate) {
this.gmtCreate = gmtCreate;
}
public Date getGmtModified() {
return gmtModified;
}
public void setGmtModified(Date gmtModified) {
this.gmtModified = gmtModified;
}
public Boolean getIsDeleted() {
return isDeleted;
}
public void setIsDeleted(Boolean isDeleted) {
this.isDeleted = isDeleted;
}
@Override
protected Serializable pkVal() {
return this.iId;
}
@Override
public String toString() {
return "Institution{" +
"iId=" + iId +
", iPid=" + iPid +
", iName=" + iName +
", gmtCreate=" + gmtCreate +
", gmtModified=" + gmtModified +
", isDeleted=" + isDeleted +
"}";
}
}
前端实现
在获取后端数据之前,先引入element-ui的选择器组件,具体代码如下所示。
<el-col :span="4" class="grid-cell">
<el-form-item label="一级机构:" prop="rL1Iid" class="required">
<el-select
v-model="formData.rL1Iid"
@change="LevelOneChanged"
class="full-width-input"
clearable
placeholder="一级机构"
:disabled="readonly"
>
<el-option
v-for="(item, index) in rL1IidOptions"
:key="index"
:label="item.iName"
:value="item.iId"
:disabled="item.disabled"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4" class="grid-cell">
<el-form-item label="二级机构:" prop="rL2Iid" class="required">
<el-select
v-model="formData.rL2Iid"
@change="LevelTwoChanged"
class="full-width-input"
clearable
placeholder="二级机构"
:disabled="readonly"
>
<el-option
v-for="(item, index) in rL2IidOptions"
:key="index"
:label="item.iName"
:value="item.iId"
:disabled="item.disabled"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4" class="grid-cell">
<el-form-item label="三级机构:" prop="rL3Iid" class="required">
<el-select
v-model="formData.rL3Iid"
class="full-width-input"
clearable
placeholder="三级机构"
:disabled="readonly"
>
<el-option
v-for="(item, index) in rL3IidOptions"
:key="index"
:label="item.iName"
:value="item.iId"
:disabled="item.disabled"
></el-option>
</el-select>
</el-form-item>
</el-col>
在该页面加载时,需要先连接后端接口,拿到所有数据,第一级机构的数据便有,具体代码如下所示。
//使用vue的created生命周期
created() {
this.initL1Institution(); //初始化一级机构
},
//初始化一级机构的数据
initL1Institution() {
queryAllInstitution().then((re) => {//接入后端接口
this.rL1IidOptions = re.data.list;//拿到所有机构的数据
});
},
在一级机构加上change事件,只要一级机构发生改变,便赋值好二级机构的数据,具体代码如下所示。
//获取二级机构
LevelOneChanged(value) {
console.log(value);
for (let i = 0; i < this.rL1IidOptions.length; i++) {
if (this.rL1IidOptions[i].iId === value) {
this.rL2IidOptions = this.rL1IidOptions[i].children;
//解决切换问题
this.formData.rL2Iid = "";
}
}
},
三级机构数据的赋值,与上述二级机构数据的赋值一致,只要二级机构数据发生改变,便触发函数完成三级机构数据的赋值操作,具体代码如下所示。
//获取三级机构
LevelTwoChanged(value) {
console.log(value);
for (let i = 0; i < this.rL2IidOptions.length; i++) {
if (this.rL2IidOptions[i].iId === value) {
this.rL3IidOptions = this.rL2IidOptions[i].children;
//解决切换问题
this.formData.rL3Iid = "";
}
}
},
测试效果
当一开始加载页面,可选择一级机构的数据,此时二级和三级机构均无数据,具体效果如下图所示。
选择了一级机构之后,便会出现二级机构的数据,具体效果如下图所示。
同理,选完二级机构,三级机构的数据也触发完成赋值。
总结
上述便是作者实现多级选择器的全过程,主要是讲解思路为主,文中的代码均是伪代码。读者若发现文中有不足之处或者有更好的方法,欢迎在评论区讨论。