P22-Vue3后台管理系统-权限管理之路由守卫判断⽤户登录状态

P22-Vue3后台管理系统-权限管理之路由守卫判断⽤户登录状态

1.概述

这篇文章介绍通过接口返回的token判断用户是否登录

2.mock接口返回token

2.1.mock调用guid返回token

在permission.js文件中模拟登录接口返回的token,通过调用mock提供的guid方法生成token字符串

在这里插入图片描述

2.2.mockjs官网guid方法

在mockjs官网点击示例,在示例中点击guid跳转到该方法使用说明

在这里插入图片描述

3.配置store管理token

3.1.新建user文件

在store下新建user.js文件管理用户相关的配置。在该配置中管理token

在这里插入图片描述

3.2.index.js导入user.js

import Vue from 'vue'
import Vuex from 'vuex'
import tab from './tab'
import user from './user'

Vue.use(Vuex)
export default new Vuex.Store({
  modules: {
    tab,
    user
  }
})

3.3.user内容

import Cookie from 'js-cookie'
export default {
  state: {
    token: ''
  },
  mutations: {
    // 登录成功添加token
    setToken(state, val) {
      state.token = val
      Cookie.set('token', val)
    },
    // 退出登录清除token
    clearToken(state) {
      state.token = ''
      Cookie.remove('token')
    },
    // 刷新页面防止state数据丢失,重新获取token
    getToken(state) {
      state.token = Cookie.get('token')
    }
  }
}

4.登录和登出配置

4.1.Login组件

配置Login组件在登录后调用setToken方法添加token

在这里插入图片描述

4.2.登出清除token和路由

在CommonHeader组件中为退出绑定登出事件

在这里插入图片描述

5.配置路由守卫

5.1.配置路由拦截器

在main.js文件中配置路由拦截器,判断token是否存在如果不存在则跳转到登录页。

在这里插入图片描述

6.登录和登出效果

在这里插入图片描述

7.优化菜单列表

用户登录成功后,将菜单列表存在tab文件中的tabsList对象中,为了防止刷新页面tabsList数据丢失,将该数据保存到Cookie中,每次刷新时从Cookie中获取菜单列表。

7.1.配置tabsList保存到Cookie

在tab.js文件中配置tabsList保存到Cookie中,在页面刷新时将Cookie数据赋值给tabsList数据对象

在这里插入图片描述

7.2.调用getMenu获取tabsList

在main.js文件中调用getMenu方法从Cookie获取tabsList

在这里插入图片描述

8.路由守卫完整代码

8.1.permission模拟登陆接口数据

import Mock from 'mockjs'
export default {
  getMenu: config => {
    const { username, password } = JSON.parse(config.body)
    console.log(JSON.parse(config.body))
    // 先判断用户是否存在
    if (username === 'admin' || username === 'wp') {
      // 判断账号和密码是否对应
      if (username === 'admin' && password === '123456') {
        return {
          code: 20000,
          data: {
            menu: [
              {
                path: '/',
                name: 'home',
                label: '首页',
                icon: 's-home',
                url: 'Home/Home'
              },
              {
                path: '/video',
                name: 'video',
                label: '视频管理页',
                icon: 'video-play',
                url: 'VideoManage/VideoManage'
              },
              {
                path: '/user',
                name: 'user',
                label: '用户管理页',
                icon: 'user',
                url: 'UserManage/UserManage'
              },
              {
                label: '其他',
                icon: 'location',
                children: [
                  {
                    path: '/page1',
                    name: 'page1',
                    label: '页面1',
                    icon: 'setting',
                    url: 'Other/PageOne'
                  },
                  {
                    path: '/page2',
                    name: 'page2',
                    label: '页面2',
                    icon: 'setting',
                    url: 'Other/PageTwo'
                  }
                ]
              }
            ],
            token: Mock.Random.guid(),
            message: '获取成功'
          }
        }
      } else if (username === 'wp' && password === '123456') {
        return {
          code: 20000,
          data: {
            menu: [
              {
                path: '/',
                name: 'home',
                label: '首页',
                icon: 's-home',
                url: 'Home/Home'
              },
              {
                path: '/video',
                name: 'video',
                label: '视频管理页',
                icon: 'video-play',
                url: 'VideoManage/VideoManage'
              }
            ],
            token: Mock.Random.guid(),
            message: '获取成功'
          }
        }
      } else {
        return {
          code: -999,
          data: {
            message: '密码错误'
          }
        }
      }
    } else {
      return {
        code: -999,
        data: {
          message: '用户不存在'
        }
      }
    }
  }
}

8.2.Login组件

<template>
  <div style="padding: 20px">
    <el-form :model="form" label-width="120">
      <el-form-item label="用户名">
        <el-input v-model="form.username"></el-input>
      </el-form-item>
      <el-form-item label="密码">
        <el-input v-model="form.password" type="password"></el-input>
      </el-form-item>
      <el-form-item align="center">
        <el-button type="primary" @click="login">登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        username: '',
        password: ''
      }
    }
  },
  methods: {
    login() {
      this.$http.post('api/permission/getMenu', this.form).then(res => {
        res = res.data
        // 判断接口返回状态码为20000登录成功
        if (res.code === 20000) {
          //  调用store的tab.js文件中clearMenu方法将登录信息清空。避免二次登录
          this.$store.commit('clearMenu')
          // 调用store的tab.js文件中setMenu方法将接口返回的menu数据传给该方法
          this.$store.commit('setMenu', res.data.menu)
          // 调用store的tab.js文件中addMenu方法,添加动态路由
          this.$store.commit('addMenu', this.$router)
          // 调用store的user.js文件中的setToken方法添加token
          this.$store.commit('setToken', res.data.token)
          // 跳转到首页
          this.$router.push({ name: 'home' })
        } else {
          // 登录失败将接口返回的信息输出
          this.$message.warning(res.data.message)
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.el-form {
  width: 50%;
  margin: auto;
  padding: 45px;
  height: 450px;
  background-color: #fff;
}
</style>

8.3.CommonHeader组件

<template>
  <header>
    <div class="l-content">
      <!-- sezi设置按钮尺寸 -->
      <el-button plain icon="el-icon-menu" size="mini" @click="collapseMenu"></el-button>
      <!-- 设置面包屑 -->
      <el-breadcrumb separator="/">
        <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
        <!-- 通过判断current是否为null设置面包屑是否显示菜单名称 -->
        <el-breadcrumb-item :to="current.path" v-if="current">
          <!-- 从计算属性的current中获取用户点击菜单的label,然后在面包屑中展示该菜单的label -->
          {{ current.label }}
        </el-breadcrumb-item>
      </el-breadcrumb>
    </div>
    <!-- 设置导航栏右边头像 -->
    <div class="r-content">
      <el-dropdown trigger="click">
        <!-- img:用户头像是从data中动态获取,不是写死的。 -->
        <span class="el-dropdown-link"><img :src="userImg" class="user"/></span>
        <el-dropdown-menu slot="dropdown">
          <el-dropdown-item>个人中心</el-dropdown-item>
          <!-- 控件没有提供click属性,调用原生的需要使用native -->
          <el-dropdown-item @click.native="logOut">退出</el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </header>
</template>

<script>
// 导入vuex状态管理模块
import { mapState } from 'vuex'
export default {
  computed: {
    // 获取store下tab.js文件selectMenu方法中currentMenu对象数据
    ...mapState({
      current: state => state.tab.currentMenu
    })
  },
  data() {
    return {
      userImg: require('../assets/images/user.png')
    }
  },
  methods: {
    // 获取用户点击折叠导航栏按钮事件,调用vuex的collapseMenu方法,改变折叠的状态
    collapseMenu() {
      this.$store.commit('collapseMenu')
    },
    // 用户登出
    logOut() {
      // 清除登录token和菜单路由
      this.$store.commit('clearToken')
      this.$store.commit('clearMenu')
      // 刷新浏览器
      location.reload()
    }
  }
}
</script>

<style lang="scss" scoped>
// 设置header为弹性容器
header {
  display: flex;
  // 设置弹性容器中元素水平方向居中
  align-items: center;
  height: 100%;
  // 设置元素两端对齐
  justify-content: space-between;
}
// 设置l-content为弹性容器
.l-content {
  display: flex;
  // 设置弹性容器中元素水平方向居中
  align-items: center;
  // 设置button按钮外右间距
  .el-button {
    margin-right: 20px;
  }
}
.r-content {
  .user {
    // 设置头像宽高
    width: 40px;
    height: 40px;
    // 设置头像为圆形
    border-radius: 50%;
  }
}
</style>
<style lang="scss">
.el-breadcrumb__item {
  // 设置面包屑首页样式
  .el-breadcrumb__inner {
    color: #666;
    font-weight: normal;
  }
  // 设置面包屑首页后面菜单样式
  &:last-child {
    .el-breadcrumb__inner {
      color: #fff;
    }
  }
}
</style>

8.4.tab.js

import Cookie from 'js-cookie'
import router from '../router'
export default {
  state: {
    // 左侧导航栏面板是否折叠
    isCollapse: false,
    // 当前点击导航栏的菜单名称
    currentMenu: null,
    menu: [],
    // 管理tag标签数据
    tabsList: [
      // tag标签默认标签是首页,且不可删除。
      {
        path: '/',
        name: 'home',
        label: '首页',
        icon: 'home'
      }
    ]
  },
  mutations: {
    // 登录后添加路由到menu
    setMenu(state, val) {
      state.menu = val
      // 将val数据进行字符串类型序列化存到cookie中
      Cookie.set('menu', JSON.stringify(val))
      // 输出val
      console.log('vuex', val)
    },
    // 登出后清除menu路由数据和cookie信息
    clearMenu(state) {
      state.menu = []
      Cookie.remove('menu')
    },
    // 登录成功后添加动态路由
    addMenu(state) {
      // 判断cookie中没有menu数据,认为没有登录,不配置动态路由,直接结束该方法。
      if (!Cookie.get('menu')) {
        return
      }
      // 防止刷新造成menu数据丢失,获取cookie中的menu数据,赋值给menu。
      let menu = JSON.parse(Cookie.get('menu'))
      state.menu = menu
      console.log(menu, 'menu')
      // 将menu中的数据添加到router路由中,展示动态路由菜单
      // 拼接路由,将路由数据按照配置的格式存放到currentMenu数组中
      let currentMenu = [
        {
          path: '/',
          component: () => import(`@/views/Main`),
          children: []
        }
      ]
      console.log(currentMenu, 'main_currentMenu')
      // 遍历menu数组取出数据拼接路由
      menu.forEach(item => {
        // 从menu数组中取出包含children字段的数据,拼接路由
        if (item.children) {
          item.children = item.children.map(item => {
            item.component = () => import(`@/views/${item.url}`)
            return item
          })
          currentMenu[0].children.push(...item.children)
        } else {
          item.component = () => import(`@/views/${item.url}`)
          currentMenu[0].children.push(item)
        }
      })
      console.log(currentMenu, 'currentMenu')
      // 将拼接好的路由添加到router中
      router.addRoutes(currentMenu)
    },
    selectMenu(state, val) {
      // 判断当前菜单name是否为home,如果是则将currentMenu属性设置为null,如果不是则将val赋值给currentMenu属性
      // val.name === 'home' ? (state.currentMenu = null) : (state.currentMenu = val)
      if (val.name !== 'home') {
        // 如果点击导航菜单不是home,将用户点击导航菜单对象赋值给currentMenu对象
        state.currentMenu = val
        // 如果点击导航菜单不是home,首先遍历tag标签数组tabsList是否已有val中的name,如果有则不再重复添加,没有则添加val到tag标签数组tabsList
        let result = state.tabsList.findIndex(item => item.name === val.name)
        result === -1 ? state.tabsList.push(val) : ''
        // 将tabsList中菜单数据添加到Cookie中
        Cookie.set('tagList', JSON.stringify(state.tabsList))
      } else {
        state.currentMenu = null
      }
    },
    // 防止页面刷新vuex中保存的菜单丢失,通过Cookie获取登录成功后的权限菜单列表
    getMenu(state) {
      if (Cookie.get('tagList')) {
        let tagList = JSON.parse(Cookie.get('tagList'))
        state.tabsList = tagList
      }
    },
    // 关闭tag标签方法
    closeTab(state, val) {
      // 首先判断val要删除的tag标签在tabsList数组中是否存在,如果存在则删除
      let result = state.tabsList.findIndex(item => item.name === val.name)
      state.tabsList.splice(result, 1)
    },
    // 控制左侧导航栏是否折叠
    collapseMenu(state) {
      state.isCollapse = !state.isCollapse
    }
  },
  actions: {},
  modules: {}
}

8.5.main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 全局配置
import '@/assets/scss/reset.scss'
// 导入axios
import http from '@/api/config'
// 导入mock
import './mock'

// 第三方包
// 导入Element组件
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

// 挂载Element组件到Vue实例
Vue.use(ElementUI)
// 挂载axios到Vue实例
Vue.prototype.$http = http

Vue.config.productionTip = false

// 配置路由拦截器,判断是否存在token,不存在则跳转到登录页
router.beforeEach((to, from, next) => {
  // 防止刷新页面后vuex里丢失token,获取token
  store.commit('getToken')
  // 防止刷新后vuex里丢失标签列表tagList
  store.commit('getMenu')
  // 判断token跳转登录页
  let token = store.state.user.token
  // 判断是否存在token,和当前页面是否登录页,如果token不存且当前页不是login在则跳转到login页
  if (!token && to.name !== 'login') {
    next({ name: 'login' })
  } else {
    next()
  }
})

new Vue({
  router,
  store,
  render: h => h(App),
  // 刷新界面重新构建vue时候动态添加路由方法
  created() {
    store.commit('addMenu', router)
  }
}).$mount('#app')

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值