在前端开发中,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;
}
🚀 性能优化技巧
- Map 缓存 :使用
Map<string, Menu>快速查找菜单对象,避免 O(n) 遍历 - 防抖处理 :状态恢复时使用
isRestoringState标志位,防止 watch 重复触发 - 按需渲染:仅当选中菜单时才加载对应的按钮列表
- 深度克隆 :使用
[...array]避免引用导致的意外修改
📝 总结
本方案实现了企业级应用所需的完整权限授权功能,具有以下优势:
- 用户体验佳:直观的树形选择 + 实时按钮预览
- 代码可维护:清晰的职责分离,TypeScript 类型保障
- 扩展性强:易于添加新的权限维度(如数据权限)
- 性能优秀:合理的缓存策略和状态管理
通过本文的实现,你可以快速在自己的项目中集成类似的权限管理系统,提升应用的安全性和灵活性。
技术栈 :Vue3 | TypeScript | Vite | Ant Design Vue | Vuex
适用场景:后台管理系统、SaaS 平台、企业级应用
💬 欢迎在评论区交流你的权限管理实践经验!如果觉得有用,请点赞收藏支持一下~