欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

vue中動態(tài)權限控制的實現(xiàn)示例

 更新時間:2025年08月24日 11:22:54   作者:百錦再@新空間  
動態(tài)權限控制通過后端獲取用戶權限數(shù)據(jù),前端動態(tài)生成路由菜單及按鈕權限,結合路由守衛(wèi)與自定義指令實現(xiàn)訪問控制,具有一定的參考價值,感興趣的可以了解一下

一、核心原理與流程總覽

動態(tài)權限控制的本質是:用戶登錄后,從后端獲取其權限數(shù)據(jù),前端根據(jù)此數(shù)據(jù)動態(tài)地構建出只屬于該用戶的可訪問路由和菜單,并在視圖層面(按鈕)進行權限控制。

整個流程可以分為以下幾個核心步驟,下圖清晰地展示了其工作原理和閉環(huán)流程:

flowchart TD
    A[用戶登錄] --> B[獲取用戶權限數(shù)據(jù)JSON]
    B -- 解析為前端所需結構 --> C[生成動態(tài)路由]
    C -- addRoute添加到路由器 --> D[路由器Router]
    D -- 根據(jù)當前路由生成 --> E[側邊欄菜單
(動態(tài)菜單組件)]
    
    E -- 點擊菜單項觸發(fā)路由切換 --> D
    
    F[訪問路由] --> G{路由守衛(wèi)檢查權限}
    G -- 有權限 --> H[正常渲染組件]
    G -- 無權限 --> I[跳轉404或登錄頁]
    
    H -- 組件內按鈕 --> J{按鈕權限指令v-permission}
    J -- 權限碼匹配 --> K[顯示按鈕]
    J -- 權限碼不匹配 --> L[移除按鈕DOM]

下面,我們將按照這個流程中的每一個環(huán)節(jié),進行詳細的原理說明和代碼實現(xiàn)。

二、詳細步驟與代碼實現(xiàn)

步驟 1: 定義權限數(shù)據(jù)結構與狀態(tài)管理

首先,我們需要在后端和前端約定好權限數(shù)據(jù)的結構。

1.1 后端返回的權限數(shù)據(jù)示例 (GET /api/user/permissions):
通常,后端會返回一個樹形結構,包含前端定義的路由和權限點。

{
  "code": 200,
  "data": {
    "userInfo": { "name": "Alice", "avatar": "" },
    "permissions": [
      {
        "id": 1,
        "parentId": 0,
        "path": "/system",
        "name": "System",
        "meta": { "title": "系統(tǒng)管理", "icon": "setting", "requiresAuth": true },
        "children": [
          {
            "id": 2,
            "parentId": 1,
            "path": "user",
            "name": "UserManagement",
            "meta": { "title": "用戶管理", "requiresAuth": true },
            "btnPermissions": ["user:add", "user:edit", "user:delete"] // 按鈕級權限標識
          }
        ]
      },
      {
        "id": 3,
        "parentId": 0,
        "path": "/about",
        "name": "About",
        "meta": { "title": "關于", "icon": "info", "requiresAuth": false }
      }
    ]
  }
}

1.2 前端定義靜態(tài)路由和動態(tài)路由
我們將路由分為兩類:

  • 靜態(tài)路由 (Constant Routes): 無需權限即可訪問的路由,如 /login, /404。
  • 動態(tài)路由 (Dynamic Routes / Async Routes): 需要根據(jù)權限動態(tài)添加的路由。

/src/router/index.js

import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/user';

// 靜態(tài)路由
export const constantRoutes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { title: '登錄', hidden: true } // hidden 表示不在側邊欄顯示
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/404.vue'),
    meta: { title: '404', hidden: true }
  }
];

// 動態(tài)路由(初始化為空,后續(xù)根據(jù)權限添加)
// 注意:這里不是直接定義,而是提供一個和后臺數(shù)據(jù)匹配的模板
export const asyncRoutesMap = {
  'UserManagement': {
    path: 'user', // 會拼接到父路由的 path 上
    name: 'UserManagement',
    component: () => import('@/views/system/UserManagement.vue'), // 需要提前創(chuàng)建好組件
    meta: { title: '用戶管理', requiresAuth: true }
  },
  'RoleManagement': {
    path: 'role',
    name: 'RoleManagement',
    component: () => import('@/views/system/RoleManagement.vue'),
    meta: { title: '角色管理', requiresAuth: true }
  }
  // ... 其他所有可能的路由
};

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes // 初始化時只掛載靜態(tài)路由
});

export default router;

1.3 使用 Pinia 存儲權限狀態(tài)
/src/stores/user.js

import { defineStore } from 'pinia';
import { ref } from 'vue';
import { getPermission } from '@/api/user';
import { asyncRoutesMap } from '@/router';
import { generateRoutes, generateMenu } from '@/utils/permission';

export const useUserStore = defineStore('user', () => {
  const token = ref('');
  const userInfo = ref({});
  const permissions = ref([]); // 存儲原始權限數(shù)據(jù)
  const dynamicRoutes = ref([]); // 存儲生成后的動態(tài)路由對象
  const menus = ref([]); // 存儲用于生成導航菜單的數(shù)據(jù)

  // 獲取用戶權限信息
  const getUserPermissions = async () => {
    try {
      const res = await getPermission();
      permissions.value = res.data.permissions;
      userInfo.value = res.data.userInfo;

      // 核心:根據(jù)權限數(shù)據(jù)生成動態(tài)路由和菜單
      const { routes, menuList } = generateRoutesAndMenus(permissions.value, asyncRoutesMap);
      dynamicRoutes.value = routes;
      menus.value = menuList;

      return dynamicRoutes.value;
    } catch (error) {
      console.error('獲取權限失敗', error);
      return [];
    }
  };

  // 退出登錄清空狀態(tài)
  const logout = () => {
    token.value = '';
    userInfo.value = {};
    permissions.value = [];
    dynamicRoutes.value = [];
    menus.value = [];
  };

  return {
    token,
    userInfo,
    permissions,
    dynamicRoutes,
    menus,
    getUserPermissions,
    logout
  };
});

// 工具函數(shù):遞歸處理權限數(shù)據(jù),生成路由和菜單
export const generateRoutesAndMenus = (permissionList, routeMap) => {
  const routes = [];
  const menuList = [];

  const traverse = (nodes, isChild = false) => {
    nodes.forEach(node => {
      // 1. 生成菜單項
      const menuItem = {
        path: node.path,
        name: node.name,
        meta: { ...node.meta, btnPermissions: node.btnPermissions }, // 保存按鈕權限
        children: []
      };
      if (isChild) {
        menuList[menuList.length - 1]?.children.push(menuItem);
      } else {
        menuList.push(menuItem);
      }

      // 2. 生成路由項 (只處理有 component 的節(jié)點,即葉子節(jié)點或需要布局的節(jié)點)
      // 如果后端返回的節(jié)點名稱能在我們的映射表 asyncRoutesMap 中找到,說明是有效路由
      if (routeMap[node.name]) {
        const route = {
          ...routeMap[node.name], // 展開映射表中的預設配置(最重要的是component)
          path: node.path,
          name: node.name,
          meta: { ...node.meta, btnPermissions: node.btnPermissions }
        };
        routes.push(route);
      }

      // 3. 遞歸處理子節(jié)點
      if (node.children && node.children.length > 0) {
        traverse(node.children, true);
      }
    });
  };

  traverse(permissionList);
  return { routes, menuList };
};

步驟 2: 登錄與獲取權限數(shù)據(jù)

/src/views/Login.vue

<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';

const router = useRouter();
const userStore = useUserStore();

const loginForm = ref({ username: '', password: '' });

const handleLogin = async () => {
  try {
    // 1. 執(zhí)行登錄請求,獲取 token
    const loginRes = await api.login(loginForm.value);
    userStore.token = loginRes.data.token;

    // 2. 獲取用戶權限信息
    const dynamicRoutes = await userStore.getUserPermissions();

    // 3. 動態(tài)添加路由
    dynamicRoutes.forEach(route => {
      // 注意:addRoute 可以接受父路由的 name 作為第一個參數(shù),來實現(xiàn)嵌套路由的添加
      // 這里假設我們的權限數(shù)據(jù)已經是一個平鋪的數(shù)組,或者使用其他方式匹配父路由
      // 一種更復雜的實現(xiàn)需要遞歸處理嵌套路由的添加,這里簡化演示
      router.addRoute(route); // 添加到根路由
      // 如果路由有父級,例如:router.addRoute('ParentRouteName', route);
    });

    // 4. 添加一個兜底的 404 路由(必須放在最后)
    router.addRoute({
      path: '/:pathMatch(.*)*',
      name: 'CatchAll',
      redirect: '/404'
    });

    // 5. 跳轉到首頁
    router.push('/');
  } catch (error) {
    console.error('登錄失敗', error);
  }
};
</script>

步驟 3: 路由守衛(wèi)進行權限校驗

/src/router/index.js (在原有代碼上追加)

// ... 之前的導入和路由初始化代碼 ...

// 路由守衛(wèi)
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();
  const token = userStore.token;

  // 1. 判斷是否有 token
  if (token) {
    // 2. 如果是訪問登錄頁,直接跳轉到首頁
    if (to.path === '/login') {
      next('/');
    } else {
      // 3. 判斷是否已經拉取過用戶權限信息
      if (userStore.permissions.length === 0) {
        try {
          // 4. 如果沒有獲取權限,則獲取權限并添加動態(tài)路由
          const dynamicRoutes = await userStore.getUserPermissions();
          dynamicRoutes.forEach(route => {
            router.addRoute(route);
          });
          // 5. 添加完動態(tài)路由后,需要重定向到目標路由 to
          // replace: true 防止重復添加路由導致導航失敗
          next({ ...to, replace: true });
        } catch (error) {
          // 6. 如果獲取失敗,可能是 token 過期,清除狀態(tài)并跳回登錄頁
          userStore.logout();
          next(`/login?redirect=${to.path}`);
        }
      } else {
        // 7. 如果已經有權限信息,直接放行
        next();
      }
    }
  } else {
    // 8. 沒有 token
    if (to.meta.requiresAuth === false || to.path === '/login') {
      // 如果目標路由不需要權限或者是登錄頁,則放行
      next();
    } else {
      // 否則,跳轉到登錄頁,并記錄重定向地址
      next(`/login?redirect=${to.path}`);
    }
  }
});

步驟 4: 根據(jù)權限數(shù)據(jù)生成動態(tài)菜單

使用上面 Pinia 中生成的 menus 來循環(huán)生成側邊欄菜單。

/src/components/Layout/Sidebar.vue

<template>
  <el-menu
    :default-active="$route.path"
    router
    unique-opened
    background-color="#304156"
    text-color="#bfcbd9"
    active-text-color="#409EFF"
  >
    <sidebar-item
      v-for="menu in userStore.menus"
      :key="menu.path"
      :item="menu"
    />
  </el-menu>
</template>

<script setup>
import SidebarItem from './SidebarItem.vue';
import { useUserStore } from '@/stores/user';

const userStore = useUserStore();
</script>

/src/components/Layout/SidebarItem.vue (遞歸組件)

<template>
  <!--- 如果有子菜單,渲染 el-sub-menu -->
  <el-sub-menu
    v-if="item.children && item.children.length > 0"
    :index="item.path"
  >
    <template #title>
      <el-icon><component :is="item.meta.icon" /></el-icon>
      <span>{{ item.meta.title }}</span>
    </template>
    <sidebar-item
      v-for="child in item.children"
      :key="child.path"
      :item="child"
    />
  </el-sub-menu>
  <!--- 如果沒有子菜單,渲染 el-menu-item -->
  <el-menu-item v-else :index="resolvePath(item.path)">
    <el-icon><component :is="item.meta.icon" /></el-icon>
    <template #title>{{ item.meta.title }}</template>
  </el-menu-item>
</template>

<script setup>
import { resolve } from 'path-browserify';

const props = defineProps({
  item: {
    type: Object,
    required: true
  },
  basePath: {
    type: String,
    default: ''
  }
});

// 處理完整路徑(如果需要處理嵌套路徑)
function resolvePath(routePath) {
  return resolve(props.basePath, routePath);
}
</script>

步驟 5: 實現(xiàn)按鈕級權限控制

有兩種常見方式:自定義指令函數(shù)組件。這里展示更優(yōu)雅的自定義指令方式。

5.1 創(chuàng)建權限指令 v-permission
/src/directives/permission.js

import { useUserStore } from '@/stores/user';

// 按鈕權限檢查函數(shù)
function checkPermission(el, binding) {
  const { value } = binding; // 指令的綁定值,例如 v-permission="'user:add'"
  const userStore = useUserStore();
  const btnPermissions = userStore.currentRouteBtnPermissions; // 需要從當前路由元信息中獲取按鈕權限

  // 從當前路由的 meta 中獲取按鈕權限列表
  // 注意:需要在路由守衛(wèi)或菜單生成時,將 btnPermissions 存儲到當前路由的 meta 中
  // 這里假設我們已經有了 currentRouteBtnPermissions

  if (value && Array.isArray(btnPermissions)) {
    const hasPermission = btnPermissions.includes(value);
    if (!hasPermission) {
      // 如果沒有權限,則移除該元素
      el.parentNode && el.parentNode.removeChild(el);
    }
  } else {
    throw new Error(`需要指定權限標識,如 v-permission="'user:add'"`);
  }
}

export default {
  mounted(el, binding) {
    checkPermission(el, binding);
  },
  updated(el, binding) {
    checkPermission(el, binding);
  }
};

/src/main.js

// ...
import permissionDirective from '@/directives/permission';

const app = createApp(App);
app.directive('permission', permissionDirective);
// ...

5.2 在 Pinia 中提供獲取當前路由按鈕權限的方法
修改 /src/stores/user.js

import { useRoute } from 'vue-router';
// ...
export const useUserStore = defineStore('user', () => {
  // ... 其他狀態(tài) ...
  
  // 計算屬性:獲取當前路由的按鈕權限
  const currentRouteBtnPermissions = computed(() => {
    const route = useRoute();
    return route.meta.btnPermissions || []; // 從當前路由的元信息中獲取
  });

  return {
    // ... 其他返回 ...
    currentRouteBtnPermissions
  };
});

5.3 在組件中使用指令
/src/views/system/UserManagement.vue

<template>
  <div>
    <el-button
      type="primary"
      v-permission="'user:add'"
      @click="handleAdd"
    >新增用戶</el-button>

    <el-button
      type="warning"
      v-permission="'user:edit'"
      @click="handleEdit"
    >編輯</el-button>

    <el-button
      type="danger"
      v-permission="'user:delete'"
      @click="handleDelete"
    >刪除</el-button>

    <el-table :data="tableData">
      <!-- ... -->
    </el-table>
  </div>
</template>

三、注意事項與優(yōu)化

  1. 路由組件加載: 確保 component: () => import(...) 中的路徑正確,Webpack/Vite 會將這些組件打包到獨立的 chunk 中實現(xiàn)懶加載。
  2. 404 路由處理: 動態(tài)添加路由后,一定要確保 router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' }) 是最后一個添加的路由。
  3. 按鈕權限的存儲: 上述指令示例中,按鈕權限是從當前路由的 meta 中獲取。你需要確保在路由導航守衛(wèi)或生成動態(tài)路由時,將每個路由對應的 btnPermissions 正確地設置到其 meta 中。
  4. 權限更新: 如果系統(tǒng)支持用戶動態(tài)更改權限(如切換角色),需要在權限變更后調用 router.go(0) 刷新頁面或手動重置路由狀態(tài)。
  5. 安全性: 前端權限控制只是為了用戶體驗和基礎防護,真正的權限校驗必須在后端 API 層面嚴格執(zhí)行。

到此這篇關于vue中動態(tài)權限控制的實現(xiàn)示例的文章就介紹到這了,更多相關vue 動態(tài)權限控制內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家! 

相關文章

  • vue3 el-upload單張圖片回顯、編輯、刪除功能實現(xiàn)

    vue3 el-upload單張圖片回顯、編輯、刪除功能實現(xiàn)

    這篇文章主要介紹了vue3 el-upload單張圖片回顯、編輯、刪除功能實現(xiàn),圖片回顯時隱藏上傳區(qū)域,鼠標懸浮顯示遮罩層進行編輯、刪除操作,刪除圖片后顯示上傳區(qū)域,本文通過實例代碼分享實現(xiàn)方法,感興趣的朋友一起看看吧
    2023-12-12
  • Vue 綁定style和class樣式的寫法

    Vue 綁定style和class樣式的寫法

    class 與 style 綁定就是專門用來實現(xiàn)動態(tài)樣式效果的技術,如果需要動態(tài)綁定 class 或 style 樣式,可以使用 v-bind 綁定,本文給大家講解Vue 綁定style和class樣式,感興趣的朋友一起看看吧
    2023-10-10
  • Vue在css中圖片路徑問題解決的配置方法

    Vue在css中圖片路徑問題解決的配置方法

    這篇文章主要為大家介紹了Vue在css中圖片路徑問題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-06-06
  • Vue 兄弟組件通信的方法(不使用Vuex)

    Vue 兄弟組件通信的方法(不使用Vuex)

    本篇文章主要介紹了Vue 兄弟組件通信的方法(不使用Vuex),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-10-10
  • vue常用的數(shù)字孿生可視化的自適應方案

    vue常用的數(shù)字孿生可視化的自適應方案

    這篇文章主要為大家介紹了vue常用的數(shù)字孿生可視化的自適應方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-07-07
  • 深入探索VueJS Scoped CSS 實現(xiàn)原理

    深入探索VueJS Scoped CSS 實現(xiàn)原理

    這篇文章主要介紹了深入探索VueJS Scoped CSS 實現(xiàn)原理,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-09-09
  • vue-model實現(xiàn)簡易計算器

    vue-model實現(xiàn)簡易計算器

    這篇文章主要為大家詳細介紹了vue-model實現(xiàn)簡易計算器,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-08-08
  • vue中表格設置某列樣式、不顯示表頭問題

    vue中表格設置某列樣式、不顯示表頭問題

    這篇文章主要介紹了vue中表格設置某列樣式、不顯示表頭問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • Vue-Cli中自定義過濾器的實現(xiàn)代碼

    Vue-Cli中自定義過濾器的實現(xiàn)代碼

    本篇文章主要介紹了Vue-Cli中自定義過濾器的實現(xiàn)代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • vue?tree封裝一個可選的樹組件方式

    vue?tree封裝一個可選的樹組件方式

    這篇文章主要介紹了vue?tree封裝一個可選的樹組件方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03

最新評論