美文网首页
导航菜单

导航菜单

作者: key君 | 来源:发表于2019-10-18 16:24 被阅读0次

    环境Vue cli 3
    elementUI安装

    vue add element
    

    在store/index.js加入getters

    import Vue from "vue";
    import Vuex from "vuex";
    import user from "./modules/user";
    import permission from "./modules/permission";
    
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      modules: { user, permission },
      getters: {
        permission_routes: state => state.permission.routes,
      },
    });
    
    export default store;
    
    

    创建components/Sidebar/index.vue

    <template>
      <div>
        <el-scrollbar wrap-class="scrollbar-wrapper">
          <el-menu
            :default-active="activeMenu"
            :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 permission_routes"
              :key="route.path"
              :item="route"
              :base-path="route.path"
            />
          </el-menu>
        </el-scrollbar>
      </div>
    </template>
    
    <script>
    import { mapGetters } from "vuex";
    import SidebarItem from "./SidebarItem";
    
    export default {
      components: { SidebarItem },
      computed: {
        ...mapGetters(["permission_routes"]),
        activeMenu() {
          const route = this.$route;
          const { meta, path } = route;
          // 默认激活项
          if (meta.activeMenu) {
            return meta.activeMenu;
          }
          return path;
        },
        variables() {
          return {
            menuText: "#bfcbd9",
            menuActiveText: "#409EFF",
            menuBg: "#304156"
          };
        }
      }
    };
    </script>
    

    创建components/Sidebar/Item.vue

    <script>
    export default {
      name: 'MenuItem',
      functional: true,
      props: {
        icon: {
          type: String,
          default: ''
        },
        title: {
          type: String,
          default: ''
        }
      },
      render(h, context) {
        const { icon, title } = context.props
        const vnodes = []
    
        if (icon) {
            // jsx => vnode
          vnodes.push(<svg-icon icon-class={icon}/>)
        }
    
        if (title) {
          vnodes.push(<span slot='title'>{(title)}</span>)
        }
        return vnodes
      }
    }
    </script>
    
    

    创建components/Sidebar/SidebarItem.vue

    <template>
      <div v-if="!item.hidden" class="menu-wrapper">
        <!--跳转链接:叶子节点,独生子-->
        <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
          <router-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
            <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
              <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
            </el-menu-item>
          </router-link>
        </template>
        <!--父菜单-->
        <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
          <template v-slot:title>
            <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
          </template>
          <sidebar-item
            v-for="child in item.children"
            :key="child.path"
            :is-nest="true"
            :item="child"
            :base-path="resolvePath(child.path)"
            class="nest-menu"
          />
        </el-submenu>
      </div>
    </template>
    
    <script>
    import path from 'path'
    import Item from './Item'
    
    export default {
      name: 'SidebarItem',
      components: { Item },
      props: {
        // route object
        item: {
          type: Object,
          required: true
        },
        isNest: {
          type: Boolean,
          default: false
        },
        basePath: {
          type: String,
          default: ''
        }
      },
      data() {
        this.onlyOneChild = null
        return {}
      },
      methods: {
        hasOneShowingChild(children = [], parent) {
          const showingChildren = children.filter(item => {
            if (item.hidden) {
              return false
            } else {
              // 如果只有一个子菜单时设置
              this.onlyOneChild = item
              return true
            }
          })
    
          // 当只有一个子路由,该子路由默认显示
          if (showingChildren.length === 1) {
            return true
          }
    
          // 没有子路由则显示父路由
          if (showingChildren.length === 0) {
            this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
            return true
          }
    
          return false
        },
        resolvePath(routePath) {
          return path.resolve(this.basePath, routePath)
        }
      }
    }
    </script>
    
    

    router/inde.js加入

    import Vue from "vue";
    import Router from "vue-router";
    import Layout from '@/layout'; // 布局页
    
    Vue.use(Router);
    
    // 通用页面:不需要守卫,可直接访问
    export const constRoutes = [
      {
        path: "/login",
        component: () => import("@/views/Login"),
        hidden: true // 导航菜单忽略该项
      },
      {
        path: "/",
        component: Layout,// 应用布局
        redirect: "/home",
        children: [
          {
            path: "home",
            component: () =>
              import(/* webpackChunkName: "home" */ "@/views/Home.vue"),
            name: "home",
            meta: { 
                title: "Home", // 导航菜单项标题
                icon: "qq" // 导航菜单项图标
            }
          }
        ]
      }
    ];
    
    // 权限页面:受保护页面,要求用户登录并拥有访问权限的角色才能访问
    export const asyncRoutes = [
      {
        path: "/about",
        component: Layout,
        redirect: "/about/index",    
        children: [
          {
            path: "index",
            component: () =>
              import(/* webpackChunkName: "home" */ "@/views/About.vue"),
            name: "about",
            meta: { 
                title: "About", 
                icon: "qq",
                roles: ['admin', 'editor']
            },
          },
          {
            path: "abc",
            component: () =>
              import(/* webpackChunkName: "home" */ "@/views/About.vue"),
            name: "abc",
            meta: { 
                title: "abc", 
                icon: "wx",
                roles: ['admin', 'editor']
            },
          }
        ]
      }
    ];
    
    export default new Router({
      mode: "history",
      base: process.env.BASE_URL,
      routes: constRoutes
    });
    

    layout/indx.vue

    <template>
      <div class="app-wrapper">
        <!-- 导航菜单 -->
        <sidebar class="sidebar-container" />
        <div class="main-container">
          <Bread></Bread>
          <Breadcrumb></Breadcrumb>
          <router-view />
        </div>
      </div>
    </template>
    <script>
    import Sidebar from '@/components/Sidebar'
    import Bread from '@/components/Bread'
    import Breadcrumb from '@/components/Breadcrumb'
    
    export default {
      components: {
        Sidebar,Bread,Breadcrumb
      },
    }
    </script>
    

    components/Bread.vue

    <template>
      <div>
        <span v-for="(item, idx) in items" :key="item.path">
          <!-- 最后一项时只显示文本 -->
          <span v-if="idx === items.length - 1">{{item.meta.title}}</span>
          <!-- 否则显示超链接 -->
          <span v-else>
            <router-link :to="item">{{item.meta.title}}</router-link>
            <span>/</span>
          </span>
        </span>
      </div>
    </template>
    
    <script>
    export default {
      computed: {
        items() {
          console.log(this.$route.matched);
            
          // 根据matched数组获取面包屑数组
          // 要求必须有title且breadcrumb不为false
          return this.$route.matched.filter(
            item => item.meta && item.meta.title && item.meta.breadcrumb !== false
          );
        }
      }
    };
    </script>
    

    components/Breadcrumb.vue

    <template>
      <el-breadcrumb class="app-breadcrumb" separator="/">
        <transition-group name="breadcrumb">
          <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
            <span
              v-if="item.redirect==='noRedirect'||index==levelList.length-1"
              class="no-redirect"
            >{{ item.meta.title }}</span>
            <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
          </el-breadcrumb-item>
        </transition-group>
      </el-breadcrumb>
    </template>
    
    <script>
    import pathToRegexp from "path-to-regexp";
    
    export default {
      data() {
        return {
          levelList: null
        };
      },
      watch: {
        $route: {
          handler(route) {
            this.getBreadcrumb();
          },
          immediate: true
        }
      },
    
      methods: {
        getBreadcrumb() {
          console.log(this.$route.matched);
    
          // 面包屑仅显示包含meta.title且item.meta.breadcrumb不为false的路由
          let matched = this.$route.matched.filter(
            item => item.meta && item.meta.title && item.meta.breadcrumb !== false
          );
    
          // 根路由
          const first = matched[0];
    
          // 根匹配只要不是home,就作为home下一级
          if (!this.isHome(first)) {
            matched = [{ path: '/', redirect: "/home", meta: { title: "首页" } }].concat(matched);
          }
    
          // 处理完指定到levelList
          this.levelList = matched
        },
        isHome(route) {
          const name = route && route.name;
          if (!name) {
            return false;
          }
          return name.trim().toLocaleLowerCase() === "home".toLocaleLowerCase();
        },
        pathCompile(path) {
          const { params } = this.$route;
          // /detail/:id
          var toPath = pathToRegexp.compile(path);
          return toPath(params);
        },
        handleLink(item) {
          const { redirect, path } = item;
          // 若存在重定向,按重定向走
          if (redirect) {
            this.$router.push(redirect);
            return;
          }
          // 编译path,避免存在路径参数
          this.$router.push(this.pathCompile(path));
        }
      }
    };
    </script>
    
    <style scoped>
    .app-breadcrumb.el-breadcrumb {
      display: inline-block;
      font-size: 14px;
      line-height: 50px;
      margin-left: 8px;
    }
    .app-breadcrumb.el-breadcrumb .no-redirect {
      color: #97a8be;
      cursor: text;
    }
    
    /* breadcrumb transition */
    .breadcrumb-enter-active,
    .breadcrumb-leave-active {
      transition: all .5s;
    }
    
    .breadcrumb-enter,
    .breadcrumb-leave-active {
      opacity: 0;
      transform: translateX(20px);
    }
    
    .breadcrumb-move {
      transition: all .5s;
    }
    
    .breadcrumb-leave-active {
      position: absolute;
    }
    </style>
    
    

    相关文章

      网友评论

          本文标题:导航菜单

          本文链接:https://www.haomeiwen.com/subject/eegimctx.html