vue权限控制

前端权限控制方向:

1、菜单权限
2、页面权限
3、按钮权限
4、请求和响应控制

一、创建项目

本次核心为vue3+ts+vite驱动
1、node版本要大于16.0
2、npm init vue@latest (基于create-vue创建)
3、安装pinia
注:如下载完依赖后main.ts中import { createApp } from 'vue'报错,关闭项目后重新打开可能会解决
4、安装pinia持久化工具
pnpm install pinia-plugin-persistedstate
main.ts中引入

import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

二、准备静态路由(登录页等)

/router/index.ts

import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import login from '@/views/login/login.vue'
import layout from '@/views/layout.vue'
import notFound from '@/views/404.vue'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/login',
      name: 'login',
      component: login,
    },
    {
      path: '/:catchAll(.*)*',
      name: 'NotFound',
      component: notFound,
    },
    {
      path: '/layout', //将所有返回的二级页面添加在此处
      name: 'layout',
      component: layout,
      children: [],
    },
  ],
})

三、登录后获取页面数据,将路由及用户信息储存在pinia中,跳转至首页

login.vue

<el-button @click="toLogin">登录</el-button>

<script setup lang="ts">
import { mockRoutesData } from '@/data/menuData' //此处模拟接口返回的数据
import { useAuthStore } from '@/stores/menu'
import { useRouter } from 'vue-router'

const router = useRouter()
const authStore = useAuthStore()
let toLogin = async () => {
  //调用getRoutes方法,将后端返回的路由数据添加在缓存中
  await authStore.getRoutes(mockRoutesData)
  authStore.setToken('this is token')
  router.push('/home')
}
</script>

/data/menuData.ts

export const mockRoutesData = [
  {
    id: '1',
    title: '首页',
    name: 'home',
    path: '/home',
    meta: { hidden: false, btn: ['view', 'edit'] },
    component: '/home.vue',
  },
  {
    id: '2',
    title: '用户中心',
    name: 'user',
    path: '/user',
    meta: { hidden: false },
    children: [
      {
        id: '21',
        title: '基本资料',
        name: 'userInfor',
        path: '/user/infor',
        meta: { hidden: false },
        component: '/user/infor.vue',
      },
      {
        id: '22',
        title: '安全设置',
        name: 'userSecure',
        path: '/user/secure',
        meta: { hidden: false },
        component: '/user/secure.vue',
      },
    ],
  },
  {
    id: '3',
    title: '应用中心',
    name: 'application',
    path: '/application',
    meta: { hidden: false },
    children: [
      {
        id: '31',
        title: '我的应用',
        name: 'myApplication',
        path: '/application/myApplication',
        meta: { hidden: false },
        component: '/application/myApplication.vue',
      },
      {
        id: '32',
        title: '安全设置',
        name: 'order',
        path: '/application/order',
        meta: { hidden: false },
        component: '/application/order.vue',
      },
    ],
  },
]

stores/menu.ts

import { ref } from 'vue'
import { defineStore } from 'pinia'
export interface RouteItemType {
  id: string
  name: string
  path: string
  meta: { hidden?: boolean; btn?: string[] }
  children?: RouteItemType[] // 可选子路由
  component?: string
}

export const useAuthStore = defineStore('infor', {
  state: () => ({
    token: ref(''),
    menuRoutes: [] as RouteItemType[], // 新增菜单数据存储
  }),
  actions: {
    setToken(token: string) {
      this.token = token
    },
    getRoutes(mockRoutes: RouteItemType[]) {
      this.menuRoutes = mockRoutes
    },
    logout() {
      this.token = ''
      this.menuRoutes = []
    },
  },
  persist: true, //pinia持久化
})

四、通过路由导航守卫动态添加路由

/router/index.ts

import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useAuthStore, type RouteItemType } from '@/stores/menu'

import login from '@/views/login/login.vue'
import layout from '@/views/layout.vue'
import notFound from '@/views/404.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/login',
      name: 'login',
      component: login,
    },
    {
      path: '/:catchAll(.*)*',
      name: 'NotFound',
      component: notFound,
    },
    {
      path: '/layout',
      name: 'layout',
      component: layout,
      children: [],
    },
  ],
})
//用来标记是否已经添加过路由
let hasAddedRoutes = false

// 添加动态路由的函数
const addDynamicRoutes = (routes: RouteItemType[]) => {
  routes.forEach((route) => {
    // 创建路由配置 - 使用 RouteRecordRaw 类型
    const routeConfig: RouteRecordRaw = {
      path: route.path,
      name: route.name,
      component: () => import(`../views${route.component}` || ''),
      meta: route.meta,
    }
    // 处理嵌套路由 - 使用 RouteRecordRaw 类型
    if (route.children && route.children.length > 0) {
      addDynamicRoutes(route.children)
    }
    // 添加路由
    router.addRoute('layout', routeConfig)
    // router.addRoute(routeConfig)
  })
}
// 路由守卫
router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()
  if (to.path === '/login') {
    next()
    return
  }
  if (!authStore.token) {
    next('/login')
    return
  }
  if (to.path === '/' && authStore.token) {
    next('/home')
    return
  }
  if (!hasAddedRoutes) {
    try {
      // 获取路由数据
      const routes = await authStore.menuRoutes
      // 添加动态路由
      await addDynamicRoutes(routes)
      // 标记已添加路由
      hasAddedRoutes = true
      // 重新导航
      // next({ ...to, replace: true })
      // 解决刷新一直进404页面
      return next({ path: to.path, query: to.query, replace: true })
    } catch (error) {
      console.error('添加路由失败:', error)
      next('/login')
    }
  } else {
    next()
  }
})
export default router

五、layout.vue中添加页面出口

layout.vue

<template>
  <div class="layout">
    <el-container>
      <el-header>
        <h5 class="title">后台管理系统</h5>
        <el-button @click="logout">退出</el-button>
      </el-header>
      <el-container>
        <el-aside width="200px">
          <AsideMenu />
        </el-aside>
        <el-main>
          <router-view></router-view>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>
<script setup lang="ts">
import AsideMenu from '@/components/AsideMenu/AsideMenu.vue'
import { useAuthStore } from '@/stores/menu.ts'

import { useRouter } from 'vue-router'

const router = useRouter()
const logout = () => {
  useAuthStore().logout()
  router.push('/login')
}
</script>
<style>
.layout,
.el-container {
  height: 100%;
}
.el-aside {
  height: 100%;
  background-color: #eee;
}
.el-header {
  background-color: #eee;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
</style>

六、使用ElementPlus制作菜单导航

components/AsideMenu/AsideMenu.vue

<template>
  <el-menu
    v-if="menuItems?.length > 0"
    :default-active="menuActive" //激活菜单项
    :default-openeds="[menuActive]" //刷新后展开原来点击的子菜单
    :unique-opened="uniqueOpened" //保持一个子菜单展开
  >
    <MenuSection v-for="(menu, index) in menuItems" :key="menu.id" :menu="menu" />
  </el-menu>
</template>
<script setup lang="ts">
import { useAuthStore, type RouteItemType } from '@/stores/menu.ts'
import { ref, onMounted, computed } from 'vue'
import MenuSection from './MenuSection.vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const menuItems = ref<RouteItemType[]>([])
const menuActive = computed(() => route.path)
const uniqueOpened = ref(true)
onMounted(async () => {
  try {
    const authStore = await useAuthStore()
    menuItems.value = authStore.menuRoutes //获取路由菜单递归给MenuSection.vue遍历出菜单
  } catch (error) {
    console.error('加载菜单失败:', error)
  }
})
</script>
<style></style>

/MenuSection.vue递归实现菜单

<template>
  <!-- 1、存在子项的使用el-sub-menu   2、不存在子项的使用el-menu-item -->
  <!-- 有子菜单的情况 -->
  <el-sub-menu v-if="menu.children?.length" :index="menu.path">
    <template #title>
      <span>{{ menu.title }}</span>
    </template>
    <MenuSection v-for="child in menu.children" :key="child.path" :menu="child" />
  </el-sub-menu>
<!-- 无子菜单情况 -->
  <el-menu-item v-else :index="menu.path">
    <router-link :to="menu.path">
      <span>{{ menu.title }}</span>
    </router-link>
  </el-menu-item>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
defineProps(['menu'])
</script>
<style scoped>
a {
  width: 100%;
  text-align: left;
}
.el-menu-item.is-active,
.el-menu-item.is-active a {
  color: #10a3c4;
}
</style>

七、按钮权限(自定义指令)

1、自定义v-permission指令

<template>
  <div>home</div>
  <button v-permission="'view'">查看</button>
  <button v-permission="'edit'">编辑</button>
  <button v-permission="'delete'">删除</button>
</template>
<script setup lang="ts"></script>
<style scoped></style>

2、新建utils文件夹用来放置全局方法
src/utils/permission.ts

import router from '@/router'
export const checkPermission = (el: any, binding: any) => {
 //获取当前路由meta中储存的按钮权限
 let routerBtn: string[] = router.currentRoute.value.meta.btn as string[] 
 let currentBtn: string = binding.value
 if (!routerBtn.includes(currentBtn)) {
   el.remove()
 }
}

3、在main.js中调用自定义指令及方法

import { checkPermission } from '@/utils/permission'
const app = createApp(App)
//此处permission与标签中使用的v-permission相同
app.directive('permission', {
  mounted(el, binding) {
    checkPermission(el, binding)
  },
})

image.png
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容