使用VUE + ElementUI 中的 tabs组件,造成重复请求接口的情况
网上已经有很多文章了,这里不再长篇叙述,只是大概总结要点。
想了解详细内容,可以参考其他文章:
1、VUE +ElementUI,tabs组件小坑——单页面被重复构建
2、vue + element-ui 制作tab切换(切换vue组件,踩坑总结)
问题描述:
做项目的时候,很多时候,我们的页面构建都是根据后端接口返回的数据来动态生成的页面元素,比如tabs页签数据、按钮组的数据、表头数据、表格body数据很多时候都是动态生成的。
但是页签数据在使用 elementui 中的 el-tabs 的时候,动态生成的页签元素,在页面加载的时候会造成重复请求接口的情况,这肯定不是我们想要的效果。
解决方案:
方案一、不动态生成页签数据,直接写死(有几个页签,就写几个 el-tab-pane )
这个方案适合的场景:比如我们有一个新增页面的弹框,新增页面有几个页签,每个页签下面都有内容,有的页签下面是表单,有的页签下面是表格。右上角有一个“保存”的按钮,我们填写完新增的数据之后,点击“保存”按钮要把几个页签下面的内容统一保存,这时候,如果涉及到有验证的规则(验证不通过不让保存),页签数据写死就可以实现效果。
如果页签是动态生成的(用 v-for 循环生成 el-tab-pane 元素),那么我们页签下面对应的内容是用 v-if 判断生成的。如果页签1下面是表单,页签3下面是表格,当我们处于页签3下面,点击保存按钮,此时如果页签1下面的表单有必填项漏掉的话,我们想获取页签1下面表单的数据是获取不到的。因为使用的是 v-if ,所以页签1下面的表单内容根本就没有渲染,所以我们获取不到。但是如果使用 v-show 来判断生成页签对应的内容,就会造成重复请求接口。所以不建议动态生成页签数据。
方案二、通过 true 和 false 控制页签下面的内容是否渲染
此方法适用页签下面的内容是组件的时候。之前做的项目碰到过这种写法,是我自己摸索出来的,这里不过多赘述。具体参考文章 vue + element-ui 制作tab切换(切换vue组件,踩坑总结)里面的内容。我觉得这个方法有个缺点就是如果有多个页签(例如有8-10个页签)的话,就要依次判断,写的代码比较多,而且看上去比较low。
参考的文章截图:
方案三:舍弃 elementui 的标签组件,自己封装一个。
自己封装的 tab 页签组件适用于展示每个页签下面的内容,彼此不关联,因为每个页签下面的内容也是通过 v-if 判断加载的。如果碰到新增或者编辑页面,通过保存按钮统一保存页面内容与接口交互的话,自己封装的组件就不适用了, 这个时候就要采取方案一了。
自己封装的页签组件代码:
<template>
<div class="tabs-wrap">
<!-- 主页签 -->
<ul class="tabs-ul" ref="tabsUL">
<li
v-for="(item,index) in tabTitle"
:key="index"
@click="tabClick(index,item)"
class="item"
:class="{'active':item.isDefault}"
>
{{item.name}}
<span class="mark" v-if="maskArr.length>0&&item.isDefault">{{maskArr[index]}}</span>
</li>
</ul>
<!-- 附属页签 -->
<ul class="sub-ul">
<li
v-for="(item,index) in subTitle"
:key="index"
@click="subClick(index,item)"
:class="{'active':item.isDefault}"
>{{item.name}}</li>
</ul>
<div class="line" :style="{'left':(offsetLeft)+'px','width':lineWidth+'px'}"></div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
return {
tabTitle: [],
subTitle: [],
hideBtn: [],
btnGroup: [], //要显示的按钮
tabsUL: "",
offsetLeft: 0,
lineWidth: 0
};
},
props: {
//过滤前的所有按钮
allBtn: {
type: Array,
default: () => {
return [];
}
},
//条数数组
maskArr: {
type: Array,
default: () => {
return [];
}
},
//tab菜单的标题信息
tabsInfo: {
type: Array,
default: () => {
return [];
}
}
},
watch: {
tabsInfo: {
handler(v) {
this.tabTitle = [];
this.subTitle = [];
this.setTabsInfo();
this.setActive();
}
}
},
mounted() {
this.setTabsInfo();
this.setActive();
},
methods: {
//处理初始化数据 (筛选出主页签和附属页签)
setTabsInfo() {
if (this.tabsInfo.length > 0) {
this.tabsInfo.forEach(item => {
if (item.isvisiable == 1) {
//临时添加 start
if (typeof item.config == "string") {
item.config = JSON.parse(item.config);
}
//临时添加 end
if (item.config && item.config.type == "subTab") {
this.subTitle.push(item);
} else {
this.tabTitle.push(item);
}
}
});
this.setShowBtn();
}
},
//设置要显示的按钮
setShowBtn() {
if (this.tabTitle.length > 0) {
//设置默认选中的页签项(若没有,则设置第一项为选中状态)
let index = this.tabTitle.findIndex(item => {
return item.isDefault === true;
});
if (index < 0) {
this.tabTitle[0].isDefault = true;
index = 0;
}
// 后端返回的页签数据中 btnhide ,设置按钮组的显示
this.hideBtn = [];
this.hideBtn =
this.tabTitle[index].btnhide &&
this.tabTitle[index].btnhide.length > 0
? this.tabTitle[index].btnhide.split(",")
: [];
this.btnGroup = [];
if (this.allBtn.length > 0) {
this.allBtn.map(val => {
if (!this.hideBtn.includes(val.code)) {
this.btnGroup.push(val);
}
});
this.tabTitle[index].btnGroup = this.btnGroup;
let obj = {
btnGroup: this.btnGroup,
wheresql: this.tabTitle[index].wheresql,
code: this.tabTitle[index].code
};
this.$emit("getShowBtn", obj);
}
}
},
// 获取 选中页签 的 offsetLeft 和 offsetWidth 值
setActive() {
let index = this.tabTitle.findIndex(value => value.isDefault == true);
this.tabsUL = this.$refs.tabsUL;
this.$nextTick(() => {
if (index >= 0 && this.tabsUL) {
let lis = this.tabsUL.getElementsByClassName("item");
this.offsetLeft = lis[index].offsetLeft;
this.lineWidth = lis[index].offsetWidth;
}
});
},
//主页签的点击事件
tabClick(index, item) {
this.tabTitle.forEach(ite => {
ite.isDefault = false;
});
this.tabTitle[index].isDefault = true;
this.setActive();
if (item.btnhide) {
this.hideBtn = item.btnhide.split(",");
this.setShowBtn();
} else {
this.btnGroup = this.allBtn;
}
item.btnGroup = this.btnGroup;
this.$emit("tabClick", item);
},
//附属页签的点击事件
subClick(idx, item) {
this.subTitle.forEach(ite => {
ite.isDefault = false;
});
this.subTitle[idx].isDefault = true;
this.$set(this.subTitle, idx, this.subTitle[idx]);
this.$emit("subClick", item);
}
}
};
</script>
<style scoped lang='scss'>
.tabs-wrap {
margin-bottom: 20px;
border-bottom: 1px solid #ccf;
// height: 40px;
line-height: 40px;
position: relative;
display: flex;
flex-direction: row;
.sub-ul {
margin-left: 20px;
li {
cursor: pointer;
float: left;
margin-right: 15px;
padding: 0 10px;
position: relative;
&::after {
content: "";
position: absolute;
width: 0px;
height: 0px;
border: 5px solid transparent;
border-bottom-color: #409EFF;
bottom: 0;
left: 50%;
margin-left: -5px;
opacity: 0;
}
&.active::after {
opacity: 1;
}
&.active {
color: #409EFF;
}
}
}
.tabs-ul {
overflow: hidden;
li {
cursor: pointer;
position: relative;
padding: 0 0.2rem;
&.active {
color: #409EFF;
}
.mark {
position: absolute;
width: 20px;
height: 20px;
line-height: 20px;
background-color: #f66;
border-radius: 100%;
text-align: center;
font-size: 12px;
color: #fff;
margin: 0;
padding: 0;
right: -9px;
top: 10px;
}
}
}
.line {
position: absolute;
left: 0;
bottom: 0px;
height: 2px;
width: 40px;
background-color: #409EFF;
transition: left 0.3s;
}
.item {
float: left;
margin-right: 0.5rem;
}
}
</style>
这个组件里面的代码,有带附属页签的,这个是目前我做的项目碰到的需求,所谓的主页签和附属页签的效果如图:
如果项目中没有附属页签,只有主页签的话,这个组件依然适用,只要不传附属页签的数据即可。
----------------- 完结 -----------------