多级选择器的前后端实现

前言

多级选择器在许多项目的业务需求都需要用到,例如常见的省市区的选择、员工所属公司的具体机构等等,为此设计一个高效、好用的多级选择器非常有必要。本次多级选择器后端将使用递归的方式一次性将数据返回给前端,极大提升了系统的响应速度;后端使用了递归的方式处理数据,也意味着数据库的设计需要存在父子关系的结构;前端将引入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 = "";
        }
      }
    },

测试效果

当一开始加载页面,可选择一级机构的数据,此时二级和三级机构均无数据,具体效果如下图所示。
在这里插入图片描述
选择了一级机构之后,便会出现二级机构的数据,具体效果如下图所示。

在这里插入图片描述
同理,选完二级机构,三级机构的数据也触发完成赋值。

在这里插入图片描述

总结

上述便是作者实现多级选择器的全过程,主要是讲解思路为主,文中的代码均是伪代码。读者若发现文中有不足之处或者有更好的方法,欢迎在评论区讨论。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值