美文网首页
导航菜单

导航菜单

作者: 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