最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

一、核心原理與流程總覽

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

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

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

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

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

步驟 1: 定義權(quán)限數(shù)據(jù)結(jié)構(gòu)與狀態(tài)管理

首先,我們需要在后端和前端約定好權(quán)限數(shù)據(jù)的結(jié)構(gòu)。

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

{
  "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"] // 按鈕級權(quán)限標(biāo)識
          }
        ]
      },
      {
        "id": 3,
        "parentId": 0,
        "path": "/about",
        "name": "About",
        "meta": { "title": "關(guān)于", "icon": "info", "requiresAuth": false }
      }
    ]
  }
}

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

  • 靜態(tài)路由 (Constant Routes): 無需權(quán)限即可訪問的路由,如 /login, /404
  • 動態(tài)路由 (Dynamic Routes / Async Routes): 需要根據(jù)權(quán)限動態(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 表示不在側(cè)邊欄顯示
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/404.vue'),
    meta: { title: '404', hidden: true }
  }
];

// 動態(tài)路由(初始化為空,后續(xù)根據(jù)權(quán)限添加)
// 注意:這里不是直接定義,而是提供一個和后臺數(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 存儲權(quán)限狀態(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([]); // 存儲原始權(quán)限數(shù)據(jù)
  const dynamicRoutes = ref([]); // 存儲生成后的動態(tài)路由對象
  const menus = ref([]); // 存儲用于生成導(dǎo)航菜單的數(shù)據(jù)

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

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

      return dynamicRoutes.value;
    } catch (error) {
      console.error('獲取權(quán)限失敗', 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ù):遞歸處理權(quán)限數(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 }, // 保存按鈕權(quán)限
        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], // 展開映射表中的預(yù)設(shè)配置(最重要的是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: 登錄與獲取權(quán)限數(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. 獲取用戶權(quán)限信息
    const dynamicRoutes = await userStore.getUserPermissions();

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

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

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

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

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

// ... 之前的導(dǎo)入和路由初始化代碼 ...

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

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

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

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

/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)按鈕級權(quán)限控制

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

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

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

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

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

  if (value && Array.isArray(btnPermissions)) {
    const hasPermission = btnPermissions.includes(value);
    if (!hasPermission) {
      // 如果沒有權(quán)限,則移除該元素
      el.parentNode && el.parentNode.removeChild(el);
    }
  } else {
    throw new Error(`需要指定權(quán)限標(biāo)識,如 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 中提供獲取當(dāng)前路由按鈕權(quán)限的方法
修改 /src/stores/user.js

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

  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. 按鈕權(quán)限的存儲: 上述指令示例中,按鈕權(quán)限是從當(dāng)前路由的 meta 中獲取。你需要確保在路由導(dǎo)航守衛(wèi)或生成動態(tài)路由時,將每個路由對應(yīng)的 btnPermissions 正確地設(shè)置到其 meta 中。
  4. 權(quán)限更新: 如果系統(tǒng)支持用戶動態(tài)更改權(quán)限(如切換角色),需要在權(quán)限變更后調(diào)用 router.go(0) 刷新頁面或手動重置路由狀態(tài)。
  5. 安全性: 前端權(quán)限控制只是為了用戶體驗和基礎(chǔ)防護,真正的權(quán)限校驗必須在后端 API 層面嚴(yán)格執(zhí)行

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

相關(guān)文章

  • 詳解Vue爬坑之vuex初識

    詳解Vue爬坑之vuex初識

    本篇文章主要介紹了詳解Vue爬坑之vuex初識 ,Vue 的狀態(tài)管理工具 Vuex可以解決大型項目中子組件之間傳遞數(shù)據(jù),有興趣的可以了解下
    2017-06-06
  • 一文詳解Vue?的雙端?diff?算法

    一文詳解Vue?的雙端?diff?算法

    這篇文章主要介紹了一文詳解Vue?的雙端?diff?算法,diff?算法是渲染器中最復(fù)雜的部分,也是面試的熱點問題。今天我們就通過?Vue?的?diff?算法來探究下diff?算法吧
    2022-06-06
  • vue webpack多頁面構(gòu)建的實例代碼

    vue webpack多頁面構(gòu)建的實例代碼

    這篇文章主要介紹了vue webpack多頁面構(gòu)建的實例代碼,代碼簡單易懂,非常不錯,具有一定的參考借鑒價值 ,需要的朋友可以參考下
    2018-09-09
  • element-ui?tree?手動展開功能實現(xiàn)(異步樹也可以)

    element-ui?tree?手動展開功能實現(xiàn)(異步樹也可以)

    這篇文章主要介紹了element-ui?tree?手動進行展開(異步樹也可以),項目中用到了vue的element-ui框架,用到了el-tree組件,需要的朋友可以參考下
    2022-08-08
  • VUE3之Non-Props屬性的具體使用

    VUE3之Non-Props屬性的具體使用

    本文主要介紹了VUE3之Non-Props屬性的具體使用,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • 在導(dǎo)入.vue文件的時候,ts報錯提示:找不到模塊“@/Layout/index.vue”或其相應(yīng)的類型聲明問題

    在導(dǎo)入.vue文件的時候,ts報錯提示:找不到模塊“@/Layout/index.vue”或其相應(yīng)的類型聲明問題

    這篇文章主要介紹了在導(dǎo)入.vue文件的時候,ts報錯提示:找不到模塊“@/Layout/index.vue”或其相應(yīng)的類型聲明問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • 深入解讀VUE中的異步渲染的實現(xiàn)

    深入解讀VUE中的異步渲染的實現(xiàn)

    這篇文章主要介紹了深入解讀VUE中的異步渲染的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-06-06
  • Vue組件上使用v-model之單選框

    Vue組件上使用v-model之單選框

    這篇文章主要介紹了Vue組件上使用v-model之單選框,代碼分為子組件內(nèi)容和父組件內(nèi)容,本文結(jié)合實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-10-10
  • Vue組件創(chuàng)建和傳值的方法

    Vue組件創(chuàng)建和傳值的方法

    這篇文章給大家介紹了vue組件創(chuàng)建和傳值的方法,創(chuàng)建組件有三種方法,文中給大家介紹的非常詳細,父組件傳值給子組件的方法,給大家介紹的也非常詳細,需要的朋友參考下吧
    2018-08-08
  • Nuxt3+ElementPlus構(gòu)建打包部署全過程

    Nuxt3+ElementPlus構(gòu)建打包部署全過程

    網(wǎng)上大部分關(guān)于Nuxt打包部署教程可謂是可以用五花八門來形容,這對于第一次接觸的朋友簡直是無從下手,這篇文章主要給大家介紹了關(guān)于Nuxt3+ElementPlus構(gòu)建打包部署的相關(guān)資料,需要的朋友可以參考下
    2023-01-01

最新評論

渭源县| 巫溪县| 竹北市| 新田县| 吴旗县| 阿拉善右旗| 攀枝花市| 二手房| 靖安县| 眉山市| 安化县| 民丰县| 津南区| 临洮县| 万年县| 义马市| 屯门区| 桐梓县| 松江区| 天峻县| 枣庄市| 客服| 藁城市| 景洪市| 诸暨市| 香格里拉县| 堆龙德庆县| 象州县| 桦甸市| 自治县| 泽州县| 莱西市| 钦州市| 大城县| 蓝山县| 米林县| 高碑店市| 蓝山县| 贵定县| 正安县| 鸡东县|