【Vue3+Ts项目】硅谷甄选 — 菜单权限+按钮权限

一、菜单权限

1.1 路由拆分 

 将项目路由拆分为:

  • 静态路由:login、404、home、screen
  • 异步路由:权限管理(包含三个子路由)、商品管理(包含四个子路由)
  • 任意路由:任意路由

 src/router/routes.ts

// 对外暴露配置路由(常量路由):全部用户都可以访问到的路由
export const constantRoute = [
  {
    // 登录
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    name: 'login',
    meta: {
      title: '登录', // 菜单标题
      hidden: true, // 代表路由标题在菜单中是否隐藏  true:隐藏  false:显示
      icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标
    },
  },
  {
    // 登录成功以后展示数据的路由
    path: '/',
    component: () => import('@/layout/index.vue'),
    name: 'layout',
    meta: {
      title: '',
      hidden: true,
      icon: '',
    },
    redirect: '/home',
    children: [
      {
        path: '/home',
        component: () => import('@/views/home/index.vue'),
        name: 'home',
        meta: {
          title: '首页',
          hidden: false,
          icon: 'HomeFilled',
        },
      },
    ],
  },
  {
    // 404
    path: '/404',
    component: () => import('@/views/404/index.vue'),
    name: '404',
    meta: {
      title: '404',
      hidden: true,
      icon: 'BrushFilled',
    },
  },
  {
    path: '/screen',
    component: () => import('@/views/screen/index.vue'),
    name: 'Screen',
    meta: {
      title: '数据大屏',
      hidden: false,
      icon: 'Platform',
    },
  }
]

//异步路由
export const asnycRoute = [
  {
    path: '/acl',
    component: () => import('@/layout/index.vue'),
    name: 'Acl',
    meta: {
      title: '权限管理',
      icon: 'Lock',
    },
    redirect: '/acl/user',
    children: [
      {
        path: '/acl/user',
        component: () => import('@/views/acl/user/index.vue'),
        name: 'User',
        meta: {
          title: '用户管理',
          icon: 'User',
        },
      },
      {
        path: '/acl/role',
        component: () => import('@/views/acl/role/index.vue'),
        name: 'Role',
        meta: {
          title: '角色管理',
          icon: 'UserFilled',
        },
      },
      {
        path: '/acl/permission',
        component: () => import('@/views/acl/permission/index.vue'),
        name: 'Permission',
        meta: {
          title: '菜单管理',
          icon: 'Monitor',
        },
      },
    ],
  },
  {
    path: '/product',
    component: () => import('@/layout/index.vue'),
    name: 'Product',
    meta: {
      title: '商品管理',
      icon: 'Goods',
    },
    redirect: '/product/trademark',
    children: [
      {
        path: '/product/trademark',
        component: () => import('@/views/product/trademark/index.vue'),
        name: 'Trademark',
        meta: {
          title: '品牌管理',
          icon: 'ShoppingCartFull',
        },
      },
      {
        path: '/product/attr',
        component: () => import('@/views/product/attr/index.vue'),
        name: 'Attr',
        meta: {
          title: '属性管理',
          icon: 'ChromeFilled',
        },
      },
      {
        path: '/product/spu',
        component: () => import('@/views/product/spu/index.vue'),
        name: 'Spu',
        meta: {
          title: 'SPU管理',
          icon: 'Calendar',
        },
      },
      {
        path: '/product/sku',
        component: () => import('@/views/product/sku/index.vue'),
        name: 'Sku',
        meta: {
          title: 'SKU管理',
          icon: 'Orange',
        },
      },
    ],
  },
]

//任意路由
export const anyRoute = {
  // 任意路由
  path: '/:pathMatch(.*)*',
  redirect: '/404',
  name: 'Any',
  meta: {
    title: '任意路由',
    hidden: true,
    icon: 'Wallet',
  },
}

1.2  菜单权限业务实现 

 PS:退出登录记得删除添加的路由,防止切换角色后还能访问另一个角色的权限

src/store/modules/user.ts

......
// 引入路由(常量路由)
import { constantRoute, asnycRoute, anyRoute } from '@/router/routes'

// 引入深拷贝方法
//@ts-expect-error
import cloneDeep from 'lodash/cloneDeep'
import router from '@/router'
// 用于过滤当前用户需要展示的异步路由
function filterAsyncRoute(asnycRoute: any, routes: any) {
  return asnycRoute.filter((item: any) => {
    if (routes.includes(item.name)) {
      if (item.children && item.children.length > 0) {
        item.children = filterAsyncRoute(item.children, routes)
      }
      return true
    }
  })
}
......

const useUserStore = defineStore('User', {
  // 小仓库存储数据的地方
  state: (): UserState => {
    return {
      ......
      menuRoutes: constantRoute, // 仓库存储生成菜单需要数组(路由)   
      removeRouteCallbackList: []
    }
  },
  // 异步|逻辑的地方
  actions: {
    ......

    // 获取用户信息
    async userInfo() {
      // 获取用户信息进行存储仓库当中(用户头像、名字)
      let result: userInfoResponeData = await reqUserInfo()
      // 如果获取信息成功,存储下用户信息
      if (result.code === 200) {
        this.username = result.data.name
        this.avatar = result.data.avatar
        // 计算当前用户需要展示的异步路由
        const userAsyncRoute = filterAsyncRoute(cloneDeep(asnycRoute),result.data.routes)
        // 菜单需要的数据整理完毕
        this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]
          // 目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加
          ;[...userAsyncRoute, anyRoute].forEach((route: any) => {
            let removeRoute = router.addRoute(route)
            this.removeRouteCallbackList.push(removeRoute)
          })
        return 'ok'
      } else {
        return Promise.reject(new Error(result.message))
      }
    },
    // 退出登录
    async userLogout() {
      let result: any = await reqLogout()
      if (result.code === 200) {
        // 目前没有mock接口:退出登录接口(通知服务器本地用户唯一标识失败)
        this.token = ''
        this.username = ''
        this.avatar = ''
        REMOVE_TOKEN()
        // 退出登录时删除登录添加的路由
        this.removeRouteCallbackList.forEach((removeRouteCallback: any) => {
          removeRouteCallback()
        })
        return 'ok'
      } else {
        return Promise.reject(new Error(result.message))
      }
    },
  },
  getters: {},
})

刷新的时候是异步路由,有可能获取到用户信息,异步路由还没有加载完毕,出现空白的效果!!

解决方法:在全局前置守卫中,获取用户信息后改成next({...to})

  • next() :直接放行(这种写法会导致刷新产生空白的效果)
  • next({...to}):等待路由加载完毕再放行

src/permission.ts

......

try {
          // 获取用户信息
          await useStore.userInfo()
          // 放行
          //等待路由加载完毕再放行
          next({ ...to})
        } catch (error) {
       
        }

.......

 二、按钮权限

1.1 按钮权限业务实现 

1.1.1 用户按钮权限信息存储

src/store/modules/user.ts 

......
state: (): UserState => {
    return {
      ......
      //存储当前用户是否包含某一个按钮
      buttons: [],
    }
......
async userInfo() {
      ......
      // 如果获取信息成功,存储下用户信息
      if (result.code === 200) {
        ......
        this.buttons = result.data.buttons
        ......
      }

 1.1.2 定义全局自定义指令

src/directive/has.ts 

import pinia from "@/store"
import useUserStore from "@/store/modules/user"
const userStore = useUserStore(pinia)
export const isHasButton = (app: any) => {
    // 获取对应的用户仓库
    // 全局自定义指令:实现按钮的权限
    app.directive('has', {
        // 代表使用这个全局指令的DOM|组件挂载完毕的时候会执行一次
        mounted(el: any, options: any) {
            // 自定义指令右侧的数值:如果在用户信息buttons数组中没有
            // 从DOM树上干掉
            if (!userStore.buttons.includes(options.value)) {
                el.parentNode.removeChild(el)
            }
        },
    })
}

 在main.ts文件中引入自定义指令文件

// 引入自定义指令文件
import { isHasButton } from '@/directive/has'
isHasButton(app)

 1.1.3 使用自定义指令配置按钮权限

PS:此处以其中一个按钮作为例子,项目中其他按钮的权限都需要配置

你可能感兴趣的:(前端,javascript,vue.js)