前端权限控制方向:
1、菜单权限
2、页面权限
3、按钮权限
4、请求和响应控制
一、创建项目
本次核心为vue3+ts+vite驱动
1、node版本要大于16.0
2、npm init vue@latest (基于create-vue创建)
3、安装pinia
注:如下载完依赖后main.ts中import { createApp } from 'vue'报错,关闭项目后重新打开可能会解决
4、安装pinia持久化工具
pnpm install pinia-plugin-persistedstate
main.ts中引入
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
二、准备静态路由(登录页等)
/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import login from '@/views/login/login.vue'
import layout from '@/views/layout.vue'
import notFound from '@/views/404.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: login,
},
{
path: '/:catchAll(.*)*',
name: 'NotFound',
component: notFound,
},
{
path: '/layout', //将所有返回的二级页面添加在此处
name: 'layout',
component: layout,
children: [],
},
],
})
三、登录后获取页面数据,将路由及用户信息储存在pinia中,跳转至首页
login.vue
<el-button @click="toLogin">登录</el-button>
<script setup lang="ts">
import { mockRoutesData } from '@/data/menuData' //此处模拟接口返回的数据
import { useAuthStore } from '@/stores/menu'
import { useRouter } from 'vue-router'
const router = useRouter()
const authStore = useAuthStore()
let toLogin = async () => {
//调用getRoutes方法,将后端返回的路由数据添加在缓存中
await authStore.getRoutes(mockRoutesData)
authStore.setToken('this is token')
router.push('/home')
}
</script>
/data/menuData.ts
export const mockRoutesData = [
{
id: '1',
title: '首页',
name: 'home',
path: '/home',
meta: { hidden: false, btn: ['view', 'edit'] },
component: '/home.vue',
},
{
id: '2',
title: '用户中心',
name: 'user',
path: '/user',
meta: { hidden: false },
children: [
{
id: '21',
title: '基本资料',
name: 'userInfor',
path: '/user/infor',
meta: { hidden: false },
component: '/user/infor.vue',
},
{
id: '22',
title: '安全设置',
name: 'userSecure',
path: '/user/secure',
meta: { hidden: false },
component: '/user/secure.vue',
},
],
},
{
id: '3',
title: '应用中心',
name: 'application',
path: '/application',
meta: { hidden: false },
children: [
{
id: '31',
title: '我的应用',
name: 'myApplication',
path: '/application/myApplication',
meta: { hidden: false },
component: '/application/myApplication.vue',
},
{
id: '32',
title: '安全设置',
name: 'order',
path: '/application/order',
meta: { hidden: false },
component: '/application/order.vue',
},
],
},
]
stores/menu.ts
import { ref } from 'vue'
import { defineStore } from 'pinia'
export interface RouteItemType {
id: string
name: string
path: string
meta: { hidden?: boolean; btn?: string[] }
children?: RouteItemType[] // 可选子路由
component?: string
}
export const useAuthStore = defineStore('infor', {
state: () => ({
token: ref(''),
menuRoutes: [] as RouteItemType[], // 新增菜单数据存储
}),
actions: {
setToken(token: string) {
this.token = token
},
getRoutes(mockRoutes: RouteItemType[]) {
this.menuRoutes = mockRoutes
},
logout() {
this.token = ''
this.menuRoutes = []
},
},
persist: true, //pinia持久化
})
四、通过路由导航守卫动态添加路由
/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useAuthStore, type RouteItemType } from '@/stores/menu'
import login from '@/views/login/login.vue'
import layout from '@/views/layout.vue'
import notFound from '@/views/404.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: login,
},
{
path: '/:catchAll(.*)*',
name: 'NotFound',
component: notFound,
},
{
path: '/layout',
name: 'layout',
component: layout,
children: [],
},
],
})
//用来标记是否已经添加过路由
let hasAddedRoutes = false
// 添加动态路由的函数
const addDynamicRoutes = (routes: RouteItemType[]) => {
routes.forEach((route) => {
// 创建路由配置 - 使用 RouteRecordRaw 类型
const routeConfig: RouteRecordRaw = {
path: route.path,
name: route.name,
component: () => import(`../views${route.component}` || ''),
meta: route.meta,
}
// 处理嵌套路由 - 使用 RouteRecordRaw 类型
if (route.children && route.children.length > 0) {
addDynamicRoutes(route.children)
}
// 添加路由
router.addRoute('layout', routeConfig)
// router.addRoute(routeConfig)
})
}
// 路由守卫
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
if (to.path === '/login') {
next()
return
}
if (!authStore.token) {
next('/login')
return
}
if (to.path === '/' && authStore.token) {
next('/home')
return
}
if (!hasAddedRoutes) {
try {
// 获取路由数据
const routes = await authStore.menuRoutes
// 添加动态路由
await addDynamicRoutes(routes)
// 标记已添加路由
hasAddedRoutes = true
// 重新导航
// next({ ...to, replace: true })
// 解决刷新一直进404页面
return next({ path: to.path, query: to.query, replace: true })
} catch (error) {
console.error('添加路由失败:', error)
next('/login')
}
} else {
next()
}
})
export default router
五、layout.vue中添加页面出口
layout.vue
<template>
<div class="layout">
<el-container>
<el-header>
<h5 class="title">后台管理系统</h5>
<el-button @click="logout">退出</el-button>
</el-header>
<el-container>
<el-aside width="200px">
<AsideMenu />
</el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import AsideMenu from '@/components/AsideMenu/AsideMenu.vue'
import { useAuthStore } from '@/stores/menu.ts'
import { useRouter } from 'vue-router'
const router = useRouter()
const logout = () => {
useAuthStore().logout()
router.push('/login')
}
</script>
<style>
.layout,
.el-container {
height: 100%;
}
.el-aside {
height: 100%;
background-color: #eee;
}
.el-header {
background-color: #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
六、使用ElementPlus制作菜单导航
components/AsideMenu/AsideMenu.vue
<template>
<el-menu
v-if="menuItems?.length > 0"
:default-active="menuActive" //激活菜单项
:default-openeds="[menuActive]" //刷新后展开原来点击的子菜单
:unique-opened="uniqueOpened" //保持一个子菜单展开
>
<MenuSection v-for="(menu, index) in menuItems" :key="menu.id" :menu="menu" />
</el-menu>
</template>
<script setup lang="ts">
import { useAuthStore, type RouteItemType } from '@/stores/menu.ts'
import { ref, onMounted, computed } from 'vue'
import MenuSection from './MenuSection.vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const menuItems = ref<RouteItemType[]>([])
const menuActive = computed(() => route.path)
const uniqueOpened = ref(true)
onMounted(async () => {
try {
const authStore = await useAuthStore()
menuItems.value = authStore.menuRoutes //获取路由菜单递归给MenuSection.vue遍历出菜单
} catch (error) {
console.error('加载菜单失败:', error)
}
})
</script>
<style></style>
/MenuSection.vue递归实现菜单
<template>
<!-- 1、存在子项的使用el-sub-menu 2、不存在子项的使用el-menu-item -->
<!-- 有子菜单的情况 -->
<el-sub-menu v-if="menu.children?.length" :index="menu.path">
<template #title>
<span>{{ menu.title }}</span>
</template>
<MenuSection v-for="child in menu.children" :key="child.path" :menu="child" />
</el-sub-menu>
<!-- 无子菜单情况 -->
<el-menu-item v-else :index="menu.path">
<router-link :to="menu.path">
<span>{{ menu.title }}</span>
</router-link>
</el-menu-item>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
defineProps(['menu'])
</script>
<style scoped>
a {
width: 100%;
text-align: left;
}
.el-menu-item.is-active,
.el-menu-item.is-active a {
color: #10a3c4;
}
</style>
七、按钮权限(自定义指令)
1、自定义v-permission指令
<template>
<div>home</div>
<button v-permission="'view'">查看</button>
<button v-permission="'edit'">编辑</button>
<button v-permission="'delete'">删除</button>
</template>
<script setup lang="ts"></script>
<style scoped></style>
2、新建utils文件夹用来放置全局方法
src/utils/permission.ts
import router from '@/router'
export const checkPermission = (el: any, binding: any) => {
//获取当前路由meta中储存的按钮权限
let routerBtn: string[] = router.currentRoute.value.meta.btn as string[]
let currentBtn: string = binding.value
if (!routerBtn.includes(currentBtn)) {
el.remove()
}
}
3、在main.js中调用自定义指令及方法
import { checkPermission } from '@/utils/permission'
const app = createApp(App)
//此处permission与标签中使用的v-permission相同
app.directive('permission', {
mounted(el, binding) {
checkPermission(el, binding)
},
})
image.png