使用elementUi的tabs组件 + vuex 实现点击菜单或者点击按钮新增一个tab标签页
一、思路
由于之前一直做的单页面项目都是使用路由控制实现的路由改变销毁原有组件加载新组件的方式,是view区域始终只有一个路由的方式实现单页面的管理系统,后面接到一个新的项目后发现原来的逻辑对于新需求不太适应,首先新的需求要求,打开的页面或者类页面的tab标签页数量上可能是无限的,这样对于使用路由逻辑上就不太好实现,比如我现在需要打开N个订单详情,路由控制的方式就比较麻烦了,虽然可以用keep-alive 标签来控制和保存参数,但是各个页面之间的交互以及菜单与tab方面的切换,还有国际化做起来就比较麻烦。后面就想到使用elementUi的tabs组件和vuex来做,因为tabs组件效果上看上去和浏览器的新标签打开页面效果比较相似,这样可以减少很多交互效果的css和js书写,不过这个组件也有很多坑(具体后面再说)
1.产品想要的效果
首先点击一个菜单项生成一个tab页面,其他的页面或者组件也能通过点击生成tabs 相同的组件但是除菜单的不可以重复外其他入口的是可以无限打开的,比如订单详情
点击x的时候关闭当前页面,点击刷新标识的时候刷新当前页面的数据。
2 菜单效果
菜单实现分为三级菜单、一、二级 为展开式菜单,三级菜单为弹出式菜单(这个菜单踩了不少坑)因为这个项目是一开始就确定使用elementUi来做的,这次是属于重构所以优先考虑的是使用elementUi自带的组件来实现这个效果,但是这个效果显然 没有现成的 elementUi的API上只有全展开或者全弹出式的菜单没有这种混合的,后面是做其他业务模块的时候使用到了Popover弹出 组件 于是来了灵感使用 于是使用 NavMenu + Popover 的方式实现了这种混合菜单。
上干货:
el-menu
:unique-opened="true"
class="el-menu_nav"
collapse-transition
:collapse="isCollapse"
:default-active="activeTabName"
@select="addTab">
:index="item.index"
:key="index"
v-if="!item.children"
v-for="(item,index) in navList"
>
{{item.title}}
background-color="#fff"
:index="item.index"
:key="index"
v-if="item.children"
v-for="(item,index) in navList"
>
{{item.title}}
v-if="!it.children||it.children.length==0"
:index="it.index"
:key="indexChild"
v-for="(it,indexChild) in item.children">
{{it.title}}
class="sencond_menu_children"
v-if="it.children && it.children.length>=1"
:index="it.index"
:key="indexChild"
v-for="(it,indexChild) in item.children">
placement="right-start"
title=""
width=""
trigger="hover"
v-if="!isCollapse"
v-model="it.visible"
>
@click="addTab(it1.index),it.visible = false"
:index="it1.index"
:key="indexChild1"
v-for="(it1,indexChild1) in it.children">
{{it1.title}}
{{it.title}}
{{it.title}}
v-if="isCollapse"
:index="it1.index"
:key="indexChild1"
v-for="(it1,indexChild1) in it.children">
{{it1.title}}
通过v-if 选择三级级菜单的显示方式 丢弃原组件自带的三级菜单给二级菜单绑定新的hover事件显示弹出三级菜单 这里要注意一个点就是三级菜单的切换时 el-menu 这个组件自带的切换效果是上下式的 我们是不希望这样的 所以要重新定义这个样式 使用 下面的写法禁用掉二级菜单的动画
// 重置二级副菜单的样式
.el-submenu.is-opened.sencond_menu_children > .el-submenu__title .el-submenu__icon-arrow{
transform:none !important;
}
下面是store的写法
state : {
// 管理tabs标签
activeTabName: "home",
tabList: [
{
label: '主页',
name: 'home',
param:{},
disabled: false,
closable: false,
component: appDashboard
}
],
navBaseInfo:navBaseInfo,
searchList:searchList,//这个是用来实现页面多个enter事件的定位的配置表
},
//实现组件懒加载
const componentsOne = resolve => require(['@/components/one'], resolve)
//这个对象只是用来新增tab是取组件方便的
let components = {
componentsOne:componentsOne
}
/**
* 新增菜单类型的tab
*@param state {Object} 当前的状态对象
*@param name {String} 必传信息 当前需要打开的tab的关键字
*/
addTab(state, index) {
let isRefresh = false
JSON.parse(sessionStorage.getItem('osMenuArr')).filter(f => {
if (f.menu_index == index) {
isRefresh = f.is_refresh == 1? true : false
}
})
//第一个版本
//let const componentsOne = resolve => require(['@/components/one'], resolve)
if (state.tabList.filter(f => f.name == index) == 0) {
state.tabList.push({
label: '',
name: index,
param:{},
disabled: false,
refresh:isRefresh || false,
closable: true,
//component: componentsOne, 这是第一个版本的写法 这样写前面的components 对象和 组件懒加载可以不用定义,但是这个方法有一个巨坑 就是最后打包上线的时候文件会变的巨大,所以最后优化的时候舍弃了
component: components[index]
})
}
state.activeTabName = index
},
/**
* 新增非菜单类型的tab
*@param tab {Object} 当前需要新打开的tab的标签信息
*@param tab.title {String} 必传信息 当前需要打开的tab的标题
*@param tab.index {String} 必传信息 当前需要打开的tab的关键字
*@param tab.param {Object} 必传信息 当前tab页面需要的参数
*@param tab.beforeCloseam {Function} 关闭前的函数
*@param tab.afterClose {Function} 关闭窗口后的函数
*/
addNewNotMenuTab(state, tab) {
let title = tab.title || "New Page",
index = tab.index,
random = parseInt(new Date().getTime())
if(!tab.index){
alert("Jump param is error, Please check !")
return false
}
let name = tab.isNoRandom?index : index + random
if (state.tabList.filter(f => f.name == name) == 0) {
state.tabList.push({
label: title,
not_menu:true,
name:name,
disabled: false,
closable: true,
refresh:tab.refresh || false,
param:tab.param||{},
component: components[index],
beforeClose:tab.beforeClose,
afterClose:tab.afterClose,
})
}
state.activeTabName = tab.isNoRandom?index : index + random
},
async closeTab(state, name,callback) {
if(typeof name == "function"){
console.log("close param is Erorr")
return false
}
let tab = state.tabList.filter(f => f.name == name)[0]
if(!tab){
return false
}
let index = state.tabList.indexOf(tab)
if(state.tabList[index].beforeClose){
try{
await new Promise((resolve, reject) => state.tabList[index].beforeClose(resolve, reject))
} catch (e) {
return false
}
}
if (index != state.tabList.length - 1) {
state.activeTabName = state.tabList[index + 1].name
} else {
state.activeTabName = state.tabList[index - 1].name
}
state.tabList[index].afterClose && state.tabList[index].afterClose()
state.tabList = state.tabList.filter(f => f.name != name)
callback && callback()
},
tabs页面结构
{{item.not_menu?item.label:$t('message.menu.'+item.name)}}
定义完这几个方法基本上需要实现的就差不多了可以了
最后为了方便就是把当前的这些方法挂载到全局上,毕竟都要用到的 不用每个页面都去引用一次vuex 那样很麻烦
import { mapMutations } from 'vuex'
export default {
install(Vue, options) {
//特殊发送请求
Vue.prototype.$restful = restful;
// 公用发送请求
Vue.prototype.$sendReq = sendReq;
// 关闭tab
Vue.prototype.$closeTab = mapMutations('navTabs', ['closeTab']).closeTab
// 新增tab
Vue.prototype.$addTab = mapMutations('navTabs', ['addTab']).addTab
// 新增一个非菜单tab
Vue.prototype.$addNewNotMenuTab = mapMutations('navTabs', ['addNewNotMenuTab']).addNewNotMenuTab
}
}
最后有一个小坑就是 回车事件 enter这个坑了 由于以前都是采取组件销毁的方式实现的 所以在组件里面单独实现是没问题的但是现在因为要实现很多页面都需要实现回车 这些页面又是同时存在的 所以就会出现 谁先出现谁生效,或者谁后出现谁生效的情况
解决思路
在tab切换的时候定位到tab的index关键字段 把这个字段给ref属性 即 activeName == index ==ref
所以可以使用下面的方法来实现,这个方法一定要等页面元素加载完再绑定要不会报错
document.onkeydown = function(e) {
//捕捉回车事件
let ev = (typeof event!= 'undefined') ? window.event : e;
if(ev.keyCode == 13) {
if(self.$store.state.navTabs.searchList.indexOf(self.activeTabName)>-1)self.$refs[self.activeTabName][0].search && self.$refs[self.activeTabName][0].search()
}
}
不过elementUi的 tabs组件有一个最大的坑就是不能做拖拽排序和拖拽替换,这个是一个坑 到目前没找到解决方法 如有知道的小伙伴可以@一下 后面有时间再分享一个国际化的实现吧
以上就是本次文章全部内容