后台管理系统权限处理

文章介绍了如何在Vue应用中实现根据用户角色的菜单和按钮权限控制,涉及静态路由、异步路由配置,以及使用路由守卫进行权限验证和处理异步路由加载问题。同时提到通过自定义指令检查按钮权限并动态显示或隐藏元素。
摘要由CSDN通过智能技术生成

一、菜单权限:

根据登录角色的不同,用户拥有的菜单权限、按钮权限不同;
其中,超级管理员具有所有的菜单、按钮权限,其他角色不定。

通常后台系统包括的路由有:

  • login(登录页面)
  • 404(404页面)
  • 任意路由(匹配不到的路由页面)
  • home(首页)
    (具体的功能页:)
  • 数据大屏
  • 权限管理(包含子路由)
  • 商品管理(包含子路由)
实现步骤:
  • 拆分路由(见 routes.ts )
    • 静态(常量)路由:所有用户都拥有的路由,通常包括 login、404
    • 异步路由:根据角色划分,不同角色拥有不同的路由
    • 任意路由:匹配不到以上路由时,进行重定向的路由

index.ts

// 引入创建路由实例的方法,和创建 hash(createWebHashHistory) / history(createWebHistory) 模式的方法
import { createRouter, createWebHashHistory } from "vue-router"

import { constantRoute } from "./routes.ts"
let router = createRouter({
  // 配置路由模式:hash
  history: createWebHashHistory(),
  // 配置路由
  routes: constantRoute,
  //路由切换时,页面滚动到顶部左侧
  scrollBehavior() {
    return {
      top: 0,
      left: 0
    }
  }
});
export default router;

routes.ts

//静态路由(所有用户都可以访问的路由:登录、首页、数据大屏、404)
export const constantRoute = [
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/login/index.vue"),
    meta: {
      title: "登录",  // 菜单标题
      hidden: true, //是否隐藏菜单
      icon: "Notebook"  //菜单图标
    }
  },
  {
    path: "/",
    name: "Layout",
    component: () => import("@/layout/index.vue"),
    redirect: "/home",
    meta: {
      title: "",
      icon: ""
    },
    children: [
      {
        path: "/home",
        name: "Home",
        component: () => import("@/views/home/index.vue"),
        meta: {
          title: "首页",
          icon: "HomeFilled"
        }
      }
    ]
  },
  {
    path: "/404",
    name: "404",
    component: () => import("@/views/404/index.vue"),
    meta: {
      title: "404",
      hidden: true,
      icon: "CirclePlusFilled"
    }
  },
  {
    path: "/screen",
    name: "Screen",
    component: () => import("@/views/screen/index.vue"),
    meta: {
      title: "数据大屏",
      icon: "Platform"
    }
  },
]

// 异步路由(需要根据用户角色过滤的路由:权限管理、商品管理)
export const asyncRoute = [
  {
    path: "/acl",
    name: "Acl",
    component: () => import("@/layout/index.vue"),
    meta: {
      title: "权限管理",
      icon: "Lock"
    },
    redirect: "/acl/user",
    children: [
      {
        path: "/acl/user",
        name: "User",
        component: () => import("@/views/acl/user/index.vue"),
        meta: {
          title: "用户管理",
          icon: "User"
        }
      },
      {
        path: "/acl/role",
        name: "Role",
        component: () => import("@/views/acl/role/index.vue"),
        meta: {
          title: "角色管理",
          icon: "UserFilled"
        }
      },
      {
        path: "/acl/permission",
        name: "Permission",
        component: () => import("@/views/acl/permission/index.vue"),
        meta: {
          title: "菜单管理",
          icon: "Grid"
        }
      }
    ]
  },
  {
    path: "/product",
    name: "Product",
    component: () => import("@/layout/index.vue"),
    meta: {
      title: "商品管理",
      icon: "Goods"
    },
    redirect: "/product/trademark",
    children: [
      {
        path: "/product/trademark",
        name: "Trademark",
        component: () => import("@/views/product/trademark/index.vue"),
        meta: {
          title: "品牌管理",
          icon: "ShoppingTrolley"
        }
      },
      {
        path: "/product/attr",
        name: "Attr",
        component: () => import("@/views/product/attr/index.vue"),
        meta: {
          title: "属性管理",
          icon: "ChatLineSquare"
        }
      },
      {
        path: "/product/spu",
        name: "Spu",
        component: () => import("@/views/product/spu/index.vue"),
        meta: {
          title: "SPU管理",
          icon: "CollectionTag"
        }
      },
      {
        path: "/product/sku",
        name: "Sku",
        component: () => import("@/views/product/sku/index.vue"),
        meta: {
          title: "SKU管理",
          icon: "Shop"
        }
      }
    ]
  }
]

// 任意路由(匹配不上静态路由和异步路由时,跳转到404)
export const anyRoute = [
  {
    // 路由都匹配不上时,匹配这个任意路由,重定向到 404
    path: "/:pathMatch(.*)*",
    redirect: "/404",
    name: "Any",
    meta: {
      title: "其他路由",
      hidden: true,
      icon: "Open"
    }
  }
]

permission.ts

// 要在main.ts中引入该文件,在切换路由时才会运行
//引入路由全局守卫
// import "@/router/permission"

/**
 * 路由里面需处理以下3个问题
 * 1、任意路由切换实现进度条业务 --nprogress
 * 2、路由鉴权(路由组件访问权限的设置)
 *    全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
 *    路由鉴权份两种情况(用户是否登录,可以通过 token 判断):
 *        1)用户未登录:可以访问登录路由(/login),其余路由不能访问
 *        2)用户登录成功:除了登录路由,其余路由都可访问
 */

// 引入路由
import router from "@/router";
// 引入页面进度条方法及样式
import NProgress from "nprogress"
NProgress.configure({ showSpinner: false });
import 'nprogress/nprogress.css'

// 获取用户的store信息,里面包含 token、username
import useUserStore from "@/store/modules/user";

// 配置路由全局前置、后置守卫
router.beforeEach(async (to, from, next) => {
  let userStore = useUserStore()
  // to:要跳转到的页面
  // from:从哪个页面来
  // next:是否放行
  NProgress.start();
  let token = userStore.token;
  let username = userStore.username;
  if(token) {
    //如果存在 token,表示用户已登录

    //登录状态下,如果访问 login 页面,重定向到 home 页面
    if(to.path === "/login") {
      next({ path: "/home" });
    } else {
      // 登录状态下访问其他页面时
      // 如果存在 username,那么路由正常跳转
      if(username) {
        next();
      } else {
        // 如果没有 usernam,那么先获取用户信息,成功后路由再继续跳转
        try {
          await userStore.userInfo()
          // console.log(to)
          // userStore.userInfo() 方法里面会进行异步路由处理,异步路由页面刷新时,
          // 有可能获取到用户信息,异步路由还没加载完毕,可能会出现空白效果
          // next 方法里面加 {...to} 是保证异步路由加载完成以后进行页面路由跳转
          next({...to});
        } catch(error) {
          // 如果获取用户信息失败,那么退出登录,清除用户信息,跳转到登录页面
          // 获取用户信息失败的原因:1、token 失效,2、用户手动修改本地 token
          await userStore.logout();
          next({ path: "/login" });
        }
      }
    }
    
  } else {
    //如果没有token表示用户未登录,未登录状态只能访问 login 页面
    if(to.path === "/login") {
      next();
    } else {
      // 如果访问其他页面,因为没有token重定向了登录页面,标记 redirect 属性,登录成功以后,跳到原页面
      next({ path: "/login", query: { redirect: to.path } })
    }
  }


  
})
router.afterEach((to, from, next) => {
  NProgress.done();
})

  • 用户登录成功以后,根据接口返回的路由权限,匹配对应路由,注册路由
    user.ts:
// 引入路由
import { constantRoute, asyncRoute, anyRoute } from "@/router/routes"
import router from "@/router"
// 深复制
import { cloneDeep } from "lodash";

const filterAsyncRouter = (asyncRoute:any, routes:any ) => {
  return asyncRoute.filter((item:any) => {
    if(routes.includes(item.name)) {
      if(item.children && item.children.length > 0) {
        item.children = filterAsyncRouter(item.children, routes)
      }
      return true;
    }
  })
}

async userInfo() {
      let result:userResponseData = await reqUserInfo();
      if(result.code === 200) {
        this.username = result.data.name;
        this.avatar = result.data.avatar;

        // 获取用户信息成功后,处理异步路由
        let userAsyncRoute = filterAsyncRouter(cloneDeep(asyncRoute), result.data.routes);
        this.menuRouters = [...constantRoute, ...userAsyncRoute, ...anyRoute];
        [...userAsyncRoute, ...anyRoute].forEach((route) => {
          router.addRoute(route)
        })
        // 获取注册过的所有路由
        console.log(router.getRoutes())
        return "ok"
      } else {
        return Promise.reject("获取用户信息失败!")
      }
    },

可能遇到的问题(假设异步路由有:1,2,3):
1、切换不同角色的用户如,从 A(拥有1、3) 切换到 B (拥有 1、2、3),因为 user.ts 在处理异步路由时,是用 item.children = filterAsyncRouter(item.children, routes) 会改变 asyncRoute ,会导致B没有 2 的权限,所以要用深复制 deepClone
2、用户登录时,如果要重定向到异步路由页面,可能会出现白屏,是因为登录成功获取用户信息后,异步路由可能还没加载完毕。所以跳转时要用 next({…to});
3、切换用户时可能会保留上一个用户的路由,所以要在退出登录时删除一下路由

二、按钮权限

  • 登录成功后接口返回用户拥有的按钮权限数据buttons= ["Uer.add","Uer.edit"],存在缓存中
  • 注册自定义指令(在节点挂载后判断 指令绑定的权限值,是否存在于 buttons 中,存在即为有权限:保留节点,否则没有权限:删除节点)
  • main.ts 中引入指令,并使用

main.js

import { createApp } from 'vue'
import App from '@/App.vue'
let app = createApp(App)
import { isHasButton } from "./directive/has"
isHasButton(app);
app.mount('#app')

directive/has

import useUserStore from "@/store/modules/user"
export const isHasButton = (app:any) => {
  // 自定义指令,判断按钮是否有操作权限
  app.directive('has', {
    // 使用这个指令的dom在挂载时会执行一次
    mounted(el:any,options:any) {
      // 在所有权限按钮中匹配不到指定绑定的权限时,将元素删除
      if(!useUserStore().buttons.includes(options.value)) {
        // el.parentElement.removeChild(el)
        el.remove()
      }
    },
  })
}

index.vue

<el-button v-has="'btn.Spu.add'" type="primary" icon="Plus" :disabled="categoryStore.c3Id?false:true"
          @click="addOrEditSpu('add')">添加SPU</el-button>
  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值