🌟前言

因为最近在整合公司的项目,需要把所有系统里的功能集成到一个项目里,这样就导致菜单栏目录会特别的多,不便于用户使用,体验效果极差。于是想到了一个方法,就是增加顶部导航栏,点击的时候让侧边菜单栏在显示相对应模块的所有菜单;这样的话就可以很大程度提升我们的用户体验啦。

🌟小伙伴们先看

在这里插入图片描述

🌟实现思路

嗯,干活前一定要先把思路理清楚,记在小本本上,画个图都行哈哈

  • 布局方面我需要在Navbar组件内添加一个导航组件以便我们去渲染顶部模块菜单;
  • 因为是动态路由所以我们可以:
    • 登录的时候让后端返回所有的当前用户下所有的菜单权限;
    • 登录时候只返回默认显示的菜单,每次点击的时候再去获取相应的模块菜单权限。

我这边用的是第一种方式,登陆的时候获取全部的存在vuex里,每次点击的时候再去处理相应的数据;小伙伴们也可以尝试一下第二种方式哦。

🌟具体代码

话不多说,直接开整。。。

<!--src/layout/components/Navbar.vue-->
<template>
  <div class="navbar">
    <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />

    <!--重点一:顶部menu-->
    <el-menu
      mode="horizontal"
      default-active="/"
      @select="handleSelect"
    >
      <el-menu-item v-for="item in menuList" :key="item.path" class="menuItem" :index="item.path">
        <icon :class="item.meta?item.meta.icon:''" />
        <span slot="title">{{ item.name }}</span>
      </el-menu-item>
    </el-menu>
    <div class="right-menu">
      <el-dropdown class="avatar-container" trigger="click">
        <div class="avatar-wrapper">
          <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
          <i class="el-icon-caret-bottom" />
        </div>
        <el-dropdown-menu slot="dropdown" class="user-dropdown">
          <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 divided @click.native="logout">
            <span style="display:block;">Log Out</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'

export default {
  components: {
    Breadcrumb,
    Hamburger
  },
  computed: {
    ...mapGetters([
      'sidebar',
      'avatar'
    ]),
    toIndex() { // 根据路径绑定到对应的一级菜单,防止页面刷新重新跳回第一个
      return '/' + this.$route.path.split('/')[1]
    }
  },
  // eslint-disable-next-line vue/order-in-components
  data() {
    return {
      menuList: [ // 水平一级菜单栏的菜单

      ]
    }
  },
  mounted() {
  // 初始化菜单数据
    this.initMenuList()
  },
  methods: {
  // 重点二:
  // 因为整个项目工程比较大,所以当时搭建了一个demo,菜单数据我写在了本地;
  // 大家在实现的时候可以通过上边第一种方法;
  // 后台获取回来数据以后通过 router.addRoutes(获取回来的菜单数组)方法;
  // 动态的挂载到我们的router上。
    initMenuList() {
      const menuList = ['/login', '/404']
      this.menuList = this.$router.options.routes.filter((v, i) => {
        return v.path !== menuList[i]
      })
    },
    // 重点三:
    // 根据当前惦记的顶部模块菜单去切换左侧菜单栏,把当前点击的菜单path存在vuex里
    // 我这边是存在了store/modules/user里边,这个没有要求,小伙伴们随意
    handleSelect(path) {
      this.$store.dispatch('user/setPath', path)
    },

    toggleSideBar() {
      this.$store.dispatch('app/toggleSideBar')
    },
    async logout() {
      await this.$store.dispatch('user/logout')
      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
    }
  }
}
</script>

<style lang="scss" scoped>
.navbar {
  display: flex;
  justify-content: space-between;
  height: 50px;
  overflow: hidden;
  position: relative;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0,21,41,.08);

  .hamburger-container {
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background .3s;
    -webkit-tap-highlight-color:transparent;

    &:hover {
      background: rgba(0, 0, 0, .025)
    }
  }

  .breadcrumb-container {
    float: left;
  }

  .right-menu {
    float: right;
    height: 100%;
    line-height: 50px;

    &: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;
        }
      }
    }
  }
}
  .menuItem{
    height: 47px;
  }
</style>

// src/store/modules/user
import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'

const getDefaultState = () => {
  return {
    token: getToken(),
    name: '',
    avatar: '',
    // 定义以下两个状态
    menuList: [], // 动态路由
    path: '/' // 当前点击的菜单模块path
  }
}

const state = getDefaultState()

const mutations = {
  RESET_STATE: (state) => {
    Object.assign(state, getDefaultState())
  },
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  // 定义SET_MENULIST方法用来保存我们的动态路由
  SET_MENULIST: (state, menuList) => {
    state.menuList = menuList
  },
   // 定义SET_MENULIST方法用来保存我当前点击的顶部模块菜单path
  SET_PATH: (state, path) => {
    state.path = path
  }
}

const actions = {
  // user login
  login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password }).then(response => {
        const { data } = response
        commit('SET_TOKEN', data.token)
        setToken(data.token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },
  
  //定义两个actions 方法用来执行我们上边定义的SET_MENULIST和SET_PATH
  setMenuList({ commit }, menuList) {
    commit('SET_MENULIST', menuList)
  },
  setPath({ commit }, path) {
    commit('SET_PATH', path)
  },

  // get user info
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo(state.token).then(response => {
        const { data } = response

        if (!data) {
          return reject('Verification failed, please Login again.')
        }

        const { name, avatar } = data

        commit('SET_NAME', name)
        commit('SET_AVATAR', avatar)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

  // user logout
  logout({ commit, state }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        removeToken() // must remove  token  first
        resetRouter()
        commit('RESET_STATE')
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // remove token
  resetToken({ commit }) {
    return new Promise(resolve => {
      removeToken() // must remove  token  first
      commit('RESET_STATE')
      resolve()
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}


动态菜单和path都存好了以后我们就可以根据当前点击的path去动态的渲染我们的侧边栏啦

<!--src/layout/components/Sidebar/index.vue-->
<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
        <sidebar-item v-for="route in menuList" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'

export default {
  components: { SidebarItem, Logo },
  computed: {
    ...mapGetters([
      'sidebar'
    ]),
    routes() {
      return this.$router.options.routes
    },
    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
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  },
  watch: {
  // 因为每次点击顶部菜单的时候path都会改变,所以我们要对它进行监听;
  // 通过数组的filter方法去过滤出来我们想要的菜单数组就可以啦。
    '$store.state.user.path': {
      handler: function(newVal, oldVal) {
        console.log('新值' + newVal, '旧值' + oldVal)
        console.log('vuex里存的菜单', this.$store.state.user.menuList)
        this.menuList = this.$store.state.user.menuList.filter(v => {
          return newVal === v.path
        })
      }
    }
  },
  mounted() {
  // 页面渲染时候获取一下vuex里的menuList,因为刚才在vuex里定义的path默认给了'/';
  // 所以第一次进来的时候默认显示的首页
    console.log('当前path', this.$store.state.user.path)
    this.$store.dispatch('user/setMenuList', this.$router.options.routes)
    this.menuList = this.$store.state.user.menuList.filter(v => {
      return this.$store.state.user.path === v.path
    })
  },
  // eslint-disable-next-line vue/order-in-components
  data() {
    return {
      menuList: []
    }
  }
}
</script>

🌟最后

当我们接到新需求的时候,一定要仔细分析把逻辑梳理清楚了;复杂的话我们可以画一下流程图以便我们更好的去写代码;万变不离其宗,思路最重要。小伙伴们如果有更好的思路,可以一起交流,共同进步。

✨原创不易,还希望各位大佬支持一下!
👍 点赞,你的认可是我创作的动力!
⭐️ 收藏,你的青睐是我努力的方向!
✏️ 评论,你的意见是我进步的财富!

Logo

前往低代码交流专区

更多推荐