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>