本项目是基于vue-element-admin的基础模板vue-admin-template进行改造的,因为实际项目很多会用到顶部导航菜单和侧边导航相结合的情况,为了满足这种需求做了改造。
先上效果图:
改造过程:
主要涉及2个地方,在layout里加了一个Topbar组件,store里加了个permission用于存储侧边导航。
Topbar
位置: src/layout/components/Topbar.vue
主要思路就是把路由第一个拿出来,作为顶部导航,然后点击时,把当前选中的子项路由存在store中以便侧边导航使用。
<template>
<div class="top-nav">
<div class="log">后台管理系统</div>
<i v-if="isScroll" class="el-icon-arrow-left leftIcon" @click="leftmove" />
<div id="classifyScroll" class="module-list-wrap">
<el-menu
:active-text-color="variables.menuActiveText"
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
>
<div v-for="item in routes" :key="item.path" class="nav-item">
<app-link :to="resolvePath(item)">
<el-menu-item
v-if="!item.hidden"
:index="item.path"
>{{ item.meta ? item.meta.title : item.children[0].meta.title }}</el-menu-item>
</app-link>
</div>
</el-menu>
</div>
<i v-if="isScroll" class="el-icon-arrow-right rightIcon" @click="rightmove" />
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
<el-dropdown-menu slot="dropdown" style="padding: 6px 0;width: 110px;text-align: center;">
<router-link to="/">
<el-dropdown-item>
Home
</el-dropdown-item>
</router-link>
<a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/">
<el-dropdown-item>Github</el-dropdown-item>
</a>
<a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
<el-dropdown-item>Docs</el-dropdown-item>
</a>
<el-dropdown-item style="line-height: 30px;" @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import AppLink from './Sidebar/Link'
import { constantRoutes } from '@/router'
import variables from '@/styles/variables.scss'
import { isExternal } from '@/utils/validate'
export default {
name: 'Topbar',
components: {
AppLink
},
data() {
return {
clock: null,
isScroll: false,
routes: constantRoutes
}
},
computed: {
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
// 如果是首页,首页高亮
if (path === '/dashboard') {
return '/'
}
// 如果不是首页,高亮一级菜单
const activeMenu = '/' + path.split('/')[1]
return activeMenu
},
variables() {
return variables
},
sidebar() {
return this.$store.state.app.sidebar
},
...mapGetters(['avatar'])
},
mounted() {
// 初始化判断
this.handleScroll()
// 全局窗口变化监听,判断父元素和子元素的大小,从而控制isScroll的开关
window.addEventListener('resize', this.handleScroll)
this.initCurrentRoutes()
},
methods: {
// 判断是否有滚动条
handleScroll() {
const div = document.getElementsByClassName('el-menu')[0]
if ((div.scrollWidth > div.clientWidth) || (div.offsetWidth > div.clientWidth)) {
this.isScroll = true
} else {
clearInterval(this.clock)
this.isScroll = false
}
},
// 向左侧滚动
leftmove() {
clearInterval(this.clock)
const div = document.getElementsByClassName('el-menu')[0]
const num = div.scrollLeft - 240 > 0 ? div.scrollLeft - 240 : 0
// 添加动画效果
this.clock = setInterval(function() {
if (div.scrollLeft !== num) {
div.scrollLeft -= 10
} else {
clearInterval(this.clock)
}
}, 10)
},
// 向右侧滚动
rightmove() {
clearInterval(this.clock)
const div = document.getElementsByClassName('el-menu')[0]
const num = div.scrollLeft + 240 < div.scrollWidth ? div.scrollLeft + 240 : div.scrollWidth
// 添加动画效果
this.clock = setInterval(function() {
if (div.scrollLeft !== num) {
div.scrollLeft += 10
} else {
clearInterval(this.clock)
}
}, 10)
},
// 通过当前路径找到二级菜单对应项,存到store,用来渲染左侧菜单
initCurrentRoutes() {
const { path } = this.$route
let route = this.routes.find(
item => item.path === '/' + path.split('/')[1]
)
// 如果找不到这个路由,说明是首页
if (!route) {
route = this.routes.find(item => item.path === '/')
}
this.$store.commit('permission/SET_CURRENT_ROUTES', route)
this.setSidebarHide(route)
},
// 判断该路由是否只有一个子项或者没有子项,如果是,则在一级菜单添加跳转路由
isOnlyOneChild(item) {
if (item.children && item.children.length === 1) {
return true
}
return false
},
resolvePath(item) {
// 如果是个完成的url直接返回
if (isExternal(item.path)) {
return item.path
}
// 如果是首页,就返回重定向路由
if (item.path === '/') {
const path = item.redirect
return path
}
// 如果有子项,默认跳转第一个子项路由
let path = ''
/**
* item 路由子项
* parent 路由父项
*/
const getDefaultPath = (item, parent) => {
// 如果path是个外部链接(不建议),直接返回链接,存在个问题:如果是外部链接点击跳转后当前页内容还是上一个路由内容
if (isExternal(item.path)) {
path = item.path
return
}
// 第一次需要父项路由拼接,所以只是第一个传parent
if (parent) {
path += (parent.path + '/' + item.path)
} else {
path += ('/' + item.path)
}
// 如果还有子项,继续递归
if (item.children) {
getDefaultPath(item.children[0])
}
}
if (item.children) {
getDefaultPath(item.children[0], item)
return path
}
return item.path
},
handleSelect(key, keyPath) {
// 把选中路由的子路由保存store
const route = this.routes.find(item => item.path === key)
this.$store.commit('permission/SET_CURRENT_ROUTES', route)
this.setSidebarHide(route)
},
// 设置侧边栏的显示和隐藏
setSidebarHide(route) {
if (!route.children || route.children.length === 1) {
this.$store.dispatch('app/toggleSideBarHide', true)
} else {
this.$store.dispatch('app/toggleSideBarHide', false)
}
},
async logout() {
await this.$store.dispatch('user/logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/_main.scss";
@import "~@/styles/variables.scss";
.header-bar {
@include box(row, space-between);
@include size(100vw, #{$headerHeight});
background-color: $menuBg;
z-index: 999;
position: fixed;
top: 0;
}
.header-name {
font-size: 24px;
color: #ffffff;
padding-left: 20px;
}
.right-menu {
float: right;
height: 100%;
line-height: 14px;
padding-right: 20px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
.avatar-container {
margin-right: 10px;
margin-top: 14px;
.user-avatar {
cursor: pointer;
width: 40px;
height: 26px;
margin-top: 2px;
border-radius: 10px;
}
}
}
</style>
permission
位置: src/store/modules/permission.js
主要就是存储下选中的一级导航,用来渲染侧边导航。
import { constantRoutes } from '@/router'
const state = {
routes: [],
addRoutes: [],
currentRoutes: {}
}
const mutations = {
SET_CURRENT_ROUTES: (state, routes) => {
state.currentRoutes = routes
}
}
export default {
namespaced: true,
state,
mutations
}
最后就是要修改下里的routes,设置为当前选中路由的子路由
routes() {
// return this.$router.options.routes
return this.$store.state.permission.currentRoutes.children
},
中间还有一些细节比如侧边导航和收缩图标的显示隐藏,可以自行下载源码查看。
摘自 # weixin_43180359的博客
在此基础上进行更改,顶栏改为弹性布局,顶栏路由过多时,可左右点击滚动,登录下拉优化
添加点击按钮和在某条件下显示,使用element UI图标
<template>
<div class="top-nav">
<div class="log">后台管理系统</div>
<i v-if="isScroll" class="el-icon-arrow-left leftIcon" @click="leftmove" />
<div id="classifyScroll" class="module-list-wrap">
<el-menu
:active-text-color="variables.menuActiveText"
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
>
<div v-for="item in routes" :key="item.path" class="nav-item">
<app-link :to="resolvePath(item)">
<el-menu-item
v-if="!item.hidden"
:index="item.path"
>{{ item.meta ? item.meta.title : item.children[0].meta.title }}</el-menu-item>
</app-link>
</div>
</el-menu>
</div>
<i v-if="isScroll" class="el-icon-arrow-right rightIcon" @click="rightmove" />
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
<el-dropdown-menu slot="dropdown" style="padding: 6px 0;width: 110px;text-align: center;">
<!-- <router-link to="/system/changePwd">
<el-dropdown-item style="line-height: 30px;">
修改信息
</el-dropdown-item>
</router-link> -->
<el-dropdown-item style="line-height: 30px;" @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
js部分
添加定时器是为了点击时有过渡效果
mounted() {
// 初始化判断
this.handleScroll()
// 全局窗口变化监听,判断父元素和子元素的大小,从而控制isScroll的开关
window.addEventListener('resize', this.handleScroll)
},
methods: {
// 判断是否有滚动条
handleScroll() {
const div = document.getElementsByClassName('el-menu')[0]
if ((div.scrollWidth > div.clientWidth) || (div.offsetWidth > div.clientWidth)) {
this.isScroll = true
} else {
clearInterval(this.clock)
this.isScroll = false
}
},
// 向左侧滚动
leftmove() {
clearInterval(this.clock)
const div = document.getElementsByClassName('el-menu')[0]
const num = div.scrollLeft - 240 > 0 ? div.scrollLeft - 240 : 0
// 添加动画效果
this.clock = setInterval(function() {
if (div.scrollLeft !== num) {
div.scrollLeft -= 10
} else {
clearInterval(this.clock)
}
}, 10)
},
// 向右侧滚动
rightmove() {
clearInterval(this.clock)
const div = document.getElementsByClassName('el-menu')[0]
const num = div.scrollLeft + 240 < div.scrollWidth ? div.scrollLeft + 240 : div.scrollWidth
// 添加动画效果
this.clock = setInterval(function() {
if (div.scrollLeft !== num) {
div.scrollLeft += 10
} else {
clearInterval(this.clock)
}
}, 10)
}
}
样式部分更改
位置: src/styles/topbar.scss
添加顶栏最小宽度和flex布局
.top-nav {
// margin-left: $sideBarWidth;
width: 100%;
min-width: 600px;
height: 56px;
display: flex;
background-color: #304156;
position: fixed;
top: 0;
left: 0;
z-index: 1001;
overflow: hidden;
.log {
width: 200px;
min-width: 200px;
padding: 0 20px;
line-height: 56px;
font-size: 24px;
font-weight: bold;
color: #f4f4f5;
}
.module-list-wrap{
width: 100%;
min-width: 360px;
overflow-x: auto;
}
.leftIcon{
width: 26px;
min-width: 26px;
text-align: center;
color: aliceblue;
line-height: 56px;
}
.leftIcon:hover{
background: #303133;
color: aliceblue;
}
.rightIcon{
display: inline-block;
width: 26px;
min-width: 26px;
text-align: center;
color: aliceblue;
line-height: 56px;
}
.rightIcon:hover{
background: #303133;
color: aliceblue;
}
.el-menu {
width: 100%;
white-space: nowrap;
overflow: hidden;
border: none!important;
background-color: #304156;
.nav-item {
display: inline-block;
.el-menu-item {
color: rgb(191, 203, 217);
&:hover {
background-color: $subMenuHover !important;
}
&:focus {
background-color: $subMenuHover !important;
// color: $subMenuActiveText !important;
}
}
}
}
.right-menu {
height: 100%;
text-align: right;
margin-left: 20px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
vue模块化开发过程中,页面transition切换时出现X轴滚动条的问题
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
</transition>
</section>
</template>
css动画切换页面的原理,它用的transform的原理,也就是说通过平移来实现视觉上的切换效果,如果说你的主页面有置顶导航与底端导航,就可以造成页面被蹭大,下方出现滚动条,也可能会导致固定在上方和底部的导航栏变形,这样在切换的一瞬间,会很丑。
分享的一个简单的解决方法:
在切换页面外面再加一个div容器:
<template>
<section class="app-main fixation-box">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
</transition>
</section>
</template>
.fixation-box{
position: absolute;
top: 0px;
bottom: 0px;
width: 100%;
overflow: hidden;
}
包裹在外面的这个div容器的css样式,通过这个div容器,来固定住,不让页面切换时的平移来把整个页面蹭大
改变动画效果 比如(淡入淡出)
代码仓库地址:
https://github.com/SongFuKangM/vue-admin-template-demo