手动实现cascader级联选择器

1,129 阅读6分钟

element-ui的级联选择器

什么是级联选择器

级联选择是指在选择器Select选项数量较大时,采用多级分类的方式将选项进行分隔,便于用户选择。

构成元素说明
1.选择器可以只支持选择,或者可以支持直接搜索选项并选择
2.下拉面板在同一个浮层中包含多个层级的菜单,选择每个层级时需要反馈选项结果至选择器中

级联选择器在下拉数据能够一次性全部获取的情况下,使用element-ui的级联是比较便捷的,通过接口获取的话,如果是同一个接口,下拉选项的key键值是同名字段的话,也可以使用el-cascader的动态加载lazyLoad方法。但是实际开发中由于需求与接口的限制,el-cascader的懒加载也不是适用于所有情况的,比如如果接口是两级级联,

el-cascader代码如下:

async orgLazyLoad(node, resolve) {
  const { value, hasChildren, children } = node;
  // 如果当前项有子级(hasChildren为true)且子级数据还未加载(children.length=0),才发请求获取下一级数据
  if (hasChildren && children.length == 0) {
    getChildOrgList({
      id: value,
    }).then((res) => {
      const { code, data } = res.data;
      if (code == 200) {
        let dataList = [];
        dataList = data.map((item) => {
          item.leaf = !item.hasNext;//给每一项添加标识,用来判断是否是最后一级
          return item;
        });
        resolve(dataList);
      } else {
        resolve([]);
      }
    });
    // 加载第二级选项
    if(node.level==1){
      this.data2List = []
      await this.queryData2Data(1,node.value) //传递第一级参数,获取第二级接口的所有数据
      resolve(this.data2List); //将第二级数据渲染在组件列表上
    }
    else{
      resolve([]);
    }
  } else {
    resolve([]);
  }
},

这种用法要求级联每一级下拉框选项数据的键值key和展示字段label是相同命名的字段,这种情况在级联接口每一级都是不同接口的情况下看起来不太适用,接口每一级的键值和name字段名都不同,需要对接口返回数据做特殊处理,进行二次的加工,来统一成同一个格式的键值与标签名,进行过这个操作后,在提交数据时还需要对二次加工的数据转回原字段,这样处理是很麻烦的,而且代码可读性低,不利于后续的维护与功能的扩展,再加上这个需求要求级联的每一级有独立的搜索,其中一级的接口支持接口模糊查询,另一级不支持模糊查询,只能在前端处理匹配实现前端的模糊查询,所以决定自己拼装实现一个适用于这种场景的级联选择器,并封装成了组件。

自己实现封装的级联选择器

实现的思路与方案

这个场景要求的级联是这样的,已勾选内容独立于级联选择器存在,级联选择器的每一级都可以通过搜索框搜索,一共两级,而接口方面,获取一级与二级的接口都是分页查询,需要传页码和页的大小,一级的接口不支持接口查询,需要前端循环递归获取所有的数据来在前端进行匹配实现模糊查询,二级的接口可以通过接口查询,且需要传一级选择数据的key来进行查询,在这种情况下,自己实现级联选择器可以保证更好的契合需求与接口,而且自由度高,未来的版本扩展功能也是比较方便的。

实现方案:封装为一个vue弹窗组件,传入参数有dialogShow(父组件控制弹窗的显示与隐藏)、tenant(对象格式,接口查询级联一级列表时需要传入的参数)、initData(数组格式,已选择的级联结果的数组,用于回显),具体部分在代码中会有说明。

<style lang="scss">
.cascader-area {
    .selected-area {
        width: 260px;
        min-height: 30px;
        max-height: 150px;
        overflow-y: auto;
        border: 1px solid #dcdfe6;
        border-radius: 3px;
        .selected-tag {
            width: 240px;
            margin-left: 5px;
            .selected-text {
                display: inline-block;
                vertical-align: middle;
                width: 210px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
        }
    }
    .options-area {
        margin-top: 20px;
        width: 500px;
        height: 300px;
        .options-item {
            width: 250px;
            height: 300px;
            border: 1px solid #dcdfe6;
            float: left;
            .input-with-select {
                margin: 6px;
                width: 230px;
                padding-bottom:3px;
            }
            .options-content {
                height: 254px;
                overflow-y: auto;
                .content-item {
                    padding-bottom:3px;
                    .item-checkbox {
                        margin: 0 5px 0 10px;
                        transform: translate(0, -3px);
                    }
                    .option-content-text {
                        display: inline-block;
                        width: 180px;
                        white-space: nowrap;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        cursor: pointer;
                        font-size: 14px;
                        line-height: 14px;
                    }
                    .option-content-text-active {
                        color: #409eff;
                        display: inline-block;
                        width: 180px;
                        white-space: nowrap;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        cursor: pointer;
                        font-size: 14px;
                        line-height: 14px;
                    }
                }
            }
            .options-content-loading {
                height: 30px;
                margin: 100px 0 20px 0;
            }
        }
    }
}
//如下都是滚动条样式的优化
.options-content::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}
.options-content::-webkit-scrollbar-thumb {
    border-radius: 5px;
    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
    background: rgba(0, 0, 0, 0.1);
}
.options-content::-webkit-scrollbar-track {
    border-radius: 5px;
    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
    background: rgba(0, 0, 0, 0.03);
}
.selected-area::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}
.selected-area::-webkit-scrollbar-thumb {
    border-radius: 5px;
    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
    background: rgba(0, 0, 0, 0.1);
}
.selected-area::-webkit-scrollbar-track {
    border-radius: 5px;
    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
    background: rgba(0, 0, 0, 0.03);
}
</style>
<template>
    <!-- 添加数据1和数据2弹窗 -->
    <el-dialog
        :visible="dialogShow"
        style="width: 1500px; margin: 0 auto; overflow: visible"
        title="添加数据1和数据2"
        center
        @opened="initData()"
        @close="handleClose()"
    >
        <el-main>
            <el-form label-width="130px" label-position="left">
                <el-form-item label="数据1和数据2" :required="true">
                    <div class="cascader-area">
                        <div class="selected-area">     <!-- 这部分是级联已选项的区域 -->
                            <el-tag
                                v-for="item in tempData1AndCenter"
                                :key="item.data2er_code"
                                closable
                                @close="deleteData(item)"
                                class="selected-tag"
                            >
                                <el-popover
                                    placement="right"
                                    trigger="hover"
                                    :content="
                                        item.name +
                                        '/' +
                                        item.data2er_name
                                    "
                                >     <!-- 级联关系的数据可能会特别长,所以超出宽度的内容截断,通过鼠标的hover来展示完整内容 -->
                                    <span slot="reference" class="selected-text"
                                        >{{ item.name }}/{{
                                            item.data2er_name
                                        }}</span
                                    >
                                </el-popover>
                            </el-tag>
                        </div>
                        <div class="options-area">
                            <div class="options-item">
                                <el-input
                                    placeholder="请输入关键字搜索数据1"
                                    v-model="inputData1"
                                    class="input-with-select"
                                    @keyup.enter.native="searchData1()"
                                >
                                    <el-button
                                        slot="append"
                                        icon="el-icon-search"
                                        @click="searchData1()"
                                    ></el-button>
                                </el-input>
                                <div class="options-content">
                                    <div
                                        v-for="item in data1List"
                                        :key="item.code"
                                        class="content-item"
                                    >
                                        <el-popover
                                            placement="right"
                                            trigger="hover"
                                            :content="item.name"
                                        >
                                            <span
                                                slot="reference"
                                                @click="selectData1(item)"
                                                :class="
                                                    item.name ==
                                                    selectedData1.name
                                                        ? 'option-content-text-active'
                                                        : 'option-content-text'
                                                "
                                                style="margin-left: 15px"
                                                >{{ item.name }}</span
                                            >       <!-- 三目运算判断这里是否添加已选样式 -->
                                        </el-popover>
                                        <i
                                            class="el-icon-arrow-right"
                                            :style="
                                                item.name ==
                                                selectedData1.name
                                                    ? 'color:#409eff'
                                                    : ''
                                            "
                                        ></i>
                                    </div>
                                </div>
                            </div>
                            <div
                                class="options-item"
                            >
                                <el-input
                                    placeholder="请输入关键字搜索数据2"
                                    v-model="inputData2"
                                    class="input-with-select"
                                    @keyup.enter.native="searchData2()"
                                >
                                    <el-button
                                        slot="append"
                                        icon="el-icon-search"
                                        @click="searchData2()"
                                    ></el-button>
                                </el-input>
                                <div
                                    class="options-content-loading"
                                    v-if="this.isLoadingData2"
                                    v-loading="isLoadingData2"
                                ></div>
                                <div class="options-content" v-else>
                                    <div
                                        v-for="item in data2List"
                                        :key="item.aCode"
                                        class="content-item"
                                        @click.stop.prevent="
                                            addData(item)
                                        "
                                    >
                                        <el-checkbox
                                            :value="
                                                tempData1AndCenter
                                                    .map(
                                                        (item) =>
                                                            item.data2er_code
                                                    )
                                                    .indexOf(
                                                        item.aCode
                                                    ) > -1
                                            "
                                            class="item-checkbox"
                                        ></el-checkbox>
                                        <el-popover
                                            placement="right"
                                            trigger="hover"
                                            :content="item.aName"
                                        >
                                            <span
                                                slot="reference"
                                                :class="
                                                    tempData1AndCenter
                                                        .map(
                                                            (item) =>
                                                                item.data2er_code
                                                        )
                                                        .indexOf(
                                                            item.aCode
                                                        ) > -1
                                                        ? 'option-content-text-active'
                                                        : 'option-content-text'
                                                "
                                                >{{ item.aName }}</span
                                            >
                                        </el-popover>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </el-form-item>
            </el-form>
        </el-main>
        <span slot="footer" class="dialog-footer">
            <el-button @click="$emit('updata:dialogShow', false);"
                >取 消</el-button
            >
            <el-button type="primary" @click="submitDataForm()"
                >添加</el-button
            >
        </span>
    </el-dialog>
</template>
<script>
import axios from "axios";
axios.defaults.withCredentials = true;
​
export default {
  props: {
        dialogShow: {
            type: Boolean,
            default: false,
        },
        tenant: {
            type: Object,
            default: () => ({}),
        },
        initData: {
            typeof: Array,
            default: [],
        }
    },
    data() {
        return {
            data1List: [],  //级联一级下拉列表的全数据
            data1Num: 0,
            data2List: [],  //级联二级下拉列表的全数据
            data2Num: 0,
            inputData1: "",  //级联一级的检索关键字
            inputData2: "",  //级联二级的检索关键字
            selectedData1: {    //已选择的一级级联选项
                code: "",
                name: "",
            },
            tempData1AndCenter: [],  //已选级联关系存储在这里
            isLoadingData2: false,  //级联二级数据加载loading,二级需要循环递归调用接口,在获取到最后一页数据后加载结束
            breakLoop: false,   //是否跳出级联二级的接口递归循环
        };
    },
​
    methods: {
        //弹窗打开时初始化数据,如有数据则添加已选项的回显
        initData() {
            this.data1List.length = 0;
            this.tempData1AndCenter = [];
            this.tempData1AndCenter = this.initData;
            this.queryData1Data();  //获取级联一级下拉列表的所有数据
        },
        //查询所有数据1
        async queryData1Data() {
                const res = await axios.get(
                    "",
                    {
                        params: {
                        },
                    }
                );
                if (res.data.errcode == 0) {
                    this.data1List = res.data.data.reslist
​
                    this.data1Num = parseInt(res.data.data.total);
                } else if (res.data.errcode == 5) {
                    window.location.href =
                        "" +
                        encodeURIComponent(window.location.href);
                }
        },
        //查询所有数据2,入参currentPage为1的话就是从第一页开始递归获取所有数据,ouCode为查询接口所需参数,keyWord为模糊检索关键字
        async queryData2Data(currentPage, ouCode, keyWord) {
            if (
                this.data2List.length < this.data2Num ||
                this.data2Num == 0
            ) {
                this.isLoadingData2 = true;
                const res = await axios.get(
                    "",
                    {
                        params: {
                        },
                    }
                );
                if (res.data.errcode == 0) {
                    if(parseInt(res.data.data.total)==0){
                        this.$message.info('未查询到相关数据2')
                    }
                    this.data2List = this.data2List.concat(
                        res.data.data.reslist
                    );
                    this.data2Num = parseInt(res.data.data.total);
                    if(this.breakLoop){
                        return
                    }
                    if (this.data2List.length < this.data2Num) {  //判断当前的列表数据是否是完整的,是否需要继续调接口获取数据,在这里循环递归
                        await this.queryData2Data(currentPage + 1, ouCode, keyWord);
                    } else {
                        this.isLoadingData2 = false;
                    }
                } else if (res.data.errcode == 561844239) {
                    window.location.href =
                        "" +
                        encodeURIComponent(window.location.href);
                }
                else{
                    this.$message.error(res.data.errmsg);
                    this.isLoadingData2 = false;
                }
            }
        },
        //点击选择数据1,点击级联的一级选项去调接口获取对应二级的数据
        selectData1(item) {
            this.selectedData1.code = item.code;
            this.selectedData1.name = item.name;
            this.data2List.length = 0;
            this.data2Num = 0;
            this.queryData2Data(1, item.ouCode, "");
        },
        //勾选添加数据1与数据2的对应关系,选中级联二级数据后添加映射关系的逻辑
        addData(item) {
            if (
                this.tempData1AndCenter
                    .map((item) => item.data2er_code)
                    .indexOf(item.aCode) > -1
            ) {
                this.tempData1AndCenter.splice(
                    this.tempData1AndCenter
                        .map((item) => item.data2er_code)
                        .indexOf(item.aCode),
                    1
                );
            } else {
                let data1AndData2 = {  //添加店铺接口所需要的数据结构拼接
                    code: this.selectedData1.b_code,
                    name: this.selectedData1.b_name,
                    data2er_id: item.aId,
                    data2er_code: item.aCode,
                    data2er_name: item.aName,
                };
                this.tempData1AndCenter.push(data1AndData2);
            }
        },
        //提交数据1和数据2表单,将选择完毕的数据传回给父组件
        submitDataForm() {
            this.$emit('data1-data2-data', this.tempData1AndCenter);
            this.$emit('updata:dialogShow', false);
            this.$message({
                showClose: true,
                message: "添加数据1和数据2成功!",
                type: "success",
            });
        },
​
        //删除已添加的
        deleteData(item) {
            this.tempData1AndCenter.splice(
                this.tempData1AndCenter
                    .map((item) => item.data2er_code)
                    .indexOf(item.mdata2er_code),
                1
            );
        },
        //点击按钮查询数据2
        searchData2() {
            if(this.selectedData1.code != ""){
                this.breakLoop = true;  //打断一级级联面板上选择的循环递归中的接口调用,跳出循环
                this.data2List.length = 0;
                this.data2Num = 0;
                var that = this;
                setTimeout(function() { //使用计时器保证递归中的循环可成功打断
                    that.data2List.length = 0;
                    that.data2Num = 0;
                    that.breakLoop = false; //跳出循环后通过关键字模糊查询接口,该功能主要应用于数据2数据量特别庞大加载时间长的场景
                    that.queryData2Data(1, that.selectedData1.code,that.inputData2);
                }, 2000);
            }
            else{
                this.$message.info("请先选择数据1");
            }
        },
        //点击按钮查询数据1,这里接口不支持关键字模糊查询,所以在前端对所有一级列表数据进行匹配获取,实现模糊查询
        async searchData1() {
            this.data1List.length = 0;
            if (this.inputData1 == "") {    //输入框为空的话则查询所有数据
                this.queryData1Data();
            } else {
                await this.queryData1Data();    //输入框不为空先获取所有数据,再对获取到的所有数据进行处理
                var that = this;
                let searchResult = [];  //模糊查询匹配到的结果项数组
                this.data1List.forEach(function (item) {   //遍历接口获取到的所有数据 
                    if (item.name.includes(that.inputData1)) {
                        searchResult.push(item) //如果在所有项数据的遍历中匹配到包含模糊查询关键字的话,将匹配到的项插入结果数组
                    }
                });
                this.data1List = searchResult;
            }
        },
        handleClose() {
          this.$emit('updata:dialogShow', false);
        }
    },
};
</script></script>