亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

一、核心原理與流程總覽

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

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

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

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

二、詳細(xì)步驟與代碼實(shí)現(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):
通常,后端會(huì)返回一個(gè)樹(shù)形結(jié)構(gòu),包含前端定義的路由和權(quán)限點(diǎ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"] // 按鈕級(jí)權(quán)限標(biāo)識(shí)
          }
        ]
      },
      {
        "id": 3,
        "parentId": 0,
        "path": "/about",
        "name": "About",
        "meta": { "title": "關(guān)于", "icon": "info", "requiresAuth": false }
      }
    ]
  }
}

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

  • 靜態(tài)路由 (Constant Routes): 無(wú)需權(quán)限即可訪問(wèn)的路由,如 /login, /404。
  • 動(dòng)態(tài)路由 (Dynamic Routes / Async Routes): 需要根據(jù)權(quán)限動(dòng)態(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 }
  }
];

// 動(dòng)態(tài)路由(初始化為空,后續(xù)根據(jù)權(quán)限添加)
// 注意:這里不是直接定義,而是提供一個(gè)和后臺(tái)數(shù)據(jù)匹配的模板
export const asyncRoutesMap = {
  'UserManagement': {
    path: 'user', // 會(huì)拼接到父路由的 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 // 初始化時(shí)只掛載靜態(tài)路由
});

export default router;

1.3 使用 Pinia 存儲(chǔ)權(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([]); // 存儲(chǔ)原始權(quán)限數(shù)據(jù)
  const dynamicRoutes = ref([]); // 存儲(chǔ)生成后的動(dòng)態(tài)路由對(duì)象
  const menus = ref([]); // 存儲(chǔ)用于生成導(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ù)生成動(dòng)態(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. 生成菜單項(xiàng)
      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. 生成路由項(xiàng) (只處理有 component 的節(jié)點(diǎn),即葉子節(jié)點(diǎn)或需要布局的節(jié)點(diǎn))
      // 如果后端返回的節(jié)點(diǎn)名稱能在我們的映射表 asyncRoutesMap 中找到,說(shuō)明是有效路由
      if (routeMap[node.name]) {
        const route = {
          ...routeMap[node.name], // 展開(kāi)映射表中的預(yù)設(shè)配置(最重要的是component)
          path: node.path,
          name: node.name,
          meta: { ...node.meta, btnPermissions: node.btnPermissions }
        };
        routes.push(route);
      }

      // 3. 遞歸處理子節(jié)點(diǎn)
      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í)行登錄請(qǐng)求,獲取 token
    const loginRes = await api.login(loginForm.value);
    userStore.token = loginRes.data.token;

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

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

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

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

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

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

使用上面 Pinia 中生成的 menus 來(lái)循環(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>
  <!--- 如果沒(méi)有子菜單,渲染 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: 實(shí)現(xiàn)按鈕級(jí)權(quán)限控制

有兩種常見(jià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)或菜單生成時(shí),將 btnPermissions 存儲(chǔ)到當(dāng)前路由的 meta 中
  // 這里假設(shè)我們已經(jīng)有了 currentRouteBtnPermissions

  if (value && Array.isArray(btnPermissions)) {
    const hasPermission = btnPermissions.includes(value);
    if (!hasPermission) {
      // 如果沒(méi)有權(quán)限,則移除該元素
      el.parentNode && el.parentNode.removeChild(el);
    }
  } else {
    throw new Error(`需要指定權(quán)限標(biāo)識(shí),如 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) ...
  
  // 計(jì)算屬性:獲取當(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>

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

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

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

相關(guān)文章

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

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

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

    Vue 綁定style和class樣式的寫法

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

    Vue在css中圖片路徑問(wèn)題解決的配置方法

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

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

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

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

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

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

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

    vue-model實(shí)現(xiàn)簡(jiǎn)易計(jì)算器

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

    vue中表格設(shè)置某列樣式、不顯示表頭問(wèn)題

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

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

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

    vue?tree封裝一個(gè)可選的樹(shù)組件方式

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

最新評(píng)論