环境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>
网友评论