feat(access): 实现基于用户角色的路由权限控制
添加权限检查功能,包括用户角色定义、路由元信息扩展和权限验证逻辑 重构路由配置和用户存储,支持动态菜单过滤 更新构建配置以支持类型声明生成
This commit is contained in:
@@ -9,7 +9,9 @@
|
|||||||
"build": "vue-tsc -b && vite build --mode dev",
|
"build": "vue-tsc -b && vite build --mode dev",
|
||||||
"build:prod": "vue-tsc -b && vite build --mode prod",
|
"build:prod": "vue-tsc -b && vite build --mode prod",
|
||||||
"preview": "vite preview",
|
"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": {
|
"dependencies": {
|
||||||
"@arco-design/web-vue": "^2.57.0",
|
"@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>
|
<template>
|
||||||
<a-row id="globalHeader" align="center" :wrap="false">
|
<a-row id="globalHeader" align="center" :wrap="false">
|
||||||
|
|
||||||
<a-col flex="auto">
|
<a-col flex="auto">
|
||||||
<a-menu mode="horizontal" :selected-keys="selectedKeys" @menu-item-click="doMenuItemClick">
|
<a-menu
|
||||||
<a-menu-item key="0" :style="{ padding: 0, marginRight: '38px' }" disabled>
|
mode="horizontal"
|
||||||
|
:selected-keys="selectedKeys"
|
||||||
|
@menu-item-click="doMenuItemClick"
|
||||||
|
>
|
||||||
|
<a-menu-item
|
||||||
|
key="0"
|
||||||
|
:style="{ padding: 0, marginRight: '38px' }"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<img class="logo" src="@/assets/logo.webp" />
|
<img class="logo" src="@/assets/logo.webp" />
|
||||||
<div class="logo-title">AI OJ</div>
|
<div class="logo-title">AI OJ</div>
|
||||||
@@ -18,28 +25,39 @@
|
|||||||
<div>MeowRain</div>
|
<div>MeowRain</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { routes } from '../router/index.ts';
|
import { routes } from "../router/index.ts";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import checkAccess from "../access/checkAccess.ts";
|
||||||
|
import { useUserStore } from "../store/user.ts";
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// 默认主页
|
// 默认主页
|
||||||
const selectedKeys = ref(["/"]);
|
const selectedKeys = ref(["/"]);
|
||||||
const doMenuItemClick = (key: string) => {
|
const doMenuItemClick = (key: string) => {
|
||||||
// console.info("触发菜单跳转,当前路径: ", key)
|
// console.info("触发菜单跳转,当前路径: ", key)
|
||||||
router.push({
|
router.push({
|
||||||
path: key
|
path: key,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
// 展示可见的路由
|
||||||
const visibleRoutes = computed(() => {
|
const visibleRoutes = computed(() => {
|
||||||
return routes.filter((item, index) => {
|
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),会在每次路由导航完成后触发。它的典型用途是:
|
* router.afterEach 是 Vue Router 的全局后置钩子(global after hook),会在每次路由导航完成后触发。它的典型用途是:
|
||||||
*
|
*
|
||||||
@@ -52,12 +70,13 @@ const visibleRoutes = computed(() => {
|
|||||||
* 做一些不影响导航结果的副作用(因为 afterEach 无法取消导航)
|
* 做一些不影响导航结果的副作用(因为 afterEach 无法取消导航)
|
||||||
*/
|
*/
|
||||||
router.afterEach((to, from, failure) => {
|
router.afterEach((to, from, failure) => {
|
||||||
console.log('导航已完成:', from.fullPath, '->', to.fullPath)
|
console.log("导航已完成:", from.fullPath, "->", to.fullPath);
|
||||||
selectedKeys.value = [to.path]
|
selectedKeys.value = [to.path];
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
#globalHeader {}
|
#globalHeader {
|
||||||
|
}
|
||||||
|
|
||||||
.title-bar {
|
.title-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
import type {RouteRecordRaw} from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
import HomeView from "../views/HomeView.vue";
|
import HomeView from "../views/HomeView.vue";
|
||||||
import AboutView from "../views/AboutView.vue";
|
import AboutView from "../views/AboutView.vue";
|
||||||
|
import ACCESS_ENUM from "../access/accessEnum";
|
||||||
|
/**
|
||||||
|
* 路由配置
|
||||||
|
*/
|
||||||
export const routes: Array<RouteRecordRaw> = [
|
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 { defineStore } from "pinia";
|
||||||
import ACCESS_ENUM from "../access/accessEnum";
|
import ACCESS_ENUM from "../access/accessEnum";
|
||||||
|
import type { LoginUesr } from "../store/types";
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
export const useUserStore = defineStore("user", {
|
export const useUserStore = defineStore("user", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
loginUser: {
|
loginUser: {
|
||||||
userName: "未登录",
|
userName: "未登录",
|
||||||
userRole: ACCESS_ENUM.NOT_LOGIN,
|
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",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": false,
|
||||||
|
"declarationDir": "./dist/types",
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"types": [
|
"types": [
|
||||||
"vite/client"
|
"vite/client"
|
||||||
|
|||||||
Reference in New Issue
Block a user