前端权限控制方向:
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
八、请求和响应控制
api/env.ts
let baseURL = ''
if (import.meta.env.MODE === 'production') {
baseURL = 'https://XXXX.net/'
} else {
baseURL = 'https://xxxxx.net/'
}
// 请求路径前缀
export default baseURL
api/config.ts
/* 不需要token就能访问的接口列表 */
export const noToken = [
'/siip/user/login/getRegisterSmsCode',
'/siip/user/login/getLoginWithPasswordSms',
]
api/request.ts
//axios二次封装
import axios from 'axios'
import baseURL from '@/api/env'
import { ElLoading, ElMessage, ElMessageBox, type Action } from 'element-plus'
import { noToken } from './config'
import { useAuthStore } from '@/stores/menu'
import router from '@/router'
const authStore = useAuthStore()
const service = axios.create({
baseURL: baseURL,
//withCredentials: true, // send cookies when cross-domain requests
timeout: 15000, // request timeout
})
let needLoadingRequestCount = 0
let fullLoading: any
// 登录报错次数
let loginErrorCount = 0
export const showLoading = () => {
if (needLoadingRequestCount === 0) {
fullLoading = ElLoading.service({
background: 'rgba(0, 0, 0, .2)',
text: '加载中',
spinner: 'el-icon-loading',
})
}
needLoadingRequestCount++
}
export const tryHideLoading = () => {
if (needLoadingRequestCount <= 0) return
needLoadingRequestCount--
if (needLoadingRequestCount === 0) {
fullLoading.close()
}
}
service.interceptors.request.use(
(config: any) => {
//排除无token就能访问的接口
const notNeedToken = noToken.some((url) => config.url.includes(url))
if (!notNeedToken) {
if (authStore.token) {
// 判断是否存在token,如果存在的话,则每个http header都加上token
config.headers.Authorization = `Bearer ${authStore.token}`
} else {
router.push({ path: '/login' })
return Promise.reject(new Error('no token'))
}
}
return config
},
(err) => {
console.log(err.request.config)
return Promise.reject(err)
},
)
// http request 拦截器
service.interceptors.response.use(
(response: any) => {
// token失效
if (response.data instanceof Blob || response.data instanceof ArrayBuffer) {
tryHideLoading()
return response.data
}
// response.data.code = response.data.status ?? 0
if (response.data.code === '401') {
//codeMsg.tokenExpire.code 401登录过期,请重新登录
if (loginErrorCount > 0) {
// return Promise.reject(response)
fullLoading.close()
sessionStorage.clear()
loginErrorCount = 0
router.push({ path: '/login' })
} else {
loginErrorCount++
ElMessageBox.alert('登录过期,请重新登录', '', {}).then(function () {
fullLoading.close()
// baseFunc.setLocalStorage('token', '')
sessionStorage.clear()
loginErrorCount = 0
router.push({ path: '/login' })
})
}
return Promise.reject(response)
} else {
if (response.data.code !== '1') {
// 接口返回1
ElMessage({
message: response.data.message || '未知错误,请查看控制台',
type: 'error',
duration: 5 * 1000,
showClose: true,
})
tryHideLoading()
return Promise.reject(response)
}
}
tryHideLoading()
return response.data
},
(error) => {
if (error.request.config === undefined || error.response === undefined) {
fullLoading.close()
tryHideLoading()
}
if (loginErrorCount > 0) {
return Promise.reject(error)
} else {
loginErrorCount++
if (error.message.indexOf('401') < 0) {
ElMessageBox.alert(
(error.response && error.response.data.message) || '出错了,请稍后再试',
'',
{
confirmButtonText: 'OK',
callback: (action: Action) => {
fullLoading.close()
tryHideLoading()
},
},
)
} else {
ElMessageBox.alert('登录失效,请重新登录。', '', {
confirmButtonText: 'OK',
callback: (action: Action) => {
fullLoading.close()
sessionStorage.clear()
localStorage.clear()
loginErrorCount = 0
router.replace({ path: '/login' })
},
})
}
}
return Promise.reject(error)
},
)
// 将axios 的 post 方法
export function $post(params: any) {
return new Promise((resolve, reject) => {
service({
...params,
...params.config,
method: 'post',
url: params.url,
})
.then((res) => {
resolve(res)
})
.catch((err) => {
reject(err)
})
})
}
// 将axios 的 get 方法
export function $get(params: any) {
return new Promise((resolve, reject) => {
service({
url: params.url,
params: params.params,
data: params.data,
...params.headers,
})
.then((res) => {
resolve(res) // 返回请求成功的数据 data
})
.catch((err) => {
reject(err)
})
})
}
// 将axios 的 delete 方法
export function $delete(params: any) {
return new Promise((resolve, reject) => {
service
.delete(params.url, params.data)
.then((res) => {
resolve(res)
})
.catch((err) => {
reject(err)
})
})
}
// 将axios 的 put 方法
export function $put(params: any) {
return new Promise((resolve, reject) => {
service
.put(params.url, params.data)
.then((res) => {
resolve(res)
})
.catch((err) => {
reject(err)
})
})
}
// del 通过body传递参数
export function $delete2(params: any) {
return service({
url: params.url,
method: 'delete',
data: params.data,
})
}
// 导出responseType
export function $download(params: any) {
return new Promise((resolve, reject) => {
service
.post(params.url, params.data, {
responseType: 'arraybuffer',
})
.then((res) => {
resolve(res)
})
.catch((err) => {
reject(err)
})
})
}
export default service
api/home.ts(封装调用)
import { $get } from '@/api/request'
export function getHomeGoodsApi(data: any) {
return $get({
url: '/home/goods/guessLike',
data,
})
}
home.vue(页面调用)
import { getHomeGoodsApi } from '@/api/home'
import { onMounted } from 'vue'
onMounted(async () => {
const res = await getHomeGoodsApi({
url: '/user/order/detail',
})
console.log(res)
})

image.png