vue-element-admin改造顶部一级导航,侧边二级导航 (优化)

本项目是基于vue-element-admin的基础模板vue-admin-template进行改造的,因为实际项目很多会用到顶部导航菜单和侧边导航相结合的情况,为了满足这种需求做了改造。

先上效果图:

20200528205111709.gif

改造过程:

主要涉及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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容