vue2后台管理(二)layout实现+首页实现+动态路由的实现

575 阅读2分钟

项目代码地址 gitee.com/zhengwantin…

1.layout基本布局实现

使用elementUI的页面布局实现,使用的时候,注意elementui的版本,vue3和vue2对应的版本不同。

image.png 首先我们先在main.js中按需引入我们需要的组件

image.png

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",
    },
]
...

此时我们登陆进去看到的页面是

image.png

左侧点击退出按钮会清除token,我们不通过点击登陆按钮直接通过路由访问,是发现是访问不了的,路由会变成login。这就是为什么我们需要设置路由守卫,必须登陆之后才能访问除登陆的其他页面。

2.实现左侧菜单栏

  1. scr/components/layout文件夹下创建 components文件夹->创建menu.vue文件 找到elementui中的NavMenu组件。

  2. 在main.js按需引入对应的菜单栏组件Menu,MenuItem,Submenu(怎么引用可以看教程一)

  3. 在layout.vue文件中引入写好的menu组件

image.png 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>

实现样式如下图

image.png

在elementui文档下方是会有对应的说明,需要的话对应引用即可

属性说明
background-color="#d3dce6"菜单的背景色
active-text-color当前激活菜单的字体颜色
unique-opened是否只展开一个子菜单(默认是false)
  1. 实现菜单栏的折叠功能
  • 实现按钮的样式
...
  <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>

实现样式如图 image.png

我们会发现,就算菜单折叠了,但是左侧的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方法进行修改

同样的退出登陆也同样实现

    // 退出登录
    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.对用户权限做对比:后台请求的数据和前端定义的路由进行对比, 将对比上的路由来生成该用户的路由

  1. 定义全部的路由 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);
}
  1. 路由对比

需要把路由存储在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");

当我们登陆不同的用户时,对应设置的后端返回菜单不同,最后生成的动态路由不同

image.png admin用户

image.png

image.png user用户

image.png

image.png

所以增加新的页面时,前端配置的路由需要手动添加。后端返回的接口也需要添加