NavBar (導(dǎo)航欄)
創(chuàng)建 src/layout/components/NavBar.vue
<template>
<div class="navbar">
<!-- sidebar抽屜按鈕 -->
<div class="sidebar-switch" @click="switchSidebar">
<i :class="open ? 'el-icon-s-fold':'el-icon-s-unfold'" />
</div>
<!-- breadcrumb -->
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首頁</el-breadcrumb-item>
<el-breadcrumb-item><a href="/">活動管理</a></el-breadcrumb-item>
<el-breadcrumb-item>活動列表</el-breadcrumb-item>
<el-breadcrumb-item>活動詳情</el-breadcrumb-item>
</el-breadcrumb>
<!-- nav menu -->
<el-dropdown class="nav-menu">
<div class="avatar-wrapper">
<img :src="'https://upload.jianshu.io/users/upload_avatars/20351000/e6ae7017-e428-4e0d-819b-59c1ae535835.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240'">
<span>康言先森</span>
<i class="el-icon-caret-bottom" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="a">個人中心</el-dropdown-item>
<el-dropdown-item command="e" divided>退出登錄</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script>
export default {
name: 'Navbar',
props: {
open: {
type: Boolean,
default: true
}
},
methods: {
switchSidebar() {
this.$emit('switchSidebar')
}
}
}
</script>
<style lang="scss">
.navbar {
height: 50px;
-webkit-box-shadow: 0 1px 4px rgba(0,21,41,0.08);
box-shadow: 0 1px 4px rgba(0,21,41,0.08);
.sidebar-switch {
height: 100%;
width: 50px;
padding: 0 15px;
line-height: 50px;
float: left;
cursor: pointer;
i {
font-size: 22px;
line-height: 50px;
}
}
.el-breadcrumb {
float: left;
height: 100%;
line-height: 50px;
}
.nav-menu {
float: right;
cursor: pointer;
height: 50px;
line-height: 50px;
.avatar-wrapper {
display: flex;
align-items: center;
}
img {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 5px;
}
}
}
</style>
這里優(yōu)化一下。/layout/components 文件夾的管理
創(chuàng)建src/layout/components/index.js
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
// 引用方式
//import { Navbar, Sidebar } from './components/'
// './components/' 它會優(yōu)先在 components 文件夾下尋找index.js文件
// 其次會是 index.vue 文件。這樣做是方便維護
// 不再是
//import Navbar from './components/Navbar .vue'
//import Sidebar from './components/Sidebar.vue'
src/layout/index.vue。改造后 引入 Navbar組件
<template>
<!-- 整體頁面布局 -->
<el-row class="app-wrapper">
<el-container>
<!-- 側(cè)邊欄 -->
<el-aside :width="open ? '210px' : '0px'">
<sidebar :open="open" />
</el-aside>
<el-container>
<!-- 頂部 -->
<el-header height="50px">
<!-- 頭部信息 -->
<navbar @switchSidebar="switchSidebar" />
</el-header>
<!-- 主頁面 -->
<el-main>主頁面</el-main>
</el-container>
</el-container>
</el-row>
</template>
<script>
import { Navbar, Sidebar } from './components/'
export default {
name: 'Layout',
components: {
Sidebar,
Navbar
},
data() {
return {
open: true
}
},
methods: {
switchSidebar() {
this.open = !this.open
}
}
}
</script>
...
樣式
</style>

接下來是數(shù)據(jù)的動態(tài)化處理
改造src/router/index.js 加入測試的三個路由
加入meta屬性,用于傳遞參數(shù)
export const routes = [
{
path: '/',
component: Layout,
name: '主頁',
meta: {
title: '主頁'
}
// component: () => import('@/views/home/index')
},
{
path: '/dog',
component: Layout,
name: '狗子世界',
meta: {
title: '狗子世界'
},
// component: () => import('@/views/home/index')
children: [
{
path: '/erha',
name: '哈士奇',
meta: {
title: '哈士奇'
},
component: () => import('@/views/home/index')
},
{
path: '/jinmao',
name: '金毛',
meta: {
title: '金毛'
},
component: () => import('@/views/home/index')
},
{
path: '/taidi',
name: '泰迪',
meta: {
title: '泰迪'
},
component: () => import('@/views/home/index')
}
]
}
]
NavBar.vue改造
<!-- breadcrumb -->
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="item in levelList" :key="item.path">
{{ item.meta.title }}
</el-breadcrumb-item>
</el-breadcrumb>
//script
export default {
name: 'Navbar',
props: {
open: {
type: Boolean,
default: true
}
},
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
switchSidebar() {
this.$emit('switchSidebar')
},
getBreadcrumb() {
// 獲取路由對應(yīng)title && 存在返回右邊,不存在返回左邊
const matched = this.$route.matched.filter(item => item.meta && item.meta.title)
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
}
}

接下來是標(biāo)簽
路由標(biāo)簽
創(chuàng)建/layout/components/ScrollPane.vue 標(biāo)簽多是啟動滾動
ScrollPane.vue
<template>
<template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.prevent="handleScroll">
<slot />
</el-scrollbar>
</template>
<script>
export default {
name: 'ScrollPane',
data() {
return {
left: 0
}
},
computed: {
scrollWrapper() {
return this.$refs.scrollContainer.$refs.wrap
}
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
}
}
}
</script>
<style lang="scss" scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
/deep/ {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
}
}
</style>
創(chuàng)建 src/store/modules/tabs.js。 需要配置Vuex 管理標(biāo)簽的信息
tabs.js
const state = {
visitedViews: [], // 標(biāo)簽組
cachedViews: [] // 需要緩存的標(biāo)簽組,根據(jù)這個數(shù)組,確定是否緩存頁面,暫時沒用到
}
const mutations = {
ADD_VISITED_VIEW(state, view) {
// 如果標(biāo)簽跳轉(zhuǎn)的路由存在就不添加
// 名字不同,路徑相同的。也加入標(biāo)簽組
if (state.visitedViews.some(v => v.path === view.path)) return
state.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
ADD_CACHED_VIEW(state, view) {
// 已存在緩存就不緩存了
if (state.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
state.cachedViews.push(view.name)
}
}
}
const actions = {
addView({ commit }, view) {
// view == this.$router
commit('ADD_VISITED_VIEW', view)
commit('ADD_CACHED_VIEW', view)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
創(chuàng)建/layout/components/Tabs.vue 路由標(biāo)簽。用于關(guān)閉頁面,刷新頁面
Tabs.vue
<template>
<div class="tabs">
<scroll-pane ref="scrollPane" class="tabs-wrapper">
<router-link
v-for="tab in visitedViews"
ref="tag"
:key="tab.path"
class="tabs-item"
:class="isActive(tab)?'active':''"
:to="{ path: tab.path, query: tab.query, fullPath: tab.fullPath }"
tag="span"
>
{{ tab.title }}
<span class="el-icon-close" />
</router-link>
</scroll-pane>
</div>
</template>
<script>
import ScrollPane from './ScrollPane.vue'
export default {
name: 'Tabs',
components: { ScrollPane },
computed: {
visitedViews() {
return this.$store.state.tabs.visitedViews
}
},
watch: {
$route() {
this.addTab() // 路由一旦變化就會觸發(fā)
}
},
mounted() {
this.addTab()
},
methods: {
addTab() {
const { name } = this.$route
// 已存在的標(biāo)簽就不更新tabs狀態(tài)
// 就是點擊過的菜單,在點擊不觸發(fā)行為。
if (name) {
console.log('this.router:', this.$route)
this.$store.dispatch('tabs/addView', this.$route)
}
return false
},
isActive(route) {
return route.path === this.$route.path
}
}
}
</script>
<style lang="scss">
.tabs {
position: relative;
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
.tabs-wrapper {
.tabs-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 30px;
line-height: 30px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 2px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
}
}
}
</style>
由于加入了標(biāo)簽。所以頭部高度將增加32px,所以
layout/index.vue 樣式加入margin-top
.app-wrapper {
position: relative;
height: 100%;
width: 100%;
.el-aside{
transition: .5s;
}
.el-header {
padding: 0;
}
.el-main {
margin-top: 32px;
}
}
保存啟動項目看看效果


增已經(jīng)完成了,接下來搭建刪以及刷新的功能
刪:
src/router/index.js 加入保護屬性
{
path: '/',
component: Layout,
name: '主頁',
meta: {
title: '主頁',
affix: true // 加入保護
}
// component: () => import('@/views/home/index')
},
src/store/modules/tabs.js 加入刪除方法
// action
// 刪除標(biāo)簽
delView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_VISITED_VIEW', view)
commit('DEL_CACHED_VIEW', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
}
// mutations
DEL_VISITED_VIEW(state, view) {
for (const [i, v] of state.visitedViews.entries()) {
if (v.path === view.path) {
state.visitedViews.splice(i, 1)
}
}
},
DEL_CACHED_VIEW(state, view) {
const index = state.cachedViews.indexOf(view.name)
index > -1 && state.cachedViews.splice(index, 1)
}
// 重構(gòu)
<span class="el-icon-close" />
// ==>
<span v-if="!isAffix(tab)" class="el-icon-close" @click.prevent="closeTabs(tab)" />
// script 方法區(qū)加入 2個方法
methods: {
isAffix(tab) { // 是否受保護,這里保護首頁面不被刪除。去掉X的按鈕
// 在保護的路由 meta 增加 affix屬性
return tab.meta && tab.meta.affix
},
closeTabs(tab) {
this.$store.dispatch('tabs/delView', tab).then(({ visitedViews }) => {
// 如果刪除的是當(dāng)前頁面,則跳轉(zhuǎn)去下一個頁面。
if (this.isActive(tab)) {
if (visitedViews.length) {
// 跳轉(zhuǎn)到最后一個標(biāo)簽
const lastTab = visitedViews[visitedViews.length - 1]
this.$router.push(lastTab.fullPath)
} else {
// 如果沒有標(biāo)簽了,則跳去首頁
this.$router.push('/')
}
}
})
}
}

標(biāo)簽拓展功能
刷新、關(guān)閉、關(guān)閉其他、關(guān)閉全部
// 加入 contextmenu屬性。實現(xiàn)鼠標(biāo)右鍵功能
<div class="tabs">
<scroll-pane ref="scrollPane" class="tabs-wrapper">
<router-link
v-for="tab in visitedViews"
ref="tab"
:key="tab.path"
class="tabs-item"
:class="isActive(tab)?'active':''"
:to="{ path: tab.path, query: tab.query, fullPath: tab.fullPath }"
tag="span"
@contextmenu.prevent="openMenu(tab,$event)"
>
{{ tab.title }}
<span v-if="!isAffix(tab)" class="el-icon-close" @click.prevent="closeTabs(tab)" />
</router-link>
</scroll-pane>
<!-- 加入菜單-->
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="menu('refresh')">刷新</li>
<li v-if="!isAffix(selectedTab)" @click="menu('close')">關(guān)閉</li>
<li @click="menu('other')">關(guān)閉其他</li>
<li @click="menu('all')">全部關(guān)閉</li>
</ul>
</div>
方法區(qū)
...
data() {
return {
visible: false, // 菜單顯隱變量
top: 0, // 菜單偏移量x
left: 0, // 菜單偏移量x
selectedTab: {} // 鼠標(biāo)右擊的tab
}
},
...
methods: {
openMenu(tab, e) {
// 計算偏移量
// this.$el = Tabs.vue 這個Dom
// getBoundingClientRect().left 獲取tabs 距離窗口左邊距離。
// 由于left 根據(jù)父元素進行偏移。
// 所以 left = 鼠標(biāo)在窗口的x坐標(biāo) - 側(cè)邊欄寬度 15為菜單離鼠標(biāo)一段距離
this.left = e.clientX - this.$el.getBoundingClientRect().left + 15 // 15: margin right
// top 由于不用適配,所以采用 鼠標(biāo)在當(dāng)前元素的相對位置
this.top = e.offsetY
// 顯示菜單
this.visible = true
// 功能操作的tab。
this.selectedTab = tab
},
menu(select) {
switch (select) {
case 'refresh':
// 清除該頁面緩存,在跳轉(zhuǎn)該路由 達(dá)到刷新效果。
this.$store.dispatch('tabs/delCachedView', this.selectedTab).then(() => {
const { fullPath } = this.selectedTab
this.$nextTick(() => {
const { query } = this.$route
this.$router.replace({ path: fullPath, query })
})
})
break
case 'close':
this.closeTabs(this.selectedTab)
break
case 'other':
this.$store.dispatch('tabs/delOtherView', this.selectedTab).then(() => {
// 不是當(dāng)前激活,刪除其他后,跳轉(zhuǎn)到該頁面
if (!this.isActive(this.selectedTab)) this.$router.push(this.selectedTab.fullPath)
})
break
case 'all':
this.$store.dispatch('tabs/delAllView').then(() => {
this.$router.push('/')
})
break
}
// 隱藏菜單
this.visible = false
}
}
...
// 樣式scss
.contextmenu {
margin: 0;
background: #fff;
z-index: 99;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
src/store/modules/tabs.js 加入刷新、關(guān)閉等執(zhí)行方法
// actions
delCachedView({ commit }, view) {
return new Promise(resolve => {
commit('DEL_CACHED_VIEW', view)
resolve()
})
},
delOtherView({ commit }, view) {
return new Promise(resolve => {
commit('DEL_Other_VIEW', view)
resolve()
})
},
delAllView({ commit }) {
return new Promise(resolve => {
commit('DEL_ALL_VIEW')
resolve()
})
}
// mutations
DEL_CACHED_VIEW(state, view) {
const index = state.cachedViews.indexOf(view.name)
index > -1 && state.cachedViews.splice(index, 1)
},
DEL_Other_VIEW(state, view) {
// 重置頁面標(biāo)簽數(shù)組和緩存數(shù)組
state.visitedViews = [
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
]
state.cachedViews = [view.name]
},
DEL_ALL_VIEW(state) {
// 重置頁面標(biāo)簽數(shù)組和緩存數(shù)組
state.visitedViews = []
state.cachedViews = []
}

最后再加上左鍵取消菜單的方法
...
watch: {
$route() {
this.addTab() // 路由一旦變化就會觸發(fā)
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
...
methods: {
closeMenu() {
this.visible = false
}
}
到此導(dǎo)航欄 header 部分 配置完了。以上功能可以根據(jù)需求進行一些加工。