permission.js(全局路由守卫和登录判断)详细讲解:

import router from './router'
import store from './store'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isHttp } from '@/utils/validate'
import { isRelogin } from '@/utils/request'

NProgress.configure({ showSpinner: false });

// 接口白名单,不需要登录即可访问
const whiteList = ['/login', '/auth-redirect', '/bind', '/register'];

// 路由守卫,在路由跳转前进行权限判断(三个参数分别为: 路由信息, 跳转的路由, 跳转的方法)
router.beforeEach((to, from, next) => {
  // 请求路由时进度条开始
  NProgress.start()
  // 判断是否有token
  if (getToken()) {
    // 设置页面标题
    to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
    /* has token*/
    // 如果用户存在令牌的情况请求登录页面,避免重复登录,直接跳转到首页
    // 判断是否为登录页面
    if (to.path === '/login') {
      // 直接跳转到首页,当然取决于你的路由重定向到哪里
      next({ path: '/' })
      // 一定要结束进度条
      NProgress.done()
    } else {
      // 判断是否已经获取用户信息
      if (store.getters.roles.length === 0) {
        // length为0,则为未获取用户信息,需要重新获取
        isRelogin.show = true // 重新登录弹窗
        // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(() => {
          isRelogin.show = false
          // 从vuex中触发GenerateRoutes方法,获取后台返回的路由信息
          store.dispatch('GenerateRoutes').then(accessRoutes => {
            // 根据roles权限生成可访问的路由表
            accessRoutes.forEach(route => {
              if (!isHttp(route.path)) {
                router.addRoute(route) // 动态添加可访问路由表
              }
            })
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch(err => {
          // 获取用户信息失败,清除token,跳转到登录页面重新登录
          store.dispatch('LogOut').then(() => {
            ElMessage.error(err)
            next({ path: '/' })
          })
        })
      } else {
        next()
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  // 每次请求结束后,进度条结束
  NProgress.done()
})

request.js:

import axios from 'axios'
import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import store from '@/store'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from '@/utils/dtcloud'
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'

let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };

// 设置默认请求头
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: import.meta.env.VITE_APP_BASE_API,
  // 超时
  timeout: 10000
})

// request拦截器
service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  // 是否需要防止数据重复提交
  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    // 拼接参数
    let url = config.url + '?' + tansParams(config.params);
    url = url.slice(0, -1);// 去掉最后一个&
    config.params = {};// 清空参数
    config.url = url;// 重新赋值
  }
  if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {// 防止数据重复提交
    const requestObj = {// 用于存储请求的对象
      url: config.url,// 请求地址
      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,// 请求参数
      time: new Date().getTime()// 请求时间
    }
    const sessionObj = cache.session.getJSON('sessionObj')// 获取sessionStorage中的请求对象
    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {// 判断sessionStorage中是否有请求对象
      cache.session.setJSON('sessionObj', requestObj)// 将请求对象存入sessionStorage
    } else {
      const s_url = sessionObj.url;                // 请求地址
      const s_data = sessionObj.data;              // 请求数据
      const s_time = sessionObj.time;              // 请求时间
      const interval = 1000;                       // 间隔时间(ms),小于此时间视为重复提交
      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {// 判断请求是否重复
        const message = '数据正在处理,请勿重复提交';
        console.warn(`[${s_url}]: ` + message)// 打印警告信息
        return Promise.reject(new Error(message))// 抛出异常
      } else {
        cache.session.setJSON('sessionObj', requestObj)// 将请求对象存入sessionStorage
      }
    }
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(res => {// 响应成功关闭loading
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    // 二进制数据则直接返回
    if(res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer'){
      return res.data
    }
    if (code === 401) {
      if (!isRelogin.show) {
        isRelogin.show = true;// 防止重复弹出
        ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).then(() => {
        isRelogin.show = false;
        store.dispatch('LogOut').then(() => {// 为了重新实例化vue-router对象 避免bug
          location.href = '/index';// 跳转到登录页
        })
      }).catch(() => {
        isRelogin.show = false;
      });
    }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
      ElMessage({
        message: msg,
        type: 'error'
      })
      return Promise.reject(new Error(msg))// 抛出异常
    } else if (code !== 200) {
      ElNotification.error({
        title: msg
      })
      return Promise.reject('error')
    } else {
      return  Promise.resolve(res.data)
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {// 网络错误
      message = "后端接口连接异常";
    }
    else if (message.includes("timeout")) {// 请求超时
      message = "系统接口请求超时";
    }
    else if (message.includes("Request failed with status code")) {// 请求失败
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    ElMessage({
      message: message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

// 通用下载方法
// url: 下载地址
// params: 参数
// fileName: 文件名称
export function download(url, params, filename) {
  downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })// 打开loading
  return service.post(url, params, {// 发送请求
    transformRequest: [(params) => { return tansParams(params) }],// 参数转换
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },// 设置请求头
    responseType: 'blob'// 设置响应类型
  }).then(async (data) => {
    // isLogin(data);// 判断是否登录
    const isLogin = await blobValidate(data);
    if (isLogin) {// 已登录
      const blob = new Blob([data])// 创建blob对象
      saveAs(blob, filename)// 保存文件
    } else {// 未登录
      const resText = await data.text();// 获取响应文本
      const rspObj = JSON.parse(resText);// 转换为对象
      const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']// 获取错误信息
      ElMessage.error(errMsg);// 提示错误信息
    }
    downloadLoadingInstance.close();// 关闭loading
  }).catch((r) => {
    console.error(r)// 打印错误信息
    ElMessage.error('下载文件出现错误,请联系管理员!')
    downloadLoadingInstance.close();// 关闭loading
  })
}

export default service// 导出axios实例

store=>modules=>permission.js (后台返回路由处理拼接):

import { constantRoutes } from '@/router'
import { getRouters,getLocalRouters } from '@/api/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink'

// 匹配views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue')

const permission = {
  state: {
    routes: [],// 所有路由
    addRoutes: [],// 有权限的路由
    defaultRoutes: [],// 默认路由
    topbarRouters: [],// 顶部菜单
    sidebarRouters: []// 侧边栏菜单
  },
  mutations: {
    SET_ROUTES: (state, routes) => {// 设置路由
      state.addRoutes = routes// 有权限的路由
      state.routes = constantRoutes.concat(routes)// 所有路由
    },
    SET_DEFAULT_ROUTES: (state, routes) => {// 设置默认路由
      state.defaultRoutes = constantRoutes.concat(routes)// 默认路由
    },
    SET_TOPBAR_ROUTES: (state, routes) => {// 设置顶部菜单
      state.topbarRouters = routes// 顶部菜单
    },
    SET_SIDEBAR_ROUTERS: (state, routes) => {// 设置侧边栏菜单
      state.sidebarRouters = routes// 侧边栏菜单
    },
  },
  actions: {
    // 生成路由
    GenerateRoutes({ commit }) {
      return new Promise(resolve => {// 获取后台路由
        // 向后端请求路由数据
        getRouters().then(res => {
          const sdata = JSON.parse(JSON.stringify(res.data))// 深拷贝
          const rdata = JSON.parse(JSON.stringify(res.data))// 深拷贝
          const defaultData = JSON.parse(JSON.stringify(res.data))// 深拷贝
          const sidebarRoutes = filterAsyncRouter(sdata)// 过滤路由
          const rewriteRoutes = filterAsyncRouter(rdata, false, true)// 过滤路由
          const defaultRoutes = filterAsyncRouter(defaultData)// 过滤路由
          commit('SET_ROUTES', rewriteRoutes)// 设置路由
          commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))// 设置侧边栏菜单
          commit('SET_DEFAULT_ROUTES', sidebarRoutes)// 设置默认路由
          commit('SET_TOPBAR_ROUTES', defaultRoutes)// 设置顶部菜单
          resolve(rewriteRoutes)// 返回有权限的路由
        })
      })
    }
  }
}

// 遍历后台传来的路由字符串,转换为组件对象
// 递归过滤异步路由表,返回符合用户角色权限的路由表
// 三个参数分别为:后台返回的路由数据,是否是侧边栏菜单,是否是顶部菜单
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {// type为true时,不过滤component
  return asyncRouterMap.filter(route => {// 遍历后台传来的路由字符串,转换为组件对象
    if (type && route.children) {// 顶部菜单
      route.children = filterChildren(route.children)// 过滤子路由
    }
    if (route.component) {// 如果路由设置了组件
      // Layout ParentView 组件特殊处理
      if (route.component === 'Layout') {// Layout组件特殊处理
        route.component = Layout// Layout组件
      } else if (route.component === 'ParentView') {// ParentView组件特殊处理
        route.component = ParentView// ParentView组件
      } else if (route.component === 'InnerLink') {// InnerLink组件特殊处理
        route.component = InnerLink// InnerLink组件
      } else {
        route.component = loadView(route.component)// 路由组件
      }
    }
    if (route.children != null && route.children && route.children.length) {// 如果有子路由,递归
      route.children = filterAsyncRouter(route.children, route, type)// 递归过滤异步路由表,返回符合用户角色权限的路由表
    } else {
      delete route['children']// 删除子路由
      delete route['redirect']// 删除重定向
    }
    return true
  })
}

// 过滤子路由
// 三个参数分别为:后台返回的路由数据,是否是侧边栏菜单,是否是顶部菜单
function filterChildren(childrenMap, lastRouter = false) {
  var children = []// 子路由
  childrenMap.forEach((el, index) => {// 遍历后台传来的路由字符串,转换为组件对象
    if (el.children && el.children.length) {// 如果有子路由,递归
      if (el.component === 'ParentView' && !lastRouter) {// ParentView组件特殊处理
        el.children.forEach(c => {// 遍历子路由
          c.path = el.path + '/' + c.path// 拼接路径
          if (c.children && c.children.length) {// 如果有子路由,递归
            children = children.concat(filterChildren(c.children, c))// 递归过滤异步路由表,返回符合用户角色权限的路由表
            return
          }
          children.push(c)// 子路由
        })
        return
      }
    }
    if (lastRouter) {// 如果有子路由,递归
      el.path = lastRouter.path + '/' + el.path// 拼接路径
    }
    children = children.concat(el)// 子路由
  })
  return children// 子路由
}

export const loadView = (view) => {// 路由懒加载
  let res;
  for (const path in modules) {// 遍历路由
    const dir = path.split('views/')[1].split('.vue')[0];// 路由路径
    if (dir === view) {// 如果路由路径和传入的路由路径相同
      res = () => modules[path]();// 路由懒加载
    }
  }
  return res;
}

export default permission

vite.config.js :

import { defineConfig, loadEnv } from 'vite'
import path from 'path'
import createVitePlugins from './vite/plugins'

// https://vitejs.dev/config/
//参数:mode:当前运行的模式,可选值为:development、production、test
//参数:command:当前运行的命令,可选值为:serve、build
export default defineConfig(({ mode, command }) => {
  const env = loadEnv(mode, process.cwd())// 加载环境变量
  const { VITE_APP_ENV } = env// 获取环境变量
  return {
    // 部署生产环境和开发环境下的URL。
    // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
    base: VITE_APP_ENV === 'production' ? '/' : '/',// 根据不同的环境设置不同的base
    // 指定构建输出目录
    plugins: createVitePlugins(env, command === 'build'),
    resolve: {
      // https://cn.vitejs.dev/config/#resolve-alias
      alias: {// 设置路径别名
        // 设置路径
        '~': path.resolve(__dirname, './'),
        // 设置别名
        '@': path.resolve(__dirname, './src')
      },
      // https://cn.vitejs.dev/config/#resolve-extensions
      // 设置导入文件的扩展名
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
    },
    // vite 相关配置
    server: {// 服务配置
      port: 80,// 端口
      host: true,// 是否自动打开浏览器
      open: true,// 是否自动打开浏览器
      proxy: {// 代理
        // https://cn.vitejs.dev/config/#server-proxy
        '/licenseUrl': {// 代理路径
          target: 'http://192.168.5.252:9110',// 代理目标地址
          changeOrigin: true,// 是否跨域
          rewrite: (p) => p.replace(/^\/licenseUrl/, '')// 重写路径
        }
      }
    },
    //fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the file
    css: {// css 配置
      postcss: {// postcss 配置
        plugins: [// 插件
          {
            postcssPlugin: 'internal:charset-removal',// 插件名称
            AtRule: {// at 规则
              charset: (atRule) => {// 字符集
                if (atRule.name === 'charset') {// 如果是字符集
                  atRule.remove();// 移除
                }
              }
            }
          }
        ]
      }
    }
  }
})

Logo

快速构建 Web 应用程序

更多推荐