后台管理系统里,菜单权限管的是「进哪个模块」;要更细地管增、删、改、查、搜索,得用按钮权限:按角色决定按钮显不显示、能不能点。
面试常踩的坑是:只藏按钮、接口仍能调;或者三种写法(函数、组件、指令)讲不清。下面先看汇智在线的真实效果,再拆实现,最后落到项目里的插件封装。
一、真实场景:汇智在线
给「商城用户」角色不设置商品删除、banner 删除、会员删除权限,该角色登录后这些按钮就不会出现。

商城用户角色下,对应删除权限是关掉的:

超级管理员角色打开全部按钮操作权限。周 sir 以超管登录后,商品管理里能看到删除商品按钮:

同一界面,商城用户登录则没有删除按钮:

要点:权限码来自后端角色配置,前端只根据当前用户的资源列表决定渲染。这是展示层控制,接口仍须鉴权。
二、函数方式
用统一方法 hasPermission,在模板里 v-if:
<template>
<a-button v-if="hasPermission(['20000', '2000010'])" color="error" class="mx-4"> 拥有[20000,2000010]code可见 </a-button>
</template>
<script lang="ts">
import { usePermission } from '/@/hooks/web/usePermission'
export default defineComponent({
setup() {
const { hasPermission } = usePermission()
return { hasPermission }
}
})
</script>
hasPermission 从全局取出当前用户权限码,判断按钮需要的码在不在里面;传数组时,命中其中一个即可:
export function usePermission() {
function hasPermission(value, def = true) {
// 默认视为有权限
if (!value) {
return def
}
const allCodeList = permissionStore.getPermCodeList
if (!isArray(value)) {
return allCodeList.includes(value)
}
// intersection是lodash提供的一个方法,用于返回一个所有给定数组都存在的元素组成的数组
return intersection(value, allCodeList).length > 0
return true
}
}
最后一行 return true 永远走不到,属于死代码,面试时主动提一句更好。多码用 intersection 做「有交集即通过」。
三、组件方式
也可以包一层 Authority,把权限码通过 value 传入:
<template>
<div>
<Authority :value="RoleEnum.ADMIN"> <a-button type="primary" block> 只有admin角色可见 </a-button> </Authority>
</div>
</template>
<script>
import { Authority } from '/@/components/Authority'
import { defineComponent } from 'vue'
export default defineComponent({ components: { Authority } })
</script>
有权限就原样渲染插槽,没有就什么都不渲染:
<script lang="ts">
import { defineComponent } from 'vue'
import { usePermission } from '/@/hooks/web/usePermission'
import { getSlot } from '/@/utils/helper/tsxHelper'
export default defineComponent({
name: 'Authority',
props: { value: { type: [Number, Array, String], default: '' } },
setup(props, { slots }) {
const { hasPermission } = usePermission()
function renderAuth() {
const { value } = props
if (!value) {
return getSlot(slots)
}
return hasPermission(value) ? getSlot(slots) : null
}
return () => {
return renderAuth()
}
}
})
</script>
四、指令方式
模板更短,写成 v-auth:
<a-button v-auth="'1000'" type="primary" class="mx-4"> 拥有code ['1000']权限可见 </a-button>
import { usePermission } from '/@/hooks/web/usePermission'
function isAuth(el, binding) {
const { hasPermission } = usePermission()
const value = binding.value
if (!value) return
if (!hasPermission(value)) {
el.parentNode?.removeChild(el)
}
}
const mounted = (el, binding) => {
isAuth(el, binding)
}
const authDirective = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted,
}
// 注册全局指令
export function setupPermissionDirective(app) {
app.directive('auth', authDirective)
}
只在 mounted 里判断:没有权限就从 DOM 拿掉节点。权限后来变了不会自动更新,这是指令方案的常见坑。
五、汇智在线:插件封装
实际项目用插件封装指令,耦合更干净:根据登录后存进 store 的按钮资源,对照指令上的权限值,决定显示或移除。
import { useMenuStore } from '@/stores/modules/menu'
export const myPlugin = {
// 插件封装全局组件、全局指令、全局...
install(app: any, options: any) {
// 定义v-permession指令, 在 `mounted` 和 `updated` 时都调用
app.directive('permession', (el: any, binding: any) => {
// 1. 获取登录时存储store中的按钮资源数据
const menuStore = useMenuStore()
// 2. 获取当前角色资源列表 [AddProduct,EditProduct...]
const resourceList = menuStore.resourceList
// 3. 按钮绑定的权限值 binding.value
const value = binding.value
if (value) {
const isHide = resourceList && resourceList.some(item => value.includes(item))
if (!isHide) {
el && el.remove()
} else {
console.log('need rols! Like v-perisson ="AddUser"')
}
}
})
}
}
指令名按项目写成了 v-permession(拼写如此)。商品删除按钮这样绑:
<el-button type="primary" size="small" v-permession="'DeleteProduct'">删除</el-button>
登录成功后并行拉菜单和按钮资源,写入 store:

// 调用菜单资源接口
let menuPromise = RequestMenu(roleId)
// 调用按钮资源接口
let resourcePromise = RequestReourceAuthor(roleId)
//多个promise封装的异常操作用上个方法实现 Promise.all([promise,promise]) Promise.race([promise,promise])
const dataList: any = await Promise.all([
menuPromise,
resourcePromise,
])
const menuData = dataList[0]
const resourceData = dataList[1]
if (menuData.resultCode === 1) {
// 1.保存菜单接口数据
this.menuList = menuData.resultInfo
// 2.格式化菜单数据
let routeMenuList = formateRouterMenu(menuData.resultInfo)
// 3.添加动态路由,router对象
routeMenuList.forEach((route: any) =>
router.addRoute(route)
)
}
if (resourceData.resultCode === 1) {
// 保存按钮资源数据
this.resourceList = resourceData.resultInfo.resource
}
| 写法 |
怎么用 |
优点 |
面试坑 |
| 函数 |
v-if="hasPermission(...)" |
逻辑集中、好测 |
模板里容易漏写 v-if |
| 组件 |
|
插槽语义清楚 |
包一层,简单按钮略重 |
| 指令 / 插件 |
v-auth / v-permession |
模板最短 |
只 mounted 时不响应权限变化;前端隐藏 ≠ 接口安全 |

长按上图,识别图中二维码即可关注。
推荐阅读:
动态权限菜单是什么?
封装递归组件轻松实现ElementPlus 表单组件二次封装,提高50%开发效率
ElementPlus 表格组件二次封装,提高50%开发效率
token是什么? 无感刷新token技术让项目安全性与体验性完美平衡
一句话总结:按钮权限是用角色资源码决定渲染;函数、组件、指令三种壳,核心都是比对权限码,并且后端接口必须再鉴一次。