mirror of
https://github.com/chansee97/nova-admin.git
synced 2025-04-05 19:41:59 +08:00
feat(project): 增加权限控制管理
This commit is contained in:
parent
53351c5f8b
commit
299d805aa6
16
README.md
16
README.md
@ -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)文件。
|
||||
|
@ -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>
|
||||
|
@ -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: [
|
||||
{
|
||||
|
Before Width: | Height: | Size: 523 B After Width: | Height: | Size: 523 B |
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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`];
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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);
|
||||
// 只有子元素有且不为空时才添加
|
||||
|
4
src/typings/business.d.ts
vendored
4
src/typings/business.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
2
src/typings/route.d.ts
vendored
2
src/typings/route.d.ts
vendored
@ -23,7 +23,7 @@ declare namespace AppRoute {
|
||||
/* 是否需要登录权限。*/
|
||||
requiresAuth?: boolean;
|
||||
/* 可以访问的角色 */
|
||||
roles?: string[];
|
||||
roles?: Auth.RoleType[];
|
||||
/* 是否开启页面缓存 */
|
||||
keepAlive?: boolean;
|
||||
/* 有些路由我们并不想在菜单中显示,比如某些编辑页面。 */
|
||||
|
@ -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);
|
||||
}
|
||||
|
13
src/views/permission/justSuper/index.vue
Normal file
13
src/views/permission/justSuper/index.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
只有超级管理员可见
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
14
src/views/permission/permission/index.vue
Normal file
14
src/views/permission/permission/index.vue
Normal 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>
|
@ -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: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user