feat(projects): 权限完善及权限示例页面

This commit is contained in:
Soybean
2022-04-23 02:21:02 +08:00
parent b9c5c34979
commit 807448aec5
27 changed files with 287 additions and 114 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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')) {

View File

@ -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';

View File

@ -8,7 +8,7 @@ const about: AuthRoute.Route = {
singleLayout: 'basic',
permissions: ['super', 'admin', 'test'],
icon: 'fluent:book-information-24-regular',
order: 7
order: 8
}
};

View 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;

View File

@ -37,7 +37,7 @@ const exception: AuthRoute.Route = {
meta: {
title: '异常页',
icon: 'ant-design:exception-outlined',
order: 5
order: 6
}
};

View File

@ -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;
}

View File

@ -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;
}
}
});

View File

@ -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;
}
}
});

View File

@ -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);
}

View File

@ -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;
/** 开启请求代理 */

View File

@ -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'

View File

@ -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>;
}
}
/** 主题相关类型 */

View File

@ -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;

View File

@ -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] : [];
}

View File

@ -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 - 单个权限路由

View File

@ -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';

View 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>

View 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>