项目代码地址 gitee.com/zhengwantin…
1.layout基本布局实现
使用elementUI的页面布局实现,使用的时候,注意elementui的版本,vue3和vue2对应的版本不同。
首先我们先在main.js中按需引入我们需要的组件
src/components中创建layout文件夹->创建layout.vue文件
<template>
<el-container class="home-container">
<!-- 头部区域 -->
<el-header>
<div>
<img src="@/assets/logo.png" alt="" />
<span>VUE2后台管理系统</span>
</div>
<el-button @click="logOut"> 退出</el-button>
</el-header>
<!-- 主体区域 -->
<el-container>
<!-- 侧边栏 -->
<el-aside width="200px"> </el-aside>
<!-- 页面展示区域 -->
<el-main> </el-main>
</el-container>
</el-container>
</template>
<script>
export default {
name: "Layout",
methods: {
// 退出登录
logOut() {
window.sessionStorage.clear();
this.$router.push("/login");
},
},
};
</script>
<style scoped lang="less">
.el-header {
background-color: #b3c0d1;
color: #fff;
font-size: 20px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
> div {
display: flex;
align-items: center;
span {
margin-left: 15px;
}
}
img {
height: 45px;
width: 45px;
border-radius: 50%;
border: 1px solid #eee;
padding: 2px;
box-shadow: 0 0 10px #ddd;
}
}
.el-aside {
background-color: #d3dce6;
color: #333;
}
.el-main {
background-color: #e9eef3;
color: #333;
}
.home-container {
height: 100%;
}
</style>
并在router.js文件中的路由加入
import Layout from "../components/layout/layout.vue"
...
export const constantRoutes = [
{
path: '/',
name: '',
hidden: true,
redirect: '/login', //重定向
},
{
path: '/login',
name: "登陆",
component:Login
},
{
path:"/home",
component:Layout,
name:"container",
},
]
...
此时我们登陆进去看到的页面是
左侧点击退出按钮会清除token,我们不通过点击登陆按钮直接通过路由访问,是发现是访问不了的,路由会变成login。这就是为什么我们需要设置路由守卫,必须登陆之后才能访问除登陆的其他页面。
2.实现左侧菜单栏
-
scr/components/layout文件夹下创建 components文件夹->创建menu.vue文件 找到elementui中的NavMenu组件。
-
在main.js按需引入对应的菜单栏组件Menu,MenuItem,Submenu(怎么引用可以看教程一)
-
在layout.vue文件中引入写好的menu组件
4. 实现menu菜单基本实现
<template>
<div>
<el-menu
background-color="#d3dce6"
text-color="#5d6c7f"
active-text-color="#409EFF"
unique-opened
>
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item index="1-1">
<span slot="title">页面1</span>
</el-menu-item>
<el-menu-item index="1-2">
<span slot="title">页面2</span>
</el-menu-item>
</el-submenu>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-menu"></i>
<span>导航二</span>
</template>
<el-menu-item index="2-1">
<span slot="title">页面1</span>
</el-menu-item>
</el-submenu>
</el-menu>
</div>
</template>
<script>
export default {
name: "Menu",
data() {
return {};
},
created() {},
methods: {},
};
</script>
实现样式如下图
在elementui文档下方是会有对应的说明,需要的话对应引用即可
| 属性 | 说明 |
|---|---|
| background-color="#d3dce6" | 菜单的背景色 |
| active-text-color | 当前激活菜单的字体颜色 |
| unique-opened | 是否只展开一个子菜单(默认是false) |
- 实现菜单栏的折叠功能
- 实现按钮的样式
...
<div class="toggle-button">
<span>|||</span>
</div>
...
<style scoped>
.toggle-button {
background-color: #c3d6eb;
color: #9fb0c4;
padding: 6px;
line-height: 24px;
cursor: pointer;
letter-spacing: 0.1em;
text-align: center;
}
</style>
- 给el-menu添加collapse属性用来控制是否折叠菜单;给按钮添加点击事件(点击控制菜单栏的是否折叠)
<template>
<div>
<div class="toggle-button" @click="toggleCollapse">
<span>|||</span>
</div>
<el-menu
background-color="#d3dce6"
text-color="#5d6c7f"
active-text-color="#409EFF"
:collapse-transition="false"
:collapse="isCollapse"
unique-opened
>
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item index="1-1">
<span slot="title">页面1</span>
</el-menu-item>
<el-menu-item index="1-2">
<span slot="title">页面2</span>
</el-menu-item>
</el-submenu>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-menu"></i>
<span>导航二</span>
</template>
<el-menu-item index="2-1">
<span slot="title">页面1</span>
</el-menu-item>
</el-submenu>
</el-menu>
</div>
</template>
<script>
export default {
name: "Menu",
data() {
return {
isCollapse: false,
};
},
created() {},
methods: {
// 点击按钮事件
toggleCollapse() {
this.isCollapse = !this.isCollapse;
},
},
};
</script>
<style scoped>
.toggle-button {
background-color: #c3d6eb;
color: #9fb0c4;
padding: 6px;
line-height: 24px;
cursor: pointer;
letter-spacing: 0.1em;
text-align: center;
}
</style>
实现样式如图
我们会发现,就算菜单折叠了,但是左侧的el-aside并没有折叠,因为aside设置的是定值200px。我们希望实现的是会折叠。这样我们就需要用到父子组件的传参这个知识,把isCollapse这个变量的值从menu.vue组件传值给layout.vue
- 解决出现的问题
toggleCollapse() {
this.isCollapse = !this.isCollapse;
// 第一个参数自定义方法名称,第二个参数方法对应的传参
this.$emit("toggleCollapse", this.isCollapse);
},
toggleCollapse() {
this.isCollapse = !this.isCollapse;
// 第一个参数自定义方法名称,第二个参数方法对应的传参
this.$emit("toggleCollapse", this.isCollapse);
},
在layout.vue中接收这个值,并通过三元表达式去改变aside的width属性
...
<el-container>
<!-- 侧边栏 -->
<el-aside :width="isCollapseVal ? '64px' : '200px'">
<menu-index @toggleCollapse="getIsCollapseVal"></menu-index>
</el-aside>
<!-- 页面展示区域 -->
<el-main> </el-main>
</el-container>
...
data() {
return {
isCollapseVal: false,
};
},
methods: {
getIsCollapseVal(val) {
this.isCollapseVal = val;
},
},
3. vuex的使用
因为登陆之后的token不止需要在本地存储,还需要在vuex中进行存储所以我们对登陆进行一些修改
// 安装vuex,注意版本
npm i vuex@3
src下创建store文件夹-> 创建index.js用来写vuex
import Vue from 'vue';
import Vuex from 'vuex';
Vue.ues(Vuex)
export default new Vuex.Store({
state:{},
mutations:{},
actions: {},
modules: {},
})
Vuex是专为vue应用程序开发的状态管理模式,采用集中式存储管理应用的所有组件的状态,解决多组件数据通信,
state:统一定义公共数据(类似data)
mutations: 使用它来修改数据(类似methods),更改state中的数据必须使用mutations来进行修改
getters:类似于computed(对现有的数据进行计算得到新的数据)
actions: 发起异步请求
modules:模块拆分
需要在main.js中引入vuex
...
import store from "./store/index"
...
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
在store下创建defaultState.js文件
export default {
get UserToken(){
return sessionStorage.getItem('token')
},
set UserToken(value){
sessionStorage.setItem('token',value)
}
}
export default {
LOGIN_IN(state,token){
state.UserToken = token;
},
LOGIN_OUT(state){
state.UserToken = "";
}
}
对登陆的login方法进行修改
// window.sessionStorage.setItem("token", token);
// 通过调用vuex中定义的LOGIN_IN方法,对token进行本地和vuex中进行存储
this.$store.commit("LOGIN_IN", token);
同样的退出登陆也同样实现
// 退出登录
logOut() {
this.$store.commit("LOGIN_OUT");
window.sessionStorage.clear();
this.$router.push("/login");
},
4.路由权限的登陆修改
之前我们是直接写在了router/index.js文件中,现在我们创建router/permission.js文件专门用来写权限的判断
引入了vuex之后,我们对权限的判断也使用vuex去获取
删除之前在router/index.js中写的路由守卫,在permission文件中重写,需要在main.js中引入permission文件
import './router/permission'
import router from "./index";
import store from "../store/index"
router.beforeEach((to,from,next) => {
if(to.path === "/login" || to.path === "/") return next();
if(!store.state.UserToken) {
// 未登录 页面是否需要登陆才能访问
if(to.matched.length >0 && !to.matched.some(recode => recode.meta.requiresAuth)){
return next();
}else{
return next("/login")
}
}else{
// token存在说明用户已登陆 路由的访问权限:当前页面是否显示
next()
}
})
to.matched数组中保存着匹配到的所有路由信息
some用于检测数组中的元素是否满足指定条件(函数提供)。会依次执行数组的每个元素:如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
个人理解:这里的to.matched会返回匹配到该路由的所有路由信息,前面一个判断是你访问的路由成功匹配到了,若没有length就为0,那就直接返回登陆页面。后面的判断中requiresAuth是在路由的meta中用来判断这个路由是否需要token才能访问。若你访问的路由中有requiresAuth为true就证明需要登陆才能访问。
5.实现动态路由
1.前端需要定义好全部的路由地址
2.通过用户角色不同,向后台请求不同的用户权限数据
3.对用户权限做对比:后台请求的数据和前端定义的路由进行对比, 将对比上的路由来生成该用户的路由
- 定义全部的路由 router文件夹下创建async-router.js文件所有的页面 对应路由自己在views文件夹中创建页面组件
import Good from "@/views/goods-manage/goods-index.vue"
/* 需要权限判断的路由 */
const asyncRouter = [
{
path:'/goods',
component: Good,
name: 'goods-manage',
meta:{
name:"商品管理",
},
children:[
{
path:'classify',
name:'goods-classify',
component:()=>import("@/views/goods-manage/goods-classify/index.vue"),
meta: {
name:"商品分类",
icon:""
}
},
{
path:'list',
name:'goods-list',
component:()=>import("@/views/goods-manage/goods-list/index.vue"),
meta: {
name:"商品列表",
icon:""
}
}
],
}
]
export default asyncRouter
2.后台返回的权限数据 在mock/data中创建admin_permission.json 用来返回admin(user)权限的路由信息例:
{
"code": 200,
"message": "获取权限成功",
"data": [
{
"name":"商品管理",
"children":[
{
"name":"商品列表"
},
{
"name":"商品分类"
}
]
}
]
}
json中的name比较时和前端定义路由的meta中的name进行比较 mock/index.js
const adminPermission = require("./data/admin_permission.json")
const userPermission = require("./data/user_permission.json")
...
app.get("/permission",(req,res)=>{
const user = url.parse(req.url,true).query.user;
if(user === "admin"){
// 响应内容传给客户端
res.send(adminPermission);
}else {
res.send(userPermission);
}
})
写完之后需要重新启动node index.js 在api/base.js中封装获取权限的接口
import store from "../store/index"
// 获取路由接口
export function fetchPermission() {
return request.get("/api/permission?user=" + store.state.UserToken);
}
- 路由对比
需要把路由存储在vuex中 在store下创建modules->创建index.js和permission.js 先引入创建关联
import permission from "./permission"
export default {
permission
}
import Vue from 'vue';
import Vuex from 'vuex';
import state from "./defaultState"
import mutations from './mutations';
import modules from './modules';
Vue.use(Vuex)
export default new Vuex.Store({
state,
mutations,
modules
})
在utils中创建recursion-router.js文件 作用:①前端创建的路由和后端返回的路由做对比生成该用户真正的路由②设置默认路由(重定向)
/**
* 对比路由权限
* @param {Array} userRouter 后台返回的路由权限
* @param {Array} allRouter 前端配置好的路由权限数据
* @returns {Array} realRouter 过滤后符合条件的路由
*/
export function recursionRouter(userRouter=[],allRouter=[]){
const realRouter = [];
allRouter.forEach((v,i)=>{
userRouter.forEach((item,index)=>{
if(item.name === v.meta.name){
// 后端返回的路由的name和前端定义的路由中的meta中name进行比较
if(item.children && item.children.length){
// 可能会有子路由,且并不是所有子路由都会属于这个权限
// 所以需要进行递归比较,只把后端返回且匹配上的路由返回
v.children = recursionRouter(item.children,v.children)
}
realRouter.push(v)
}
})
})
return realRouter
}
/**
* 指定返回的默认路由
* @param {Array} routes
*/
export function setDefaultRouter(routes){
routes.forEach((v,i) => {
// 如果这个路由有子路由,就指定父路由重定向为第一个子路由的路径
if(v.children && v.children.length > 0){
// 也可设定为路径跳转
v.redirect = { name : v.children[0].name};
// 可能会出现子路由嵌套子路由所以需要递归
setDefaultRouter(v.children);
}
})
}
store/modules/permission.js
import { fetchPermission } from "@/api/base.js";
import router,{ asyncRoutes } from "@/router/index.js";
import asyncRouter from "../../router/async-router";
import {recursionRouter,setDefaultRouter} from "@/utils/recursion-router.js";
export default {
namespaced: true,
state: {
permissionList: null, // 所有路由
sidebarMenu: [], //导航菜单
currentMenu:'', //高亮
},
getters: {},
mutations: {
// 设置权限
SET_PERMISSION(state,routes) {
state.permissionList = routes
},
// 清理所有权限
CLEAR_PERMISSION(state){
state.permissionList = null;
},
// 设置菜单
SET_MENU(state,menu){
state.sidebarMenu = menu;
},
// 清除菜单
CLEAR_MENU(state){
state.sidebarMenu = [];
}
},
// 异步处理
actions:{
async FETCH_PERMISSION(){
let permission = [];
await fetchPermission().then((res) => {
console.log(res)
permission = res;
}).catch((err) =>{
console.log(err)
})
}
}
}
在路由配置router/permission.js中访问,用户登陆成功后就需要立即获取权限和菜单栏信息
import router from "./index";
import store from "../store/index"
router.beforeEach((to,from,next) => {
if(to.path === "/login" || to.path === "/") return next();
if(!store.state.UserToken) {
// 未登录 页面是否需要登陆才能访问
if(to.matched.length >0 && !to.matched.some(recode => recode.meta.requiresAuth)){
return next();
}else{
return next("/login")
}
}else{
// token存在说明用户已登陆 路由的访问权限:当前页面是否显示
return next()
}
})
在store/modules/permission.js中实现功能
import { fetchPermission } from "@/api/base.js";
import router,{ asyncRoutes } from "@/router/index.js";
import asyncRouter from "@/router/async-router";
import {recursionRouter,setDefaultRouter} from "@/utils/recursion-router.js";
export default {
namespaced: true,
state: {
permissionList: null, // 该用户的所有权限
sidebarMenu: [], //导航菜单
currentMenu:'', //高亮
},
getters: {},
mutations: {
// 设置权限
SET_PERMISSION(state,routes) {
state.permissionList = routes
},
// 清理所有权限
CLEAR_PERMISSION(state){
state.permissionList = null;
},
// 设置菜单
SET_MENU(state,menu){
state.sidebarMenu = menu;
},
// 清除菜单
CLEAR_MENU(state){
state.sidebarMenu = [];
}
},
// 异步处理
actions:{
async FETCH_PERMISSION({commit,state}){
await fetchPermission().then((res) => {
let permission = [];
permission = res;
// 生成对应的路由
let routes = recursionRouter(permission,asyncRouter);
// 我们需要把生成的动态路由添加到router/index.js中我们定义的asyncRoutes的路由中
// 所有的路由都是layout的子路由
let MainContainer = asyncRoutes.find( v => v.path === "");
let children = MainContainer.children;
children.push(...routes);
// 生成菜单
commit("SET_MENU",children);
// 设置默认路由
setDefaultRouter([MainContainer]);
// 初始化路由
// 静态路由
let initRoutes = router.options.routes;
// 把动态路由添加进去
router.addRoutes(asyncRoutes);
// 设置该用户的所有权限
commit("SET_PERMISSION",[...initRoutes, ...asyncRoutes])
}).catch((err) =>{
console.log(err)
})
}
}
}
6.将获取到的动态菜单在左侧菜单栏显示
1.通过循环得到左侧菜单(判断是否父级菜单是否有子菜单进行分类)
2.添加菜单点击跳转事件
需要注意的是我这里只考虑了双层菜单,不考虑多层重叠菜单,若菜单重叠过多建议使用递归实现。
<template>
<div>
<div class="toggle-button" @click="toggleCollapse">
<span>|||</span>
</div>
<el-menu
background-color="#d3dce6"
text-color="#5d6c7f"
active-text-color="#409EFF"
:collapse-transition="false"
:collapse="isCollapse"
unique-opened
>
<template v-for="item in menuList">
<el-submenu
v-if="item.children && item.children.length > 0"
:index="item.name"
:key="item.name"
>
<!-- 一级菜单 -->
<template slot="title">
<!-- 图标以及名称 -->
<i :class="item.meta.icon"></i>
<span>{{ item.meta.name }}</span>
</template>
<!-- 二级菜单 -->
<el-menu-item
@click="gotoRoute(subItem.name)"
v-for="subItem in item.children"
:index="subItem.name"
:key="subItem.name"
>
<i :class="subItem.meta.icon"></i>
<span>{{ subItem.meta.name }}</span>
</el-menu-item>
</el-submenu>
<el-menu-item
v-else
:index="item.name"
:key="item.name"
@click="gotoRoute(item.name)"
>
<i :class="item.meta.icon"></i>
<span>{{ item.meta.name }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script>
export default {
name: "Menu",
data() {
return {
isCollapse: false,
menuList: [],
};
},
created() {
this.menuList = this.$store.state.permission.sidebarMenu;
console.log(this.menuList);
},
methods: {
toggleCollapse() {
this.isCollapse = !this.isCollapse;
this.$emit("toggleCollapse", this.isCollapse);
},
gotoRoute(pathName) {
// 判断点击的是否为同一路径名
if (this.$router.currentRoute.name == pathName) {
// 相同,重新加载该页面
this.$router.go(0);
} else {
// 不同就直接跳转
this.$router
.push({
name: pathName,
})
.catch((err) => {
console.log(err);
});
}
},
},
};
</script>
<style scoped>
.toggle-button {
background-color: #c3d6eb;
color: #9fb0c4;
padding: 6px;
line-height: 24px;
cursor: pointer;
letter-spacing: 0.1em;
text-align: center;
}
</style>
同时在layout.vue界面中,添加页面占位符
<el-main>
<router-view></router-view>
</el-main>
需要在layout.vue中在退出登陆方法中添加window.location.reload()
logOut() {
this.$store.commit("LOGIN_OUT");
window.sessionStorage.clear();
this.$router.push("/login");
/* 防止切换角色时addRoutes重复添加路由导致出现警告 */
window.location.reload();
},
登陆时也需要清除之前存在的permission和menu值
this.$store.commit("permission/CLEAR_PERMISSION");
this.$store.commit("permission/CLEAR_MENU");
async login() {
this.$refs.loginFrom.validate(async (valid) => {
if (valid) {
await login(this.loginForm.userName)
.then((res) => {
if (res) {
let token = res.token;
console.log("登陆成功获取到的token值为:" + token);
this.$store.commit("permission/CLEAR_PERMISSION");
this.$store.commit("permission/CLEAR_MENU");
this.$store.commit("LOGIN_IN", token);
this.$message.success("登陆成功");
this.$router.push("/home");
// 1.登陆成功之后的token,保存在sessionStorage中
// 1.1.项目中的其他页面以及接口,必须登陆成功之后才能被访问
// 1.2 token只应在当前网站打开期间生效,所以将token存放在sessionStorage中
// 也可以存在在localStorage中,设置token的失效时间,若失效,就需要重新登陆
// 2.登陆成功后生成对应的权限的路由地址,以及导航到首页中/home。
} else {
this.$message.error("用户名密码错误请重新输入");
this.$refs.loginFrom.resetFields();
}
})
.catch((err) => {
console.log(err);
});
} else {
console.log("error submit!!");
return false;
}
});
},
当我们登陆不同的用户时,对应设置的后端返回菜单不同,最后生成的动态路由不同
admin用户
user用户
所以增加新的页面时,前端配置的路由需要手动添加。后端返回的接口也需要添加