前言
本篇主要介绍用户登录及权限管理,对于后台管理来说,用户鉴权和权限管理是非常核心的一个功能模块,所以在撸具体业务功能的代码前,我们先撸一套比较简单的权限管理功能出来。
上一篇传送门
实现思路
-
用户登录:用户输入账号密码后请求登录接口,服务端验证账号密码是否正常,验证通过后,返回token。
-
用户信息:前端在根据token去调用接口获取用户信息(用户基本信息、权限)。
-
菜单权限:根据后台拿到的用户权限信息,通过 router.addRoute 动态渲染菜单路由。
-
按钮权限:根据后台拿到的用户权限信息,通过自定义指令动态渲染按钮。
用户登录
此处模拟实现一个基础登录功能,账号+密码登录,输入账号密码后点击登录按钮,前端校验账号密码输入内容是否合法,然后提交服务端进行登录验证。
点击展开源码>>>
Login.vue
<template>
<div class="login-container">
<el-form
ref="loginForm"
:model="state.loginForm"
:rules="state.loginRules"
class="login-form"
auto-complete="on"
label-position="left"
>
<div class="title-container">
<h3 class="title">登录</h3>
</div>
<el-form-item prop="username">
<span class="svg-container">
<svg-icon name="user" />
</span>
<el-input
v-model="state.loginForm.username"
placeholder="Username"
name="username"
type="text"
tabindex="1"
auto-complete="on"
/>
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon name="password" icon-class="password" />
</span>
<el-input
v-model="state.loginForm.password"
placeholder="Password"
tabindex="2"
auto-complete="on"
show-password
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-button
:loading="loading"
type="primary"
style="width: 100%; margin-bottom: 30px"
@click="handleLogin"
>登录</el-button
>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
const router = useRouter()
const state = reactive({
loginForm: {
username: '',
password: ''
},
loginRules: {
username: [
{
required: true,
trigger: 'blur',
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请输入登录账号'))
} else {
callback()
}
}
}
],
password: [
{
required: true,
trigger: 'blur',
validator: (rule, value, callback) => {
if (value.trim() === '') {
callback(new Error('请输入用户密码'))
} else {
callback()
}
}
}
]
}
})
const store = useStore()
const loginForm = ref(null)
const loading = ref(false)
function handleLogin() {
loginForm.value.validate((valid) => {
if (valid) {
loading.value = true
store
.dispatch('user/login', state.loginForm)
.then(() => {
loading.value = false
router.push({ path: '/' })
})
.catch(() => {
loading.value = false
ElMessage.error('用户名或密码错误')
})
}
})
}
</script>
action
login({ commit, dispatch }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo)
.then((response) => {
const { data } = response
commit('SET_TOKEN', data.token) // 存储用户token
resolve()
})
.catch((error) => {
reject(error)
})
})
}
用户信息
在用户通过登录验证后,调用接口获取用户基本信息以及用户权限等。
- 通过路由导航守卫
router.beforeEach
来拦截路由 - 判断token是否有效
- 判断当前路由是否为非登录页面
- 判断是否已获取用户信息
const whiteList = ['/login']
router.beforeEach((to, form, next) => {
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 已登录访问Login跳转首页
next({ path: '/' })
NProgress.done()
} else {
try {
// 判断是否已获取用户信息
if (store.getters.userInfo.userName) {
next()
} else {
// 调用获取用户信息接口获取用户信息
store.dispatch('user/getUserInfo').then(() => {
next({ path: '/', replace: true })
})
}
} catch (error) {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
} else if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
})
菜单权限
本段只描述动态路由具体实现逻辑,不涉及具体的菜单展示组件开发;通过路由导航守卫router.beforeEach
来拦截路由进行权限控制,通过 router.addRoute
动态渲染菜单路由。
- 判断token是否有效
- 判断当前路由是否为非登录页面
- 判断是否已获取用户信息
- 判断路由数据是否已加载动态路由
- 递归遍历动态渲染菜单路由
const whiteList = ['/login']
const modules = import.meta.glob('../**/**/**.vue')
// 递归遍历接口返回的路由数据,动态渲染成菜单路由
const readNodes = (nodes = [], routerInfo, parent) => {
nodes.forEach((res) => {
let name = ''
res.component = modules[`../${res.componentPath}.vue`]
if (parent) {
routerInfo.addRoute(parent, res)
} else {
name = res.name
routerInfo.addRoute(res)
}
if (res.children && res.children.length) {
readNodes(res.children, router, name)
}
})
}
router.beforeEach((to, form, next) => {
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 已登录访问Login跳转首页
next({ path: '/' })
NProgress.done()
} else {
try {
// 判断是否已获取用户信息
if (store.getters.userInfo.userName) {
// router.defaultRouters 为router初始化过程中自定义变量,主要存储默认路由数据
if (router.defaultRouters.length === router.getRoutes().length) { // 判断默认路由数据
是否等于router中所有路由数据(解决如刷新等引起的路由记录丢失)
// 调用获取用户信息接口获取用户路由信息
store.dispatch('user/getUserInfo').then(() => {
// 递归遍历接口返回的路由数据,动态渲染成菜单路由
readNodes(store.getters.role.menulist, router)
next({ path: '/', replace: true })
})
} else {
next()
}
} else {
// 调用获取用户信息接口获取用户路由信息
store.dispatch('user/getUserInfo').then(() => {
// 递归遍历接口返回的路由数据,动态渲染成菜单路由
readNodes(store.getters.role.menulist, router)
next({ path: '/', replace: true })
})
}
} catch (error) {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
} else if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
})
按钮权限
我们需要根据实际需求来做按钮权限控制。
- 应用场景不多的情况下,通常可以在前端用v-if手动判断来控制按钮权限。
- 大多数页面普遍需要控制按钮权限的情况下,我们可以封装一个自定义指令来做权限控制。
权限数据demo
{
path: '/order/list',
name: 'orderList',
meta: {
title: '订单列表',
icon: 'order',
auth: ['create', 'edit']
},
component: 'views/order/OrderList'
}
封装自定义指令
// hasPermission.js
import route from '@/router/index'
export const hasPermission = (el, binding) => {
//判断按钮是否在当前路由拥有的按钮权限列表范围内
if (binding?.value && route.currentRoute.value?.meta?.auth) {
if (!route.currentRoute.value.meta.auth.includes(binding.value)) {
el.remove()
}
}
}
export default hasPermission
封装自定义指令初始化文件,新建directive/index.js
import hasPermission from './hasPermission'
export default {
install(app) {
app.directive('has', {
mounted: (el, binding) => {
hasPermission(el, binding)
}
})
}
}
修改main.js
import directive from './directive'
const app = createApp(App)
app.use(directive)
vue组建中使用指令
<el-button @click="addOrder" v-has="'create'">添加</el-button>
此处只是提供一个大概的权限控制实现思路,可以根据每个公司实际情况来进行优化完善;
等空闲时间在整理出完整的系列代码放到git上。
Git地址
通过此git链接可以查看本系列完整的功能代码,勉强开箱即用,如有BUG概不负责
。
vue3-vite2-element-template