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')