放在前面
最近在搭一套 Vue2 + 周边生态的管理系统。菜单、面包屑和 Tabs 联动,用 Vuex 简单设计了一下,感觉挺清爽。
附上我画的思维导图:
一眼看上去觉得有点乱,不要紧,咱们一步步分析这个设计。
Layout
Layout 主要有几个部分组成(Collapse-收缩组件,Breadcrumb-面包屑,Aside-侧边菜单栏,Tabs-打开的页签,Main-路由对应显示的页面),我们就从每个部分开始分析。
<template>
<div class="layout">
<el-container class="el-container">
<el-aside>
<!-- 侧边菜单栏 -->
<Aside></Aside>
</el-aside>
<el-container class="el-container">
<el-header style="height: auto">
<!-- 头部 -->
<Header></Header>
<!-- 打开的页签集合 -->
<Tabs></Tabs>
</el-header>
<el-main>
<section class="main-box">
<!-- 路由对应显示的页面 -->
<router-view v-slot="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<keep-alive>
<component :is="Component" :key="route.path"></component>
</keep-alive>
</transition>
</router-view>
</section>
</el-main>
<el-footer>
<Footer></Footer>
</el-footer>
</el-container>
</el-container>
</div>
</template>
Store Module
menus 模块
存放“菜单收缩”状态和“菜单集合”状态。
export const menus = {
state: {
// 左侧导航 - 菜单收缩状态
isCollapse: false,
// 左侧导航 - 菜单集合
menuList: [],
},
mutations: {
// 设置收缩状态
setCollapse(state) {
state.isCollapse = !state.isCollapse;
},
// 设置左侧导航菜单集合
setMenuList(state, menuList) {
state.menuList = menuList
}
},
actions: {},
}
tabs 模块
存放“当前选中 tab 值”的状态和“当前系统所有打开 tab 的集合”状态。
import { HOME_URL, TABS_BLACK_LIST } from '@/config/config'
import router from '@/router/index';
export const tabs = {
state: {
// 默认选中 - 首页
tabsMenuValue: HOME_URL,
// 默认打开 tab 的集合 - 首页
tabsMenuList: [{ title: "首页", path: HOME_URL, icon: "el-icon-s-home", close: false }]
},
mutations: {
// 设置选中的 tab
setTabsMenuValue (state, val) {
state.tabsMenuValue = val
},
// 添加一个 tab
addTabsMenuList (state, val) {
state.tabsMenuList.push(val)
},
// 设置 tabs 集合
setTabsMenuList (state, val) {
state.tabsMenuList = val
},
},
actions: {
// 添加一个 tab
addTabs ({ commit, state }, tabItem) {
// 不添加 tabs 黑名单
if (TABS_BLACK_LIST.includes(tabItem.path)) return
const tabInfo = {
title: tabItem.title,
path: tabItem.path,
close: tabItem.close,
};
// 如果打开的 tabs list 中没有这个 item,则添加到 list 中。(用 path 判断)
if (state.tabsMenuList.every(item => item.path !== tabInfo.path)) {
commit('addTabsMenuList', tabInfo)
}
// 设置新添加的菜单项为选中的 tab
commit('setTabsMenuValue', tabInfo.path)
// 路由跳转到新添加的菜单路由
router.push(tabInfo.path)
},
// 移除一个 tab
removeTabs ({ commit, state }, tabPath) {
let tabsMenuValue = state.tabsMenuValue
const tabsMenuList = state.tabsMenuList
// 关闭当前选中的 tab;tab 选中的值变更为下一个list当前的 tab,或上一个。
if (tabPath === tabsMenuValue) {
tabsMenuList.forEach((item, index) => {
if (item.path !== tabPath) return
const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1]
if (!nextTab) return
tabsMenuValue = nextTab.path
router.push(nextTab.path)
})
}
// 将处理后的值再次赋值
commit('setTabsMenuValue', tabsMenuValue)
commit('setTabsMenuList', tabsMenuList.filter(item => item.path !== tabPath))
},
// 移除多个 tab
closeMultipleTabs ({ commit, state }, tabPath) {
commit('setTabsMenuList', state.tabsMenuList.filter(item => {
return item.path === tabPath || item.path === HOME_URL
}))
},
// 跳转首页
goHome ({ commit }) {
router.push(HOME_URL)
commit('setTabsMenuValue', HOME_URL)
},
}
}
Header
Collapse、Breadcrumb 和 Avatar 组件在 Header 组件中。
Collapse
收缩组件
- 收缩状态(state):
isCollapse - 点击更改(mutations):
setCollapse()
<template>
<div>
<i class="icon" :class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'" @click="handleSetCollapse"></i>
</div>
</template>
computed: {
isCollapse () {
// 获取 Vuex 菜单模块中的 isCollapse 状态
return this.$store.state.menus.isCollapse
}
},
methods: {
// 切换时,更改 Vuex 菜单模块中的 isCollapse 状态
handleSetCollapse() {
this.$store.commit('setCollapse')
},
},
Breadcrumb
面包屑组件
- 第一项固定为首页。
- 后面的用:
$route.matched
<template>
<div>
<el-breadcrumb separator="/">
<transition-group name="breadcrumb" mode="out-in">
<el-breadcrumb-item :to="{ path: HOME_URL }" key="/home">首页</el-breadcrumb-item>
<el-breadcrumb-item v-for="item in matched" :key="item.path" :to="{ path: item.path }">
{{ item.meta.title }}
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</div>
</template>
computed: {
matched () {
// $route.matched 类型:Array 一个数组,包含当前路由的所有嵌套路径片段的路由记录 。路由记录就是 routes 配置数组中的对象副本 (还有在 children 数组)。
return this.$route.matched.filter(item => item.meta && item.meta.title)
}
},
Aside
侧边导航菜单组件
- 收缩状态(state):
isCollapse - 默认选中菜单($route.path):
activeMenu - 设置菜单集合(mutations):
setMenuList - 所有菜单集合(state):
menuList - 监听浏览器变化设置收缩状态:
listeningWindow
<el-menu
:default-active="activeMenu"
class="el-menu-vertical-demo"
:collapse="isCollapse"
:router="true"
:collapse-transition="false"
:unique-opened="true"
background-color="#191a20"
text-color="#bdbdc0"
active-text-color="#fff"
>
<template v-for="(subItem, index) in menuList">
<el-submenu :key="subItem.path" v-if="subItem.children && subItem.children.length > 0" :index="subItem.path">
<template #title>
<i :class="subItem.meta.icon"></i>
<span>{{ subItem.meta.title }}</span>
</template>
<SubItem :menuList="subItem.children"></SubItem>
</el-submenu>
<el-menu-item :key="index" v-else :index="subItem.path">
<i :class="subItem.meta.icon"></i>
<span>{{ subItem.meta.title }}</span>
</el-menu-item>
</template>
</el-menu>
computed: {
// 菜单收缩状态
isCollapse () {
return this.$store.state.menus.isCollapse
},
// 打开菜单的菜单为:当前路由的值
activeMenu () {
return this.$route.path
},
// 所有的系统菜单
menuList () {
return this.$store.state.menus.menuList
},
},
mounted () {
const vm = this
// 监听屏幕变化,设置收缩状态。
window.onresize = () => {
vm.listeningWindow()
}
// 设置左侧导航菜单集合
vm.handleSetMenuList()
},
methods: {
listeningWindow() {
const screenWidth = document.body.clientWidth;
if (this.isCollapse === false && screenWidth < 1200) this.$store.commit('setCollapse');
if (this.isCollapse === true && screenWidth > 1200) this.$store.commit('setCollapse');
},
handleSetMenuList() {
// 如果是动态获取的路由,需要调接口后设置。this.$store.commit('setMenuList', res.data);
// 目前本项目是静态的路由
this.$store.commit('setMenuList', asyncRouter);
},
},
Tabs
系统打开的页签组件
- tab选中状态(state):
tabsMenuValue - 所有打开tab集合(state):
tabsMenuList - watch
$route添加tab(actions):addTabs- 没有打开过,添加一个tab(mutation):
addTabsMenuList(tabInfo) - 设置当前选中的tab状态(mutation):
setTabsMenuValue(tabInfo.path)
- 没有打开过,添加一个tab(mutation):
- 点击某个tab:
tabClick(val)->this.$router.push({ path: val.name })-> 到3 - 移除某个tab(actions):
removeTabs- 关闭的tab就是打开的这个tab。需要路由跳转到tabs集合中下一个tab或上一个tab对应的路由。
- 设置关闭后的选中的 tab 的值(mutations): setTabsMenuValue。
- 设置筛选后的 tab 集合的值(mutations):setTabsMenuList。
<div class="tabs-menu">
<el-tabs
v-model="tabsMenuValue"
type="card"
@tab-click="tabClick"
@tab-remove="removeTabs"
>
<el-tab-pane
v-for="item in tabsMenuList"
:key="item.path"
:path="item.path"
:label="item.title"
:name="item.path"
:closable="item.close"
>
<template #label>
<i v-if="item.icon" :class="item.icon"></i>
{{ item.title }}
</template>
</el-tab-pane>
</el-tabs>
<MoreButton></MoreButton>
</div>
watch: {
// 监听路由的变化,防止后退前进不变化 tabsMenuValue
'$route': {
handler: function (value) {
let params = {
title: value.meta.title,
path: value.path,
close: true
};
this.$store.dispatch('addTabs', params);
},
immediate: true,
},
},
computed: {
tabsMenuValue: {
get: function() {
return this.$store.state.tabs.tabsMenuValue
},
set: function(value) {
this.$store.commit('setTabsMenuValue', value)
},
},
tabsMenuList() {
return this.$store.state.tabs.tabsMenuList
},
},
methods: {
tabClick (val) {
this.$router.push({
path: val.name
})
},
removeTabs (val) {
this.$store.dispatch('removeTabs', val)
},
},
Main
打开页面的视图展示组件
<el-main>
<section class="main-box">
<router-view v-slot="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<keep-alive>
<component :is="Component" :key="route.path"></component>
</keep-alive>
</transition>
</router-view>
</section>
</el-main>
写在最后
现在再结合前面的流程图,你会觉得清晰了一点吧。
这个项目我放在了自己的 github 上有兴趣的可以去看看吧。