mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
feat(projects): 权限完善及权限示例页面
This commit is contained in:
@ -1,9 +1,11 @@
|
||||
import type { App } from 'vue';
|
||||
import setupNetworkDirective from './network';
|
||||
import setupLoginDirective from './login';
|
||||
import setupPermissionDirective from './permission';
|
||||
|
||||
/** setup custom vue directives. - [安装自定义的vue指令] */
|
||||
export function setupDirectives(app: App) {
|
||||
setupNetworkDirective(app);
|
||||
setupLoginDirective(app);
|
||||
setupPermissionDirective(app);
|
||||
}
|
||||
|
@ -1,16 +1,28 @@
|
||||
import type { App, Directive } from 'vue';
|
||||
import { useAuthStore } from '@/store';
|
||||
import { isArray, isString } from '@/utils';
|
||||
|
||||
export default function setupLoginDirective(app: App) {
|
||||
export default function setupPermissionDirective(app: App) {
|
||||
const auth = useAuthStore();
|
||||
|
||||
const loginDirective: Directive<HTMLElement, Auth.RoleType | undefined> = {
|
||||
const permissionDirective: Directive<HTMLElement, Auth.RoleType | Auth.RoleType[]> = {
|
||||
mounted(el: HTMLElement, binding) {
|
||||
if (binding.value !== auth.userInfo.userRole) {
|
||||
const { userRole } = auth.userInfo;
|
||||
const elPermission = binding.value;
|
||||
let hasPermission = userRole === 'super';
|
||||
if (!hasPermission) {
|
||||
if (isArray(elPermission)) {
|
||||
hasPermission = (elPermission as Auth.RoleType[]).includes(userRole);
|
||||
}
|
||||
if (isString(elPermission)) {
|
||||
hasPermission = (elPermission as Auth.RoleType) === userRole;
|
||||
}
|
||||
}
|
||||
if (!hasPermission) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
app.directive('login', loginDirective);
|
||||
app.directive('permission', permissionDirective);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export async function createDynamicRouteGuard(
|
||||
const isLogin = Boolean(getToken());
|
||||
|
||||
// 初始化权限路由
|
||||
if (!route.isInitedAuthRoute) {
|
||||
if (!route.isInitAuthRoute) {
|
||||
// 未登录情况下直接回到登录页,登录成功后再加载权限路由
|
||||
if (!isLogin) {
|
||||
if (to.name === routeName('login')) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { App } from 'vue';
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';
|
||||
import { transformAuthRoutesToVueRoutes } from '@/utils';
|
||||
import { transformAuthRoutesToVueRoutes, transformRouteNameToRoutePath } from '@/utils';
|
||||
import { constantRoutes } from './routes';
|
||||
import { scrollBehavior } from './helpers';
|
||||
import { createRouterGuard } from './guard';
|
||||
@ -20,5 +20,10 @@ export async function setupRouter(app: App) {
|
||||
await router.isReady();
|
||||
}
|
||||
|
||||
/** 路由名称 */
|
||||
export const routeName = (key: AuthRoute.RouteKey) => key;
|
||||
/** 路由路径 */
|
||||
export const routePath = (key: Exclude<AuthRoute.RouteKey, 'not-found-page'>) => transformRouteNameToRoutePath(key);
|
||||
|
||||
export * from './routes';
|
||||
export * from './modules';
|
||||
|
@ -8,7 +8,7 @@ const about: AuthRoute.Route = {
|
||||
singleLayout: 'basic',
|
||||
permissions: ['super', 'admin', 'test'],
|
||||
icon: 'fluent:book-information-24-regular',
|
||||
order: 7
|
||||
order: 8
|
||||
}
|
||||
};
|
||||
|
||||
|
35
src/router/modules/auth-demo.ts
Normal file
35
src/router/modules/auth-demo.ts
Normal file
@ -0,0 +1,35 @@
|
||||
const authDemo: AuthRoute.Route = {
|
||||
name: 'auth-demo',
|
||||
path: '/auth-demo',
|
||||
component: 'basic',
|
||||
children: [
|
||||
{
|
||||
name: 'auth-demo_permission',
|
||||
path: '/auth-demo/permission',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '指令和权限切换',
|
||||
requiresAuth: true,
|
||||
icon: 'ic:round-construction'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'auth-demo_super',
|
||||
path: '/auth-demo/super',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '超级管理员可见',
|
||||
requiresAuth: true,
|
||||
permissions: ['super'],
|
||||
icon: 'ic:round-supervisor-account'
|
||||
}
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
title: '权限示例',
|
||||
icon: 'ic:baseline-security',
|
||||
order: 5
|
||||
}
|
||||
};
|
||||
|
||||
export default authDemo;
|
@ -37,7 +37,7 @@ const exception: AuthRoute.Route = {
|
||||
meta: {
|
||||
title: '异常页',
|
||||
icon: 'ant-design:exception-outlined',
|
||||
order: 5
|
||||
order: 6
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,7 @@ export const constantRoutes: AuthRoute.Route[] = [
|
||||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect: '/dashboard/analysis',
|
||||
redirect: import.meta.env.VITE_ROUTE_HOME_PATH,
|
||||
meta: {
|
||||
title: 'Root'
|
||||
}
|
||||
@ -64,16 +64,3 @@ export const constantRoutes: AuthRoute.Route[] = [
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/** 路由名称 */
|
||||
export const routeName = (key: AuthRoute.RouteKey) => key;
|
||||
|
||||
/** 路由路径 */
|
||||
export function routePath(key: Exclude<AuthRoute.RouteKey, 'not-found-page'>): AuthRoute.RoutePath {
|
||||
const rootPath: AuthRoute.RoutePath = '/';
|
||||
if (key === 'root') return rootPath;
|
||||
const splitMark: AuthRoute.RouteSplitMark = '_';
|
||||
const pathSplitMark = '/';
|
||||
const path = key.split(splitMark).join(pathSplitMark);
|
||||
return (pathSplitMark + path) as AuthRoute.RoutePath;
|
||||
}
|
||||
|
@ -87,6 +87,9 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
await this.loginByToken(data);
|
||||
}
|
||||
this.loginLoading = false;
|
||||
},
|
||||
updateUserRole(userRole: Auth.RoleType) {
|
||||
this.userInfo.userRole = userRole;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -7,8 +7,11 @@ import {
|
||||
transformAuthRouteToMenu,
|
||||
transformAuthRoutesToVueRoutes,
|
||||
transformAuthRoutesToSearchMenus,
|
||||
getCacheRoutes
|
||||
getCacheRoutes,
|
||||
filterAuthRoutesByUserPermission,
|
||||
transformRoutePathToRouteName
|
||||
} from '@/utils';
|
||||
import { useAuthStore } from '../auth';
|
||||
import { useTabStore } from '../tab';
|
||||
|
||||
interface RouteState {
|
||||
@ -19,7 +22,7 @@ interface RouteState {
|
||||
*/
|
||||
authRouteMode: ImportMetaEnv['VITE_AUTH_ROUTE_MODE'];
|
||||
/** 是否初始化了权限路由 */
|
||||
isInitedAuthRoute: boolean;
|
||||
isInitAuthRoute: boolean;
|
||||
/** 路由首页name(前端静态路由时生效,后端动态路由该值会被后端返回的值覆盖) */
|
||||
routeHomeName: AuthRoute.RouteKey;
|
||||
/** 菜单 */
|
||||
@ -33,8 +36,8 @@ interface RouteState {
|
||||
export const useRouteStore = defineStore('route-store', {
|
||||
state: (): RouteState => ({
|
||||
authRouteMode: import.meta.env.VITE_AUTH_ROUTE_MODE,
|
||||
isInitedAuthRoute: false,
|
||||
routeHomeName: 'dashboard_analysis',
|
||||
isInitAuthRoute: false,
|
||||
routeHomeName: transformRoutePathToRouteName(import.meta.env.VITE_ROUTE_HOME_PATH),
|
||||
menus: [],
|
||||
searchMenus: [],
|
||||
cacheRoutes: []
|
||||
@ -73,9 +76,9 @@ export const useRouteStore = defineStore('route-store', {
|
||||
* @param router - 路由实例
|
||||
*/
|
||||
async initStaticRoute(router: Router) {
|
||||
// 先根据用户权限过滤一下staticRoutes
|
||||
|
||||
this.handleAuthRoutes(staticRoutes, router);
|
||||
const auth = useAuthStore();
|
||||
const routes = filterAuthRoutesByUserPermission(staticRoutes, auth.userInfo.userRole);
|
||||
this.handleAuthRoutes(routes, router);
|
||||
},
|
||||
/**
|
||||
* 初始化权限路由
|
||||
@ -84,6 +87,7 @@ export const useRouteStore = defineStore('route-store', {
|
||||
async initAuthRoute(router: Router) {
|
||||
const { initHomeTab } = useTabStore();
|
||||
const { userId } = getUserInfo();
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
const isDynamicRoute = this.authRouteMode === 'dynamic';
|
||||
@ -94,7 +98,8 @@ export const useRouteStore = defineStore('route-store', {
|
||||
}
|
||||
|
||||
initHomeTab(this.routeHomeName, router);
|
||||
this.isInitedAuthRoute = true;
|
||||
|
||||
this.isInitAuthRoute = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -21,7 +21,7 @@ export const useTabStore = defineStore('tab-store', {
|
||||
name: 'root',
|
||||
path: '/',
|
||||
meta: {
|
||||
title: 'root'
|
||||
title: 'Root'
|
||||
},
|
||||
scrollPosition: {
|
||||
left: 0,
|
||||
@ -53,7 +53,8 @@ export const useTabStore = defineStore('tab-store', {
|
||||
initHomeTab(routeHomeName: string, router: Router) {
|
||||
const routes = router.getRoutes();
|
||||
const findHome = routes.find(item => item.name === routeHomeName);
|
||||
if (findHome) {
|
||||
if (findHome && !findHome.children) {
|
||||
// 有子路由的不能作为Tab
|
||||
this.homeTab = getTabRouteByVueRoute(findHome);
|
||||
}
|
||||
},
|
||||
@ -165,16 +166,20 @@ export const useTabStore = defineStore('tab-store', {
|
||||
iniTabStore(currentRoute: RouteLocationNormalizedLoaded) {
|
||||
const theme = useThemeStore();
|
||||
|
||||
const isHome = currentRoute.path === this.homeTab.path;
|
||||
const tabs: GlobalTabRoute[] = theme.tab.isCache ? getTabRoutes() : [];
|
||||
|
||||
const hasHome = isInTabRoutes(tabs, this.homeTab.path);
|
||||
const hasCurrent = isInTabRoutes(tabs, currentRoute.path);
|
||||
if (!hasHome) {
|
||||
if (!hasHome && this.homeTab.name !== 'root') {
|
||||
tabs.unshift(this.homeTab);
|
||||
}
|
||||
|
||||
const isHome = currentRoute.path === this.homeTab.path;
|
||||
const hasCurrent = isInTabRoutes(tabs, currentRoute.path);
|
||||
if (!isHome && !hasCurrent) {
|
||||
tabs.push(getTabRouteByVueRoute(currentRoute));
|
||||
const currentTab = getTabRouteByVueRoute(currentRoute);
|
||||
tabs.push(currentTab);
|
||||
}
|
||||
|
||||
this.tabs = tabs;
|
||||
this.setActiveTab(currentRoute.path);
|
||||
}
|
||||
|
5
src/typings/env.d.ts
vendored
5
src/typings/env.d.ts
vendored
@ -27,8 +27,11 @@ interface ImportMetaEnv {
|
||||
/**
|
||||
* 权限路由模式:
|
||||
* - static - 前端声明的静态
|
||||
* - dynamic - 后端返回的动态 */
|
||||
* - dynamic - 后端返回的动态
|
||||
*/
|
||||
readonly VITE_AUTH_ROUTE_MODE: 'static' | 'dynamic';
|
||||
/** 路由首页的路径 */
|
||||
readonly VITE_ROUTE_HOME_PATH: Exclude<AuthRoute.RoutePath, '/not-found-page' | '/:pathMatch(.*)*'>;
|
||||
/** vite环境类型 */
|
||||
readonly VITE_ENV_TYPE?: EnvType;
|
||||
/** 开启请求代理 */
|
||||
|
3
src/typings/route.d.ts
vendored
3
src/typings/route.d.ts
vendored
@ -36,6 +36,9 @@ declare namespace AuthRoute {
|
||||
| 'plugin_icon'
|
||||
| 'plugin_print'
|
||||
| 'plugin_swiper'
|
||||
| 'auth-demo'
|
||||
| 'auth-demo_permission'
|
||||
| 'auth-demo_super'
|
||||
| 'exception'
|
||||
| 'exception_403'
|
||||
| 'exception_404'
|
||||
|
8
src/typings/system.d.ts
vendored
8
src/typings/system.d.ts
vendored
@ -84,6 +84,14 @@ declare namespace Service {
|
||||
/** 接口消息 */
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** mock的响应option */
|
||||
interface MockOption {
|
||||
url: Record<string, any>;
|
||||
body: Record<string, any>;
|
||||
query: Record<string, any>;
|
||||
headers: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
/** 主题相关类型 */
|
||||
|
@ -37,7 +37,7 @@ export function getUserInfo() {
|
||||
userId: '',
|
||||
userName: '',
|
||||
userPhone: '',
|
||||
userRole: 'test'
|
||||
userRole: 'normal'
|
||||
};
|
||||
const userInfo: Auth.UserInfo = getLocal<Auth.UserInfo>(EnumStorageKey['user-info']) || emptyInfo;
|
||||
return userInfo;
|
||||
|
@ -1,16 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/**
|
||||
* 根据用户权限过滤路由
|
||||
* @param routes - 权限路由
|
||||
* @param permission - 权限
|
||||
*/
|
||||
export function filterAuthRoutesByUserPermission(routes: AuthRoute.Route[], permission: Auth.RoleType) {
|
||||
const filters: AuthRoute.Route[] = [];
|
||||
|
||||
routes.forEach(route => {
|
||||
filterAuthRouteByUserPermission(route, permission);
|
||||
});
|
||||
return filters;
|
||||
return routes.map(route => filterAuthRouteByUserPermission(route, permission)).flat(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -19,5 +13,12 @@ export function filterAuthRoutesByUserPermission(routes: AuthRoute.Route[], perm
|
||||
* @param permission - 权限
|
||||
*/
|
||||
function filterAuthRouteByUserPermission(route: AuthRoute.Route, permission: Auth.RoleType): AuthRoute.Route[] {
|
||||
return [];
|
||||
const hasPermission =
|
||||
!route.meta.permissions || permission === 'super' || route.meta.permissions.includes(permission);
|
||||
|
||||
if (route.children) {
|
||||
const filterChildren = route.children.map(item => filterAuthRouteByUserPermission(item, permission)).flat(1);
|
||||
Object.assign(route, { children: filterChildren });
|
||||
}
|
||||
return hasPermission ? [route] : [];
|
||||
}
|
||||
|
@ -31,6 +31,34 @@ export function transformAuthRoutesToSearchMenus(routes: AuthRoute.Route[], tree
|
||||
}, treeMap);
|
||||
}
|
||||
|
||||
/** 将路由名字转换成路由路径 */
|
||||
export function transformRouteNameToRoutePath(
|
||||
name: Exclude<AuthRoute.RouteKey, 'not-found-page'>
|
||||
): AuthRoute.RoutePath {
|
||||
const rootPath: AuthRoute.RoutePath = '/';
|
||||
if (name === 'root') return rootPath;
|
||||
|
||||
const splitMark: AuthRoute.RouteSplitMark = '_';
|
||||
const pathSplitMark = '/';
|
||||
const path = name.split(splitMark).join(pathSplitMark);
|
||||
|
||||
return (pathSplitMark + path) as AuthRoute.RoutePath;
|
||||
}
|
||||
|
||||
/** 将路由路径转换成路由名字 */
|
||||
export function transformRoutePathToRouteName(
|
||||
path: Exclude<AuthRoute.RoutePath, '/not-found-page' | '/:pathMatch(.*)*'>
|
||||
): AuthRoute.RouteKey {
|
||||
if (path === '/') return 'root';
|
||||
|
||||
const pathSplitMark = '/';
|
||||
const routeSplitMark: AuthRoute.RouteSplitMark = '_';
|
||||
|
||||
const name = path.split(pathSplitMark).slice(1).join(routeSplitMark) as AuthRoute.RouteKey;
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个权限路由转换成vue路由
|
||||
* @param item - 单个权限路由
|
||||
|
@ -1,6 +1,7 @@
|
||||
export * from './module';
|
||||
export * from './helpers';
|
||||
export * from './cache';
|
||||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './breadcrumb';
|
||||
export * from './tab';
|
||||
|
46
src/views/auth-demo/permission/index.vue
Normal file
46
src/views/auth-demo/permission/index.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="权限指令 v-permission" class="h-full shadow-sm rounded-16px">
|
||||
<div class="pb-12px">
|
||||
<n-gradient-text type="primary" :size="20">当前用户的权限:{{ auth.userInfo.userRole }}</n-gradient-text>
|
||||
</div>
|
||||
<n-space>
|
||||
<n-button v-permission="`super`">super可见</n-button>
|
||||
<n-button v-permission="`admin`">admin可见</n-button>
|
||||
<n-button v-permission="['admin', 'test']">admin和test可见</n-button>
|
||||
</n-space>
|
||||
<div class="py-12px">
|
||||
<n-gradient-text type="primary" :size="20">切换用户权限</n-gradient-text>
|
||||
</div>
|
||||
<n-select
|
||||
:value="auth.userInfo.userRole"
|
||||
class="w-120px"
|
||||
size="small"
|
||||
:options="roleList"
|
||||
@update:value="auth.updateUserRole"
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useAppStore, useAuthStore } from '@/store';
|
||||
|
||||
const app = useAppStore();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const roleList = [
|
||||
{ label: '超级管理员', value: 'super' },
|
||||
{ label: '管理员', value: 'admin' },
|
||||
{ label: '测试', value: 'test' }
|
||||
];
|
||||
|
||||
watch(
|
||||
() => auth.userInfo.userRole,
|
||||
async () => {
|
||||
app.reloadPage();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style scoped></style>
|
8
src/views/auth-demo/super/index.vue
Normal file
8
src/views/auth-demo/super/index.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="当前页面只有super才能看到" class="h-full shadow-sm rounded-16px"> </n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style scoped></style>
|
Reference in New Issue
Block a user