feat(project): 增加权限控制管理

This commit is contained in:
chen.home 2023-03-19 23:49:39 +08:00
parent 53351c5f8b
commit 299d805aa6
15 changed files with 174 additions and 52 deletions

View File

@ -11,15 +11,21 @@
</div>
## 🌈 介绍
一个基于Vue3、Vite3、Typescript、pinia、Naive UI、Vue-Router的后台管理免费开源模板助力提高开发效率让大家早点下班做自己的事情
[Ench-admin](https://github.com/chen-see/ench-admin)一个基于Vue3、Vite3、Typescript、pinia、Naive UI、Vue-Router的后台管理免费开源模板助力提高开发效率让大家早点下班做自己的事情
## 😎 线上预览地址
- [Ench-Admin 预览地址](https://ench-admin.vercel.app/)
## 💾 代码仓库
## ⚡特性
- [github](https://github.com/chen-see/ench-admin)
- **最新流行技术栈** - 基于Vue3、Vite、TypeScript、NaiveUI、Pinia等最新技术栈开发
- **网络请求功能封装** - 完善的axios封装和配置统一的响应处理和多场景能力
- **权限控制** - 完善的前后端权限管理方案
- **路由系统** - 支持本地静态路由和后台返回路由两种获取模式
- **组件封装** - 对日常使用频率较高的组件二次封装,满足基础工作需求
- **主题配置** - 黑暗主题适配
- **代码规范** - 完整支持的代码风格规范和代码提交规范
## 🚧 安装使用
@ -51,7 +57,7 @@ pnpm commit
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 🙌 学习交流
Ench-Admin 是完全开源免费的项目旨在帮助开发者更方便地进行中大型管理系统开发有使用问题欢迎在QQ交流群内提问。
## 🧩贡献
如果您发现了任何问题或有改进建议请创建一个issue或提交一个PR。我们欢迎您的贡献
@ -60,6 +66,6 @@ pnpm commit
如果感觉本项目对你工作或学习有帮助请帮我点一个✨Star,这将是对我极大的鼓励与支持。
## 🧾许可证
## 🧾License
该项目采用MIT许可证详见[LICENSE](LICENSE)文件。

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title>
</head>

View File

@ -7,11 +7,10 @@ const token = () => Random.string('upper', 32, 32);
const userInfo = {
userId: '1',
userName: 'admin',
userName: 'iamsee',
realName: '管理员大人',
avatar: 'https://z3.ax1x.com/2021/10/29/5jnWgf.jpg',
role: 'admin',
password: '123456',
role: "user",
};
const userRoutes = [
{
@ -284,6 +283,37 @@ const userRoutes = [
},
],
},
{
name: 'permission',
path: '/permission',
redirect: '/permission/permission',
meta: {
title: '权限示例',
requiresAuth: true,
icon: 'icon-park-outline:people-safe',
},
children: [
{
name: 'permission_permission',
path: '/permission/permission',
meta: {
title: '权限示例',
requiresAuth: true,
icon: 'icon-park-outline:right-user',
},
},
{
name: 'permission_justSuper',
path: '/permission/justSuper',
meta: {
title: '超管super可见',
requiresAuth: true,
roles:['super'],
icon: 'icon-park-outline:wrong-user',
},
},
]
},
{
name: 'error',
path: '/error',
@ -292,6 +322,7 @@ const userRoutes = [
title: '异常页',
requiresAuth: true,
icon: 'icon-park-outline:error-computer',
},
children: [
{

View File

Before

Width:  |  Height:  |  Size: 523 B

After

Width:  |  Height:  |  Size: 523 B

View File

@ -1,3 +1,7 @@
import { useAuthStore } from '@/store';
import { isArray, isString } from '@/utils';
interface AppInfo {
/** 项目名称 */
name: string;
@ -21,3 +25,31 @@ export function useAppInfo(): AppInfo {
desc,
};
}
/** 权限判断 */
export function usePermission() {
const authStore = useAuthStore()
function hasPermission(permission: Auth.RoleType | Auth.RoleType[] | undefined) {
if (!permission) return true
const { role } = authStore.userInfo
let has = role === 'super';
if (!has) {
if (isArray(permission)) {
has = (permission as Auth.RoleType[]).includes(role);
}
if (isString(permission)) {
has = (permission as Auth.RoleType) === role;
}
}
return has;
}
return {
hasPermission
};
}

View File

@ -1,6 +1,7 @@
import { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '@/layouts/index';
import { useRouteStore } from '@/store';
import { useRouteStore, useAuthStore } from '@/store';
import { usePermission } from '@/hooks'
// 引入所有页面
const modules = import.meta.glob('../../views/**/*.vue');
@ -22,6 +23,13 @@ function FlatAuthRoutes(routes: AppRoute.Route[]) {
return result;
}
function filterPermissionRoutes(routes: AppRoute.Route[]) {
const { hasPermission } = usePermission();
return routes.filter((route) => {
return hasPermission(route.meta.roles)
})
}
function createCatheRoutes(routes: AppRoute.Route[]) {
return routes
.filter((item) => {
@ -32,11 +40,13 @@ function createCatheRoutes(routes: AppRoute.Route[]) {
export async function createDynamicRoutes(routes: AppRoute.Route[]) {
// 数组降维成一维数组,然后删除所有的childen
const flatRoutes = FlatAuthRoutes(routes);
// 对降维后的数组过滤需要缓存的路由name数组
/* 路由权限过滤 */
const permissionRoutes = filterPermissionRoutes(flatRoutes)
// 过滤需要缓存的路由name数组
const routeStore = useRouteStore();
routeStore.cacheRoutes = createCatheRoutes(flatRoutes);
routeStore.cacheRoutes = createCatheRoutes(permissionRoutes);
// 生成路由有redirect的不需要引入文件
const mapRoutes = flatRoutes.map((item: any) => {
const mapRoutes = permissionRoutes.map((item: any) => {
if (!item.redirect) {
// 动态加载对应页面
item['component'] = modules[`../../views${item.path}/index.vue`];

View File

@ -13,6 +13,7 @@ export async function createPermissionGuard(
// 判断有无TOKEN,登录鉴权
const isLogin = Boolean(getToken());
if (!isLogin) {
if (to.name === 'login') {
next();
@ -36,9 +37,16 @@ export async function createPermissionGuard(
}
}
// 权限路由已经加载仍然未找到重定向到404
if (to.name === 'not-found') {
next({ name: 'not-found', replace: true });
return false;
}
// 判断当前页是否在login,则定位去首页
if (to.name === 'login') {
next({ path: '/appRoot' })
return false;
}
// 设置菜单高亮
@ -52,5 +60,5 @@ export async function createPermissionGuard(
tabStore.addTab(to);
// 设置高亮标签;
tabStore.setCurrentTab(to.name as string);
next();
next()
}

View File

@ -10,7 +10,7 @@ export const routes: RouteRecordRaw[] = [
component: BasicLayout,
children: [
{
path: '/no-found',
path: '/not-found',
name: 'not-found',
component: () => import('@/views/error/not-found/index.vue'),
meta: {
@ -38,7 +38,7 @@ export const routes: RouteRecordRaw[] = [
},
{
path: '/:pathMatch(.*)*',
redirect: '/no-found',
redirect: '/not-found',
},
],
},

View File

@ -1,10 +1,12 @@
import { defineStore } from 'pinia';
import { renderIcon, getUserInfo } from '@/utils';
import { renderIcon, getUserInfo ,isEmpty} from '@/utils';
import { MenuOption } from 'naive-ui';
import { createDynamicRoutes } from '@/router/guard/dynamic';
import { router } from '@/router';
import { fetchUserRoutes } from '@/service';
import { staticRoutes } from '@/router/modules';
import { useAuthStore } from '@/store';
import { usePermission } from '@/hooks'
interface RoutesStatus {
isInitAuthRoute: boolean;
@ -78,20 +80,28 @@ export const useRouteStore = defineStore('route-store', {
},
//* 将返回的路由表渲染成侧边栏 */
transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]): MenuOption[] {
const authStore = useAuthStore()
const { role } = authStore.userInfo
return userRoutes
/** 隐藏不需要显示的菜单 */
.filter((item) => {
return !item.meta.hide;
})
.filter((item: AppRoute.Route) => {
const { hasPermission } = usePermission();
return hasPermission(item.meta.roles)
})
/** 转换为侧边菜单数据结构 */
.map((item) => {
const target: MenuOption = {
label: item.meta.title,
key: item.path,
};
// 判断有无图标
/** 判断有无图标 */
if (item.meta.icon) {
target.icon = renderIcon(item.meta.icon);
}
// 判断子元素
/** 判断子元素 */
if (item.children) {
const children = this.transformAuthRoutesToMenus(item.children);
// 只有子元素有且不为空时才添加

View File

@ -13,7 +13,7 @@ declare namespace Auth {
token: string;
refreshToken: string;
}
type RoleType = 'super' | 'admin' | 'manage' | 'user';
interface UserInfo {
/** 用户id */
userId: string;
@ -57,7 +57,7 @@ declare namespace CommonList {
gender: '0' | '1' | null;
email: string;
address: string;
role: 'super' | 'admin' | 'user';
role: RoleType;
disabled: boolean;
}
}

View File

@ -23,7 +23,7 @@ declare namespace AppRoute {
/* 是否需要登录权限。*/
requiresAuth?: boolean;
/* 可以访问的角色 */
roles?: string[];
roles?: Auth.RoleType[];
/* 是否开启页面缓存 */
keepAlive?: boolean;
/* 有些路由我们并不想在菜单中显示,比如某些编辑页面。 */

View File

@ -5,18 +5,42 @@ export function is(val: unknown, type: string) {
return toString.call(val) === `[object ${type}]`;
}
export function isDef<T = unknown>(val?: T): val is T {
return typeof val !== 'undefined';
export function isString(val: unknown): val is string {
return is(val, 'String');
}
export function isNumber(val: unknown): val is number {
return is(val, 'Number');
}
export function isBoolean(val: unknown): val is boolean {
return is(val, 'Boolean');
}
export function isNull(val: unknown): val is null {
return val === null;
}
export function isUnDef<T = unknown>(val?: T): val is T {
return !isDef(val);
}
export function isDef<T = unknown>(val?: T): val is T {
return typeof val !== 'undefined';
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val);
}
export function isObject(val: any): val is Record<any, any> {
return val !== null && is(val, 'Object');
}
export function isArray(val: any): val is Array<any> {
return val && Array.isArray(val);
}
export function isEmpty<T = unknown>(val: T): val is T {
if (isArray(val) || isString(val)) {
return val.length === 0;
@ -37,29 +61,11 @@ export function isDate(val: unknown): val is Date {
return is(val, 'Date');
}
export function isNull(val: unknown): val is null {
return val === null;
}
export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val);
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val);
}
export function isNumber(val: unknown): val is number {
return is(val, 'Number');
}
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch);
}
export function isString(val: unknown): val is string {
return is(val, 'String');
}
export function isFunction(val: unknown): val is Function {
return typeof val === 'function';
@ -69,18 +75,11 @@ export function isFile<T extends File>(val: T | unknown): val is T {
return is(val, 'File');
}
export function isBoolean(val: unknown): val is boolean {
return is(val, 'Boolean');
}
export function isRegExp(val: unknown): val is RegExp {
return is(val, 'RegExp');
}
export function isArray(val: any): val is Array<any> {
return val && Array.isArray(val);
}
export function isWindow(val: any): val is Window {
return typeof window !== 'undefined' && is(val, 'Window');
}
@ -93,9 +92,8 @@ export const isServer = typeof window === 'undefined';
export const isClient = !isServer;
export function isUrl<T>(path: T): boolean {
export function isUrl(path: string): boolean {
const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
// @ts-expect-error
return reg.test(path);
}

View File

@ -0,0 +1,13 @@
<template>
<div>
只有超级管理员可见
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@ -0,0 +1,14 @@
<template>
<div>
权限示例:
<n-h1> 当前权限{{ role }}</n-h1>
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store';
const authStore = useAuthStore();
const { role } = authStore.userInfo;
</script>
<style scoped></style>

View File

@ -30,7 +30,7 @@ export default defineConfig(({ command, mode }: ConfigEnv) => {
server: {
host: '0.0.0.0',
port: 3000,
open: true,
open: false,
proxy: isOpenProxy ? createViteProxy(envConfig) : undefined,
},
preview: {