Vue实战之从零搭建Vite2+Vue3全家桶(五)

1,537 阅读4分钟

前言

本篇主要介绍用户登录及权限管理,对于后台管理来说,用户鉴权和权限管理是非常核心的一个功能模块,所以在撸具体业务功能的代码前,我们先撸一套比较简单的权限管理功能出来。

上一篇传送门

Vue实战之从零搭建Vite2+Vue3全家桶(四)

Vue实战之从零搭建Vite2+Vue3全家桶(三)

Vue实战之从零搭建Vite2+Vue3全家桶(二)

Vue实战之从零搭建Vite2+Vue3全家桶(一)

实现思路

  • 用户登录:用户输入账号密码后请求登录接口,服务端验证账号密码是否正常,验证通过后,返回token。

  • 用户信息:前端在根据token去调用接口获取用户信息(用户基本信息、权限)。

  • 菜单权限:根据后台拿到的用户权限信息,通过 router.addRoute 动态渲染菜单路由。

  • 按钮权限:根据后台拿到的用户权限信息,通过自定义指令动态渲染按钮。

用户登录

此处模拟实现一个基础登录功能,账号+密码登录,输入账号密码后点击登录按钮,前端校验账号密码输入内容是否合法,然后提交服务端进行登录验证。 image.png

点击展开源码>>>

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概不负责9A2D13A1A6167C33F8E2BACCB3F107E9.png 9A2D13A1A6167C33F8E2BACCB3F107E9.png
vue3-vite2-element-template

往期传送门

Vue实战之从零搭建Vite2+Vue3全家桶(四)

Vue实战之从零搭建Vite2+Vue3全家桶(三)

Vue实战之从零搭建Vite2+Vue3全家桶(二)

Vue实战之从零搭建Vite2+Vue3全家桶(一)

基于Vue的架构设计