基于 Vue3 + TS + Ant Design Vue 实现精细化菜单按钮权限授权组件

在前端开发中,RBAC(基于角色的访问控制)是企业级应用的核心需求。本文将分享如何使用 Vue3 + TypeScript + Vite + Ant Design Vue 实现一个功能完善的菜单与按钮级别的权限授权组件,支持树形菜单选择、动态按钮联动、状态持久化等高级特性。

🎯 核心功能亮点

  • 双栏布局:左侧树形菜单选择,右侧动态按钮列表
  • 细粒度控制:支持菜单级和按钮级双重权限控制
  • 状态保持:切换菜单时自动保存/恢复按钮选中状态
  • 父子联动:自动处理菜单的半选/全选状态逻辑
  • 类型安全:完整的 TypeScript 类型定义
  • 性能优化:使用 Map 缓存菜单数据,避免重复查询

📁 项目结构概览

复制代码
src/
├── components/
│   └── MenuAuthModal.vue      # 菜单授权弹窗组件
├── store/
│   └── index.ts               # Vuex Store,管理全局按钮权限
└── api/
    └── contract.ts            # 权限相关 API 接口

🔧 核心技术实现

1. 组件架构设计

采用 Ant Design Vue 的 Modal + Tree + Checkbox 组合,实现左右分栏布局:

vue 复制代码
<template>
  <a-modal :visible="visible" width="960px">
    <div class="menu-auth-content">
      <!-- 标题行 -->
      <div class="header-row">
        <div class="header-left">可授权菜单</div>
        <div class="header-right">可授权按钮</div>
      </div>
      
      <!-- 内容区域 -->
      <div class="content-row">
        <!-- 左侧:树形菜单 -->
        <div class="content-left">
          <a-tree
            v-model:checkedKeys="checkedMenuKeys"
            v-model:halfCheckedKeys="halfCheckedMenuKeys"
            checkable
            :tree-data="menuTreeData"
            @select="onMenuSelect"
          />
        </div>
        
        <!-- 右侧:按钮列表 -->
        <div class="content-right">
          <a-checkbox-group v-model:value="checkedButtons">
            <div v-for="button in buttonList" :key="button.key">
              <a-checkbox :value="button.key" :disabled="button.disabled">
                {{ button.label }}
              </a-checkbox>
            </div>
          </a-checkbox-group>
        </div>
      </div>
    </div>
  </a-modal>
</template>

2. 状态管理策略

使用 全局 Map 保存每个菜单的按钮选中状态,解决切换菜单时状态丢失问题:

typescript 复制代码
// 全局按钮选中状态(菜单ID -> 按钮ID数组)
const globalButtonStates = ref<Map<string, string[]>>(new Map())

// 监听按钮变化,实时保存
watch(checkedButtons, (newValue) => {
  if (isRestoringState.value) return // 避免恢复状态时重复触发
  
  if (selectedMenuKeys.value.length > 0) {
    const currentMenuId = selectedMenuKeys.value[0]
    globalButtonStates.value.set(currentMenuId, [...newValue])
  }
}, { deep: true })

3. 菜单树数据处理

将后端返回的嵌套菜单结构转换为 Ant Design Tree 所需格式:

typescript 复制代码
const convertMenuTree = (menu: FindMenuButtonTreeByRoleInfo): any => {
  return {
    key: String(menu.id),
    name: menu.name,
    icon: menu.icon || '',
    children: menu.children?.map(child => convertMenuTree(child)) || []
  }
}

4. 权限收集逻辑

提交时递归收集所有选中菜单(含父级)和按钮:

typescript 复制代码
const collectAllMenuIdsWithParents = (): string[] => {
  const allMenuIds = new Set<string>()
  
  // 添加完全选中和半勾选的菜单
  checkedMenuKeys.value.forEach(id => allMenuIds.add(id))
  halfCheckedMenuKeys.value.forEach(id => allMenuIds.add(id))
  
  // 递归查找所有父级菜单
  checkedMenuKeys.value.forEach(menuId => {
    const parentIds = findParentMenuIds(menuId)
    parentIds.forEach(parentId => allMenuIds.add(parentId))
  })
  
  return Array.from(allMenuIds)
}

5. 全局权限存储

在 Vuex Store 中构建按钮权限映射表,供全局使用:

typescript 复制代码
// store/index.ts
function buildButtonPermissionMap(menuList: MenuButtonTree[], buttonMap: Map<string, boolean>) {
  menuList.forEach((menu: MenuButtonTree) => {
    const menuKey = menu.active || (menu as any).key || ''
    
    if (menuKey && menu.buttons) {
      menu.buttons.forEach((button: any) => {
        const buttonKey = `${menuKey}_${button.id}`
        buttonMap.set(buttonKey, true)
      })
    }
    
    menu.children?.forEach(child => 
      buildButtonPermissionMap([child], buttonMap)
    )
  })
}

// Getter 用于权限判断
hasButtonPermission: (state) => (menuActive: string, buttonId: number | string) => {
  const buttonKey = `${menuActive}_${buttonId}`
  return state.buttonPermissions.has(buttonKey)
}

💡 使用示例

父组件调用

vue 复制代码
<template>
  <a-button @click="openAuthModal">菜单授权</a-button>
  <MenuAuthModal ref="authModalRef" @ok="handleAuthSuccess" />
</template>

<script setup lang="ts">
const authModalRef = ref()

const openAuthModal = () => {
  authModalRef.value.open({
    id: '123',
    roleName: '超级管理员'
  })
}

const handleAuthSuccess = ({ roleId, menuPermissions, buttonPermissions }) => {
  console.log('授权成功', { roleId, menuPermissions, buttonPermissions })
  // 刷新列表或更新状态
}
</script>

页面按钮权限控制

vue 复制代码
<template>
  <a-button 
    v-if="$store.getters.hasButtonPermission('ContractList', 1001)"
    @click="handleEdit"
  >
    编辑
  </a-button>
</template>

🎨 样式优化

使用 Less 实现美观的双栏布局和滚动条样式:

less 复制代码
.menu-auth-content {
  .header-row {
    display: flex;
    border-bottom: 1px solid #f0f0f0;
    
    .header-left {
      width: 30%;
      border-right: 1px solid #f0f0f0;
    }
    .header-right {
      width: 70%;
    }
  }
  
  .content-row {
    display: flex;
    
    .content-left, .content-right {
      max-height: 500px;
      overflow-y: auto;
    }
    
    .content-left {
      width: 30%;
      border-right: 1px solid #f0f0f0;
    }
    
    .content-right {
      width: 70%;
      
      .button-checkbox-item:nth-child(even) {
        background: #FAFAFA;
      }
    }
  }
}

// 自定义滚动条
.content-left::-webkit-scrollbar {
  width: 6px;
}
.content-left::-webkit-scrollbar-thumb {
  background: #d9d9d9;
  border-radius: 3px;
}

🚀 性能优化技巧

  1. Map 缓存 :使用 Map<string, Menu> 快速查找菜单对象,避免 O(n) 遍历
  2. 防抖处理 :状态恢复时使用 isRestoringState 标志位,防止 watch 重复触发
  3. 按需渲染:仅当选中菜单时才加载对应的按钮列表
  4. 深度克隆 :使用 [...array] 避免引用导致的意外修改

📝 总结

本方案实现了企业级应用所需的完整权限授权功能,具有以下优势:

  • 用户体验佳:直观的树形选择 + 实时按钮预览
  • 代码可维护:清晰的职责分离,TypeScript 类型保障
  • 扩展性强:易于添加新的权限维度(如数据权限)
  • 性能优秀:合理的缓存策略和状态管理

通过本文的实现,你可以快速在自己的项目中集成类似的权限管理系统,提升应用的安全性和灵活性。


技术栈 :Vue3 | TypeScript | Vite | Ant Design Vue | Vuex
适用场景:后台管理系统、SaaS 平台、企业级应用

💬 欢迎在评论区交流你的权限管理实践经验!如果觉得有用,请点赞收藏支持一下~