NavBar (导航栏)
创建 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>
这里优化一下。/layout/components 文件夹的管理
创建src/layout/components/index.js
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
// 引用方式
//import { Navbar, Sidebar } from './components/'
// './components/' 它会优先在 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>
<!-- 侧边栏 -->
<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>
接下来是数据的动态化处理
改造src/router/index.js 加入测试的三个路由
加入meta属性,用于传递参数
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() {
// 获取路由对应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)
}
}
}
接下来是标签
路由标签
创建/layout/components/ScrollPane.vue 标签多是启动滚动
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>
创建 src/store/modules/tabs.js。 需要配置Vuex 管理标签的信息
tabs.js
const state = {
visitedViews: [], // 标签组
cachedViews: [] // 需要缓存的标签组,根据这个数组,确定是否缓存页面,暂时没用到
}
const mutations = {
ADD_VISITED_VIEW(state, view) {
// 如果标签跳转的路由存在就不添加
// 名字不同,路径相同的。也加入标签组
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
}
创建/layout/components/Tabs.vue 路由标签。用于关闭页面,刷新页面
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() // 路由一旦变化就会触发
}
},
mounted() {
this.addTab()
},
methods: {
addTab() {
const { name } = this.$route
// 已存在的标签就不更新tabs状态
// 就是点击过的菜单,在点击不触发行为。
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>
由于加入了标签。所以头部高度将增加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;
}
}
保存启动项目看看效果
增已经完成了,接下来搭建删以及刷新的功能
删:
src/router/index.js 加入保护属性
{
path: '/',
component: Layout,
name: '主页',
meta: {
title: '主页',
affix: true // 加入保护
}
// component: () => import('@/views/home/index')
},
src/store/modules/tabs.js 加入删除方法
// action
// 删除标签
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)
}
// 重构
<span class="el-icon-close" />
// ==>
<span v-if="!isAffix(tab)" class="el-icon-close" @click.prevent="closeTabs(tab)" />
// script 方法区加入 2个方法
methods: {
isAffix(tab) { // 是否受保护,这里保护首页面不被删除。去掉X的按钮
// 在保护的路由 meta 增加 affix属性
return tab.meta && tab.meta.affix
},
closeTabs(tab) {
this.$store.dispatch('tabs/delView', tab).then(({ visitedViews }) => {
// 如果删除的是当前页面,则跳转去下一个页面。
if (this.isActive(tab)) {
if (visitedViews.length) {
// 跳转到最后一个标签
const lastTab = visitedViews[visitedViews.length - 1]
this.$router.push(lastTab.fullPath)
} else {
// 如果没有标签了,则跳去首页
this.$router.push('/')
}
}
})
}
}
标签拓展功能
刷新、关闭、关闭其他、关闭全部
// 加入 contextmenu属性。实现鼠标右键功能
<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')">关闭</li>
<li @click="menu('other')">关闭其他</li>
<li @click="menu('all')">全部关闭</li>
</ul>
</div>
方法区
...
data() {
return {
visible: false, // 菜单显隐变量
top: 0, // 菜单偏移量x
left: 0, // 菜单偏移量x
selectedTab: {} // 鼠标右击的tab
}
},
...
methods: {
openMenu(tab, e) {
// 计算偏移量
// this.$el = Tabs.vue 这个Dom
// getBoundingClientRect().left 获取tabs 距离窗口左边距离。
// 由于left 根据父元素进行偏移。
// 所以 left = 鼠标在窗口的x坐标 - 侧边栏宽度 15为菜单离鼠标一段距离
this.left = e.clientX - this.$el.getBoundingClientRect().left + 15 // 15: margin right
// top 由于不用适配,所以采用 鼠标在当前元素的相对位置
this.top = e.offsetY
// 显示菜单
this.visible = true
// 功能操作的tab。
this.selectedTab = tab
},
menu(select) {
switch (select) {
case 'refresh':
// 清除该页面缓存,在跳转该路由 达到刷新效果。
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(() => {
// 不是当前激活,删除其他后,跳转到该页面
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 加入刷新、关闭等执行方法
// 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) {
// 重置页面标签数组和缓存数组
state.visitedViews = [
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
]
state.cachedViews = [view.name]
},
DEL_ALL_VIEW(state) {
// 重置页面标签数组和缓存数组
state.visitedViews = []
state.cachedViews = []
}
最后再加上左键取消菜单的方法
...
watch: {
$route() {
this.addTab() // 路由一旦变化就会触发
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
...
methods: {
closeMenu() {
this.visible = false
}
}
到此导航栏 header 部分 配置完了。以上功能可以根据需求进行一些加工。