深色模式
中后台系统几乎必然要做权限控制。这篇文章整理一套完整方案:路由权限 + 菜单权限 + 按钮权限,基于 Vue 2 + Vue Router + Vuex。
权限控制的三个层次
- 路由权限:某些页面需要登录/特定角色才能访问
- 菜单权限:侧边栏只显示有权限的菜单项
- 操作权限:按钮级别(新增、编辑、删除分开控制)
数据结构设计
用户登录后,从接口获取权限信息:
javascript
// 接口返回
{
user: { id: 1, name: 'Alice', role: 'editor' },
permissions: ['user:list', 'user:edit', 'article:list', 'article:create']
}路由表上声明所需权限:
javascript
const routes = [
{
path: "/users",
component: UserListPage,
meta: {
requiresAuth: true,
permission: "user:list", // 访问此路由需要的权限
},
},
{
path: "/users/create",
component: UserCreatePage,
meta: {
requiresAuth: true,
permission: "user:create",
},
},
];Vuex 存储权限状态
javascript
// store/modules/permission.js
const state = {
permissions: [],
userInfo: null,
};
const getters = {
hasPermission: (state) => (perm) => {
return state.permissions.includes(perm);
},
isAdmin: (state) => {
return state.userInfo?.role === "admin";
},
};
const mutations = {
SET_PERMISSIONS(state, permissions) {
state.permissions = permissions;
},
SET_USER_INFO(state, userInfo) {
state.userInfo = userInfo;
},
};
const actions = {
async initPermissions({ commit }) {
const { user, permissions } = await fetchCurrentUser();
commit("SET_USER_INFO", user);
commit("SET_PERMISSIONS", permissions);
return permissions;
},
};路由守卫
javascript
// router/permission.js
import store from "@/store";
import router from "@/router";
const whiteList = ["/login", "/403", "/404"];
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem("token");
if (!token) {
if (whiteList.includes(to.path)) {
next();
} else {
next(`/login?redirect=${to.fullPath}`);
}
return;
}
// 已登录但没有权限信息,先获取
if (!store.getters.userInfo) {
try {
await store.dispatch("permission/initPermissions");
} catch (e) {
// token 失效
localStorage.removeItem("token");
next(`/login?redirect=${to.fullPath}`);
return;
}
}
// 检查路由权限
const required = to.meta?.permission;
if (required && !store.getters["permission/hasPermission"](required)) {
next("/403");
return;
}
next();
});动态菜单
菜单根据权限动态渲染:
javascript
// 菜单配置(包含权限声明)
const menuConfig = [
{
title: "用户管理",
icon: "el-icon-user",
permission: "user:list",
path: "/users",
},
{
title: "文章管理",
icon: "el-icon-document",
children: [
{ title: "文章列表", path: "/articles", permission: "article:list" },
{
title: "新增文章",
path: "/articles/create",
permission: "article:create",
},
],
},
];vue
{% raw %}
<!-- 菜单组件 -->
<template>
<el-menu>
<template v-for="item in visibleMenus">
<!-- 有子菜单 -->
<el-submenu v-if="item.children" :key="item.title">
<template slot="title">{{ item.title }}</template>
<el-menu-item
v-for="child in item.children"
v-if="hasPermission(child.permission)"
:key="child.path"
:index="child.path"
>
{{ child.title }}
</el-menu-item>
</el-submenu>
<!-- 无子菜单 -->
<el-menu-item v-else :key="item.path" :index="item.path">
{{ item.title }}
</el-menu-item>
</template>
</el-menu>
</template>
<script>
export default {
computed: {
visibleMenus() {
return menuConfig.filter((item) => {
if (item.permission) {
return this.$store.getters["permission/hasPermission"](
item.permission,
);
}
// 有子菜单:只要有任意一个子菜单权限就显示父菜单
if (item.children) {
return item.children.some((child) =>
this.$store.getters["permission/hasPermission"](child.permission),
);
}
return true;
});
},
},
methods: {
hasPermission(perm) {
return this.$store.getters["permission/hasPermission"](perm);
},
},
};
</script>
{% endraw %}按钮级权限指令
javascript
// 自定义指令
Vue.directive("permission", {
inserted(el, binding) {
const perm = binding.value;
const hasPermission = store.getters["permission/hasPermission"](perm);
if (!hasPermission) {
el.parentNode?.removeChild(el);
}
},
});vue
<template>
<div>
<el-button v-permission="'user:create'" type="primary">新增用户</el-button>
<el-button v-permission="'user:edit'">编辑</el-button>
<el-button v-permission="'user:delete'" type="danger">删除</el-button>
</div>
</template>小结
- 路由级权限靠路由守卫 + meta 声明
- 菜单级权限靠 computed 过滤
- 按钮级权限靠自定义指令
- 权限数据存在 Vuex,整个应用共享
- admin 可以跳过权限检查(
isAdmingetter)