feat(access): 实现基于用户角色的路由权限控制
添加权限检查功能,包括用户角色定义、路由元信息扩展和权限验证逻辑 重构路由配置和用户存储,支持动态菜单过滤 更新构建配置以支持类型声明生成
This commit is contained in:
@@ -9,7 +9,9 @@
|
||||
"build": "vue-tsc -b && vite build --mode dev",
|
||||
"build:prod": "vue-tsc -b && vite build --mode prod",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .ts,.vue"
|
||||
"lint": "eslint . --ext .ts,.vue",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"build:types": "vue-tsc --declaration --emitDeclarationOnly"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.57.0",
|
||||
|
||||
37
src/access/checkAccess.ts
Normal file
37
src/access/checkAccess.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { LoginUesr } from "../store/types";
|
||||
import ACCESS_ENUM from "./accessEnum";
|
||||
|
||||
/**
|
||||
* 检查当前用户是否拥有权限
|
||||
* @param loginUser 登录的用户信息
|
||||
* @param needAccess 需要的权限
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const checkAccess = (
|
||||
loginUser: LoginUesr,
|
||||
needAccess = ACCESS_ENUM.NOT_LOGIN
|
||||
): boolean => {
|
||||
// 获取当前登录用户具有的权限
|
||||
const loginUserAccess = loginUser?.userRole ?? ACCESS_ENUM.NOT_LOGIN;
|
||||
// 对比权限是否足够
|
||||
// 如果需要的权限是随便是个人都能访问
|
||||
if (needAccess === ACCESS_ENUM.NOT_LOGIN) {
|
||||
return true;
|
||||
}
|
||||
// 如果用户登录才能访问
|
||||
if (needAccess === ACCESS_ENUM.USER) {
|
||||
if (loginUserAccess !== ACCESS_ENUM.NOT_LOGIN) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 如果需要管理员权限
|
||||
if (needAccess === ACCESS_ENUM.ADMIN) {
|
||||
// 如果不为管理员
|
||||
if (loginUserAccess !== ACCESS_ENUM.ADMIN) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 如果说啥都不符合,那还说啥了,直接拒了
|
||||
return false;
|
||||
};
|
||||
export default checkAccess;
|
||||
6
src/access/index.ts
Normal file
6
src/access/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import router from "../router/router";
|
||||
import { useUserStore } from "../store/user";
|
||||
import ACCESS_ENUM from "./accessEnum";
|
||||
import checkAccess from "./checkAccess";
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<a-row id="globalHeader" align="center" :wrap="false">
|
||||
|
||||
<a-col flex="auto">
|
||||
<a-menu mode="horizontal" :selected-keys="selectedKeys" @menu-item-click="doMenuItemClick">
|
||||
<a-menu-item key="0" :style="{ padding: 0, marginRight: '38px' }" disabled>
|
||||
<a-menu
|
||||
mode="horizontal"
|
||||
:selected-keys="selectedKeys"
|
||||
@menu-item-click="doMenuItemClick"
|
||||
>
|
||||
<a-menu-item
|
||||
key="0"
|
||||
:style="{ padding: 0, marginRight: '38px' }"
|
||||
disabled
|
||||
>
|
||||
<div class="title-bar">
|
||||
<img class="logo" src="@/assets/logo.webp" />
|
||||
<div class="logo-title">AI OJ</div>
|
||||
@@ -18,28 +25,39 @@
|
||||
<div>MeowRain</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { routes } from '../router/index.ts';
|
||||
import { routes } from "../router/index.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import checkAccess from "../access/checkAccess.ts";
|
||||
import { useUserStore } from "../store/user.ts";
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 默认主页
|
||||
const selectedKeys = ref(["/"]);
|
||||
const doMenuItemClick = (key: string) => {
|
||||
// console.info("触发菜单跳转,当前路径: ", key)
|
||||
router.push({
|
||||
path: key
|
||||
})
|
||||
}
|
||||
path: key,
|
||||
});
|
||||
};
|
||||
// 展示可见的路由
|
||||
const visibleRoutes = computed(() => {
|
||||
return routes.filter((item, index) => {
|
||||
return true;
|
||||
})
|
||||
})
|
||||
if (item?.meta?.hideInMenu) {
|
||||
return false;
|
||||
}
|
||||
// 根据权限过滤菜单
|
||||
if (!checkAccess(userStore.loginUser, item?.meta?.access)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
/**
|
||||
* router.afterEach 是 Vue Router 的全局后置钩子(global after hook),会在每次路由导航完成后触发。它的典型用途是:
|
||||
*
|
||||
@@ -52,12 +70,13 @@ const visibleRoutes = computed(() => {
|
||||
* 做一些不影响导航结果的副作用(因为 afterEach 无法取消导航)
|
||||
*/
|
||||
router.afterEach((to, from, failure) => {
|
||||
console.log('导航已完成:', from.fullPath, '->', to.fullPath)
|
||||
selectedKeys.value = [to.path]
|
||||
})
|
||||
console.log("导航已完成:", from.fullPath, "->", to.fullPath);
|
||||
selectedKeys.value = [to.path];
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
#globalHeader {}
|
||||
#globalHeader {
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
@@ -72,4 +91,4 @@ router.afterEach((to, from, failure) => {
|
||||
.logo-title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import type {RouteRecordRaw} from "vue-router";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import HomeView from "../views/HomeView.vue";
|
||||
import AboutView from "../views/AboutView.vue";
|
||||
|
||||
import ACCESS_ENUM from "../access/accessEnum";
|
||||
/**
|
||||
* 路由配置
|
||||
*/
|
||||
export const routes: Array<RouteRecordRaw> = [
|
||||
{path: "/home", name: "HomeView", component: HomeView},
|
||||
{path: "/about", name: "AboutView", component: AboutView},
|
||||
{
|
||||
path: "/home",
|
||||
name: "HomeView",
|
||||
component: HomeView,
|
||||
meta: { hideInMenu: false, access: ACCESS_ENUM.NOT_LOGIN },
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "AboutView",
|
||||
component: AboutView,
|
||||
meta: { hideInMenu: false, access: ACCESS_ENUM.NOT_LOGIN },
|
||||
},
|
||||
];
|
||||
|
||||
5
src/store/types.d.ts
vendored
Normal file
5
src/store/types.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
export interface LoginUesr {
|
||||
userName: string;
|
||||
userRole?: string;
|
||||
}
|
||||
@@ -1,11 +1,34 @@
|
||||
import { defineStore } from "pinia";
|
||||
import ACCESS_ENUM from "../access/accessEnum";
|
||||
import type { LoginUesr } from "../store/types";
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const useUserStore = defineStore("user", {
|
||||
state: () => ({
|
||||
loginUser: {
|
||||
userName: "未登录",
|
||||
userRole: ACCESS_ENUM.NOT_LOGIN,
|
||||
},
|
||||
} as LoginUesr,
|
||||
}),
|
||||
actions: {},
|
||||
actions: {
|
||||
// 获取登录用户
|
||||
async getLoginUser() {
|
||||
try {
|
||||
// 从后端获取当前登录用户信息
|
||||
|
||||
}catch(e) {
|
||||
console.error("获取登录用户失败", e);
|
||||
// 网络错误情况也视为未登录
|
||||
this.loginUser = {
|
||||
...this.loginUser,
|
||||
userRole: ACCESS_ENUM.NOT_LOGIN,
|
||||
};
|
||||
}
|
||||
},
|
||||
// 手动更新用户状态
|
||||
updateUserLoginStatus(user: LoginUesr) {
|
||||
this.loginUser = user;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
12
src/types/router.d.ts
vendored
Normal file
12
src/types/router.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// src/types/router.d.ts
|
||||
import 'vue-router'; // 让 TypeScript 知道 vue-router 模块的类型定义
|
||||
|
||||
/**
|
||||
* 路由元信息
|
||||
*/
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
hideInMenu?: boolean;
|
||||
access?: string;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"declarationDir": "./dist/types",
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": [
|
||||
"vite/client"
|
||||
|
||||
Reference in New Issue
Block a user