mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-23 14:46:18 +08:00
feat: add some test case
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/helpers": "workspace:*",
|
||||
"@vben-core/preferences": "workspace:*",
|
||||
"@vben-core/stores": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
@@ -30,6 +31,7 @@
|
||||
"@vben/icons": "workspace:*",
|
||||
"@vben/layouts": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/request": "workspace:*",
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
|
139
apps/antd-view/src/router/guard.ts
Normal file
139
apps/antd-view/src/router/guard.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { generatorMenus, generatorRoutes } from '@vben-core/helpers';
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import { useAccessStore } from '@vben-core/stores';
|
||||
import type { RouteLocationNormalized, Router } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { $t } from '@vben/locales';
|
||||
import { startProgress, stopProgress } from '@vben/utils';
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
import { dynamicRoutes } from '@/router/routes';
|
||||
|
||||
/**
|
||||
* 通用守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function configCommonGuard(router: Router) {
|
||||
// 记录已经加载的页面
|
||||
const loadedPaths = new Set<string>();
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
// 页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
startProgress();
|
||||
}
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
return true;
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||
loadedPaths.add(to.path);
|
||||
|
||||
// 关闭页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
stopProgress();
|
||||
}
|
||||
|
||||
// 动态修改标题
|
||||
if (preferences.app.dynamicTitle) {
|
||||
const { title } = to.meta;
|
||||
useTitle(`${$t(title)} - ${preferences.app.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 不需要权限的页面白名单
|
||||
const WHITE_ROUTE_NAMES = new Set<string>([]);
|
||||
|
||||
/**
|
||||
* 跳转登录页面
|
||||
* @param to
|
||||
*/
|
||||
function loginPageMeta(to: RouteLocationNormalized) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限访问守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function configAccessGuard(router: Router) {
|
||||
router.beforeEach(async (to, from) => {
|
||||
const accessStore = useAccessStore();
|
||||
const accessToken = accessStore.getAccessToken;
|
||||
|
||||
// accessToken 检查
|
||||
if (!accessToken) {
|
||||
if (to.path === '/') {
|
||||
return loginPageMeta(to);
|
||||
}
|
||||
|
||||
// 明确声明忽略权限访问权限,则可以访问
|
||||
if (to.meta.ignoreAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 白名单路由列表检查
|
||||
// TODO: 不是很需要,通过 ignoreAccess 也可以做到,考虑删除
|
||||
if (WHITE_ROUTE_NAMES.has(to.name as string)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 没有访问权限,跳转登录页面
|
||||
if (to.fullPath !== LOGIN_PATH) {
|
||||
return loginPageMeta(to);
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
const accessRoutes = accessStore.getAccessRoutes;
|
||||
|
||||
// 是否已经生成过动态路由
|
||||
if (accessRoutes && accessRoutes.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 生成路由表
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userRoles = accessStore.getUserRoles;
|
||||
const routes = await generatorRoutes(dynamicRoutes, userRoles);
|
||||
// 动态添加到router实例内
|
||||
routes.forEach((route) => router.addRoute(route));
|
||||
|
||||
const menus = await generatorMenus(routes, router);
|
||||
|
||||
// 保存菜单信息和路由信息
|
||||
accessStore.setAccessMenus(menus);
|
||||
accessStore.setAccessRoutes(routes);
|
||||
const redirectPath = (from.query.redirect || to.path) as string;
|
||||
const redirect = decodeURIComponent(redirectPath);
|
||||
|
||||
return {
|
||||
path: redirect,
|
||||
replace: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export { configAccessGuard };
|
||||
|
||||
/**
|
||||
* 项目守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function createRouterGuard(router: Router) {
|
||||
/** 通用 */
|
||||
configCommonGuard(router);
|
||||
/** 权限访问 */
|
||||
configAccessGuard(router);
|
||||
}
|
||||
|
||||
export { createRouterGuard };
|
@@ -1,184 +0,0 @@
|
||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben/types';
|
||||
|
||||
import { useAccessStore } from '@vben-core/stores';
|
||||
import type { RouteRecordRaw, Router } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { filterTree, mapTree, traverseTreeValues } from '@vben/utils';
|
||||
|
||||
import { dynamicRoutes } from '@/router/routes';
|
||||
|
||||
// 不需要权限的页面白名单
|
||||
const WHITE_ROUTE_NAMES = new Set<string>([]);
|
||||
|
||||
/**
|
||||
* 权限访问守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function configAccessGuard(router: Router) {
|
||||
router.beforeEach(async (to, from) => {
|
||||
const accessStore = useAccessStore();
|
||||
const accessToken = accessStore.getAccessToken;
|
||||
|
||||
// accessToken 检查
|
||||
if (!accessToken) {
|
||||
// 明确声明忽略权限访问权限,则可以访问
|
||||
if (to.meta.ignoreAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 白名单路由列表检查
|
||||
// TODO: 不是很需要,通过 ignoreAccess 也可以做到,考虑删除
|
||||
if (WHITE_ROUTE_NAMES.has(to.name as string)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 没有访问权限,跳转登录页面
|
||||
if (to.fullPath !== LOGIN_PATH) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
const accessRoutes = accessStore.getAccessRoutes;
|
||||
|
||||
// 是否已经生成过动态路由
|
||||
if (accessRoutes && accessRoutes.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 生成路由表
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userRoles = accessStore.getUserRoles;
|
||||
const routes = await generatorRoutes(userRoles);
|
||||
// 动态添加到router实例内
|
||||
routes.forEach((route) => router.addRoute(route));
|
||||
|
||||
const menus = await generatorMenus(routes, router);
|
||||
|
||||
// 保存菜单信息和路由信息
|
||||
accessStore.setAccessMenus(menus);
|
||||
accessStore.setAccessRoutes(routes);
|
||||
const redirectPath = (from.query.redirect || to.path) as string;
|
||||
const redirect = decodeURIComponent(redirectPath);
|
||||
|
||||
return {
|
||||
path: redirect,
|
||||
replace: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态生成路由
|
||||
*/
|
||||
async function generatorRoutes(roles: string[]): Promise<RouteRecordRaw[]> {
|
||||
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
||||
return filterTree(dynamicRoutes, (route) => {
|
||||
return hasVisible(route) && hasAuthority(route, roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 routes 生成菜单列表
|
||||
* @param routes
|
||||
*/
|
||||
async function generatorMenus(
|
||||
routes: RouteRecordRaw[],
|
||||
router: Router,
|
||||
): Promise<MenuRecordRaw[]> {
|
||||
// 获取所有router最终的path及name
|
||||
const finalRoutes = traverseTreeValues(
|
||||
router.getRoutes(),
|
||||
({ name, path }) => {
|
||||
return {
|
||||
name,
|
||||
path,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
|
||||
// 路由表的路径写法有多种,这里从router获取到最终的path并赋值
|
||||
const matchRoute = finalRoutes.find(
|
||||
(finalRoute) => finalRoute.name === route.name,
|
||||
);
|
||||
|
||||
// 转换为菜单结构
|
||||
const path = matchRoute?.path ?? route.path;
|
||||
const { meta, name: routeName, redirect, children } = route;
|
||||
const {
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
hideChildrenInMenu = false,
|
||||
icon,
|
||||
orderNo,
|
||||
target,
|
||||
title = '',
|
||||
} = meta || {};
|
||||
|
||||
const name = (title || routeName || '') as string;
|
||||
|
||||
// 隐藏子菜单
|
||||
const resultChildren = hideChildrenInMenu
|
||||
? []
|
||||
: (children as MenuRecordRaw[]);
|
||||
|
||||
// 将菜单的所有父级和父级菜单记录到菜单项内
|
||||
if (resultChildren && resultChildren.length > 0) {
|
||||
resultChildren.forEach((child) => {
|
||||
child.parents = [...(route.parents || []), path];
|
||||
child.parent = path;
|
||||
});
|
||||
}
|
||||
// 隐藏子菜单
|
||||
const resultPath = hideChildrenInMenu ? redirect : target || path;
|
||||
return {
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
icon,
|
||||
name,
|
||||
orderNo,
|
||||
parent: route.parent,
|
||||
parents: route.parents,
|
||||
path: resultPath,
|
||||
children: resultChildren,
|
||||
};
|
||||
});
|
||||
|
||||
return menus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否有权限访问
|
||||
* @param route
|
||||
* @param access
|
||||
*/
|
||||
function hasAuthority(route: RouteRecordRaw, access: string[]) {
|
||||
const authority = route.meta?.authority;
|
||||
if (!authority) {
|
||||
return true;
|
||||
}
|
||||
const authSet = new Set(authority);
|
||||
return access.some((value) => {
|
||||
return authSet.has(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否需要在菜单中显示
|
||||
* @param route
|
||||
*/
|
||||
function hasVisible(route: RouteRecordRaw) {
|
||||
return !route.meta?.hideInMenu;
|
||||
}
|
||||
|
||||
export { configAccessGuard };
|
@@ -1,55 +0,0 @@
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import type { Router } from 'vue-router';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { startProgress, stopProgress } from '@vben/utils';
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
import { configAccessGuard } from './access';
|
||||
|
||||
/**
|
||||
* 通用守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function configCommonGuard(router: Router) {
|
||||
// 记录已经加载的页面
|
||||
const loadedPaths = new Set<string>();
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
// 页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
startProgress();
|
||||
}
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
return true;
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||
loadedPaths.add(to.path);
|
||||
|
||||
// 关闭页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
stopProgress();
|
||||
}
|
||||
|
||||
// 动态修改标题
|
||||
if (preferences.app.dynamicTitle) {
|
||||
const { title } = to.meta;
|
||||
useTitle(`${$t(title)} - ${preferences.app.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function createRouteGuard(router: Router) {
|
||||
/** 通用 */
|
||||
configCommonGuard(router);
|
||||
/** 权限访问 */
|
||||
configAccessGuard(router);
|
||||
}
|
||||
|
||||
export { createRouteGuard };
|
@@ -3,7 +3,7 @@ import type { RouteRecordName, RouteRecordRaw } from 'vue-router';
|
||||
import { traverseTreeValues } from '@vben/utils';
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
import { createRouteGuard } from './guard';
|
||||
import { createRouterGuard } from './guard';
|
||||
import { staticRoutes } from './routes';
|
||||
|
||||
/**
|
||||
@@ -54,6 +54,6 @@ function resetRoutes() {
|
||||
});
|
||||
}
|
||||
// 创建路由守卫
|
||||
createRouteGuard(router);
|
||||
createRouterGuard(router);
|
||||
|
||||
export { resetRoutes, router };
|
||||
|
@@ -1,24 +1,17 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import {
|
||||
// beforeEach,
|
||||
describe,
|
||||
// expect,
|
||||
it,
|
||||
} from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
// import { useAccessStore } from '../modules/access';
|
||||
import { useCounterStore } from './example';
|
||||
|
||||
describe('useCounterStore', () => {
|
||||
it('app Name with test', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
// let referenceStore = usePreferencesStore();
|
||||
});
|
||||
|
||||
// beforeEach(() => {
|
||||
// referenceStore = usePreferencesStore();
|
||||
// });
|
||||
it('count test', () => {
|
||||
setActivePinia(createPinia());
|
||||
const counterStore = useCounterStore();
|
||||
|
||||
// expect(referenceStore.appName).toBe('vben-admin');
|
||||
// referenceStore.setAppName('vbenAdmin');
|
||||
// expect(referenceStore.getAppName).toBe('vbenAdmin');
|
||||
expect(counterStore.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
@@ -9,5 +9,6 @@ export const useCounterStore = defineStore('counter', {
|
||||
getters: {
|
||||
double: (state) => state.count * 2,
|
||||
},
|
||||
persist: [],
|
||||
state: () => ({ count: 0 }),
|
||||
});
|
||||
|
@@ -5,8 +5,8 @@ import { useAccessStore } from '@vben-core/stores';
|
||||
|
||||
import { getUserInfo, userLogin } from '@/services';
|
||||
import { AuthenticationLogin } from '@vben/common-ui';
|
||||
import { useRequest } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useRequest } from '@vben/request';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -35,6 +35,7 @@ const { loading: userInfoLoading, runAsync: runGetUserInfo } = useRequest(
|
||||
async function handleLogin(values: LoginAndRegisterParams) {
|
||||
// 异步处理用户登录操作并获取 accessToken
|
||||
// Asynchronously handle the user login operation and obtain the accessToken
|
||||
|
||||
const { accessToken } = await runUserLogin(values);
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
|
Reference in New Issue
Block a user