f
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
This commit is contained in:
19
packages/utils/README.md
Normal file
19
packages/utils/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# @vben/utils
|
||||
|
||||
用于多个 `app` 公用的工具包,继承了 `@vben-core/shared/utils` 的所有能力。业务上有通用的工具函数可以放在这里。
|
||||
|
||||
## 用法
|
||||
|
||||
### 添加依赖
|
||||
|
||||
```bash
|
||||
# 进入目标应用目录,例如 apps/xxxx-app
|
||||
# cd apps/xxxx-app
|
||||
pnpm add @vben/utils
|
||||
```
|
||||
|
||||
### 使用
|
||||
|
||||
```ts
|
||||
import { isString } from '@vben/utils';
|
||||
```
|
||||
27
packages/utils/package.json
Normal file
27
packages/utils/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@vben/utils",
|
||||
"version": "5.5.4",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/utils"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"vue-router": "catalog:"
|
||||
}
|
||||
}
|
||||
37
packages/utils/src/helpers/find-menu-by-path.ts
Normal file
37
packages/utils/src/helpers/find-menu-by-path.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
function findMenuByPath(
|
||||
list: MenuRecordRaw[],
|
||||
path?: string,
|
||||
): MenuRecordRaw | null {
|
||||
for (const menu of list) {
|
||||
if (menu.path === path) {
|
||||
return menu;
|
||||
}
|
||||
const findMenu = menu.children && findMenuByPath(menu.children, path);
|
||||
if (findMenu) {
|
||||
return findMenu;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根菜单
|
||||
* @param menus
|
||||
* @param path
|
||||
*/
|
||||
function findRootMenuByPath(menus: MenuRecordRaw[], path?: string, level = 0) {
|
||||
const findMenu = findMenuByPath(menus, path);
|
||||
const rootMenuPath = findMenu?.parents?.[level];
|
||||
const rootMenu = rootMenuPath
|
||||
? menus.find((item) => item.path === rootMenuPath)
|
||||
: undefined;
|
||||
return {
|
||||
findMenu,
|
||||
rootMenu,
|
||||
rootMenuPath,
|
||||
};
|
||||
}
|
||||
|
||||
export { findMenuByPath, findRootMenuByPath };
|
||||
81
packages/utils/src/helpers/generate-menus.ts
Normal file
81
packages/utils/src/helpers/generate-menus.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { Router, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { filterTree, mapTree } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* 根据 routes 生成菜单列表
|
||||
* @param routes
|
||||
*/
|
||||
async function generateMenus(
|
||||
routes: RouteRecordRaw[],
|
||||
router: Router,
|
||||
): Promise<MenuRecordRaw[]> {
|
||||
// 将路由列表转换为一个以 name 为键的对象映射
|
||||
// 获取所有router最终的path及name
|
||||
const finalRoutesMap: { [key: string]: string } = Object.fromEntries(
|
||||
router.getRoutes().map(({ name, path }) => [name, path]),
|
||||
);
|
||||
|
||||
let menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
|
||||
// 路由表的路径写法有多种,这里从router获取到最终的path并赋值
|
||||
const path = finalRoutesMap[route.name as string] ?? route.path;
|
||||
|
||||
// 转换为菜单结构
|
||||
// const path = matchRoute?.path ?? route.path;
|
||||
const { meta, name: routeName, redirect, children } = route;
|
||||
const {
|
||||
activeIcon,
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
hideChildrenInMenu = false,
|
||||
icon,
|
||||
link,
|
||||
order,
|
||||
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 || path : link || path;
|
||||
return {
|
||||
activeIcon,
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
icon,
|
||||
name,
|
||||
order,
|
||||
parent: route.parent,
|
||||
parents: route.parents,
|
||||
path: resultPath as string,
|
||||
show: !route?.meta?.hideInMenu,
|
||||
children: resultChildren || [],
|
||||
};
|
||||
});
|
||||
|
||||
// 对菜单进行排序,避免order=0时被替换成999的问题
|
||||
menus = menus.sort((a, b) => (a?.order ?? 999) - (b?.order ?? 999));
|
||||
|
||||
const finalMenus = filterTree(menus, (menu) => {
|
||||
return !!menu.show;
|
||||
});
|
||||
return finalMenus;
|
||||
}
|
||||
|
||||
export { generateMenus };
|
||||
86
packages/utils/src/helpers/generate-routes-backend.ts
Normal file
86
packages/utils/src/helpers/generate-routes-backend.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import type {
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
RouteRecordStringComponent,
|
||||
} from '@vben-core/typings';
|
||||
|
||||
import { mapTree } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* 动态生成路由 - 后端方式
|
||||
*/
|
||||
async function generateRoutesByBackend(
|
||||
options: GenerateMenuAndRoutesOptions,
|
||||
): Promise<RouteRecordRaw[]> {
|
||||
const { fetchMenuListAsync, layoutMap = {}, pageMap = {} } = options;
|
||||
|
||||
try {
|
||||
const menuRoutes = await fetchMenuListAsync?.();
|
||||
if (!menuRoutes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizePageMap: ComponentRecordType = {};
|
||||
|
||||
for (const [key, value] of Object.entries(pageMap)) {
|
||||
normalizePageMap[normalizeViewPath(key)] = value;
|
||||
}
|
||||
|
||||
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
|
||||
|
||||
return routes;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function convertRoutes(
|
||||
routes: RouteRecordStringComponent[],
|
||||
layoutMap: ComponentRecordType,
|
||||
pageMap: ComponentRecordType,
|
||||
): RouteRecordRaw[] {
|
||||
return mapTree(routes, (node) => {
|
||||
const route = node as unknown as RouteRecordRaw;
|
||||
const { component, name } = node;
|
||||
|
||||
if (!name) {
|
||||
console.error('route name is required', route);
|
||||
}
|
||||
|
||||
// layout转换
|
||||
if (component && layoutMap[component]) {
|
||||
route.component = layoutMap[component];
|
||||
// 页面组件转换
|
||||
} else if (component) {
|
||||
const normalizePath = normalizeViewPath(component);
|
||||
const pageKey = normalizePath.endsWith('.vue')
|
||||
? normalizePath
|
||||
: `${normalizePath}.vue`;
|
||||
if (pageMap[pageKey]) {
|
||||
route.component = pageMap[pageKey];
|
||||
} else {
|
||||
console.error(`route component is invalid: ${pageKey}`, route);
|
||||
route.component = pageMap['/_core/fallback/not-found.vue'];
|
||||
}
|
||||
}
|
||||
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeViewPath(path: string): string {
|
||||
// 去除相对路径前缀
|
||||
const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, '');
|
||||
|
||||
// 确保路径以 '/' 开头
|
||||
const viewPath = normalizedPath.startsWith('/')
|
||||
? normalizedPath
|
||||
: `/${normalizedPath}`;
|
||||
|
||||
// 这里耦合了vben-admin的目录结构
|
||||
return viewPath.replace(/^\/views/, '');
|
||||
}
|
||||
export { generateRoutesByBackend };
|
||||
58
packages/utils/src/helpers/generate-routes-frontend.ts
Normal file
58
packages/utils/src/helpers/generate-routes-frontend.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { filterTree, mapTree } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* 动态生成路由 - 前端方式
|
||||
*/
|
||||
async function generateRoutesByFrontend(
|
||||
routes: RouteRecordRaw[],
|
||||
roles: string[],
|
||||
forbiddenComponent?: RouteRecordRaw['component'],
|
||||
): Promise<RouteRecordRaw[]> {
|
||||
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
||||
const finalRoutes = filterTree(routes, (route) => {
|
||||
return hasAuthority(route, roles);
|
||||
});
|
||||
|
||||
if (!forbiddenComponent) {
|
||||
return finalRoutes;
|
||||
}
|
||||
|
||||
// 如果有禁止访问的页面,将禁止访问的页面替换为403页面
|
||||
return mapTree(finalRoutes, (route) => {
|
||||
if (menuHasVisibleWithForbidden(route)) {
|
||||
route.component = forbiddenComponent;
|
||||
}
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否有权限访问
|
||||
* @param route
|
||||
* @param access
|
||||
*/
|
||||
function hasAuthority(route: RouteRecordRaw, access: string[]) {
|
||||
const authority = route.meta?.authority;
|
||||
if (!authority) {
|
||||
return true;
|
||||
}
|
||||
const canAccess = access.some((value) => authority.includes(value));
|
||||
|
||||
return canAccess || (!canAccess && menuHasVisibleWithForbidden(route));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否在菜单中显示,但是访问会被重定向到403
|
||||
* @param route
|
||||
*/
|
||||
function menuHasVisibleWithForbidden(route: RouteRecordRaw) {
|
||||
return (
|
||||
!!route.meta?.authority &&
|
||||
Reflect.has(route.meta || {}, 'menuVisibleWithForbidden') &&
|
||||
!!route.meta?.menuVisibleWithForbidden
|
||||
);
|
||||
}
|
||||
|
||||
export { generateRoutesByFrontend, hasAuthority };
|
||||
10
packages/utils/src/helpers/get-popup-container.ts
Normal file
10
packages/utils/src/helpers/get-popup-container.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* If the node is holding inside a form, return the form element,
|
||||
* otherwise return the parent node of the given element or
|
||||
* the document body if the element is not provided.
|
||||
*/
|
||||
export function getPopupContainer(node?: HTMLElement): HTMLElement {
|
||||
return (
|
||||
node?.closest('form') ?? (node?.parentNode as HTMLElement) ?? document.body
|
||||
);
|
||||
}
|
||||
8
packages/utils/src/helpers/index.ts
Normal file
8
packages/utils/src/helpers/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './find-menu-by-path';
|
||||
export * from './generate-menus';
|
||||
export * from './generate-routes-backend';
|
||||
export * from './generate-routes-frontend';
|
||||
export * from './get-popup-container';
|
||||
export * from './merge-route-modules';
|
||||
export * from './reset-routes';
|
||||
export * from './unmount-global-loading';
|
||||
28
packages/utils/src/helpers/merge-route-modules.ts
Normal file
28
packages/utils/src/helpers/merge-route-modules.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
// 定义模块类型
|
||||
interface RouteModuleType {
|
||||
default: RouteRecordRaw[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并动态路由模块的默认导出
|
||||
* @param routeModules 动态导入的路由模块对象
|
||||
* @returns 合并后的路由配置数组
|
||||
*/
|
||||
function mergeRouteModules(
|
||||
routeModules: Record<string, unknown>,
|
||||
): RouteRecordRaw[] {
|
||||
const mergedRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
for (const routeModule of Object.values(routeModules)) {
|
||||
const moduleRoutes = (routeModule as RouteModuleType)?.default ?? [];
|
||||
mergedRoutes.push(...moduleRoutes);
|
||||
}
|
||||
|
||||
return mergedRoutes;
|
||||
}
|
||||
|
||||
export { mergeRouteModules };
|
||||
|
||||
export type { RouteModuleType };
|
||||
31
packages/utils/src/helpers/reset-routes.ts
Normal file
31
packages/utils/src/helpers/reset-routes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Router, RouteRecordName, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { traverseTreeValues } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* @zh_CN 重置所有路由,如有指定白名单除外
|
||||
*/
|
||||
export function resetStaticRoutes(router: Router, routes: RouteRecordRaw[]) {
|
||||
// 获取静态路由所有节点包含子节点的 name,并排除不存在 name 字段的路由
|
||||
const staticRouteNames = traverseTreeValues<
|
||||
RouteRecordRaw,
|
||||
RouteRecordName | undefined
|
||||
>(routes, (route) => {
|
||||
// 这些路由需要指定 name,防止在路由重置时,不能删除没有指定 name 的路由
|
||||
if (!route.name) {
|
||||
console.warn(
|
||||
`The route with the path ${route.path} needs to have the field name specified.`,
|
||||
);
|
||||
}
|
||||
return route.name;
|
||||
});
|
||||
|
||||
const { getRoutes, hasRoute, removeRoute } = router;
|
||||
const allRoutes = getRoutes();
|
||||
allRoutes.forEach(({ name }) => {
|
||||
// 存在于路由表且非白名单才需要删除
|
||||
if (name && !staticRouteNames.includes(name) && hasRoute(name)) {
|
||||
removeRoute(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
31
packages/utils/src/helpers/unmount-global-loading.ts
Normal file
31
packages/utils/src/helpers/unmount-global-loading.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 移除并销毁loading
|
||||
* 放在这里是而不是放在 index.html 的app标签内,是因为这样比较不会生硬,渲染过快可能会有闪烁
|
||||
* 通过先添加css动画隐藏,在动画结束后在移除loading节点来改善体验
|
||||
* 不好的地方是会增加一些代码量
|
||||
* 自定义loading可以见:https://doc.vben.pro/guide/in-depth/loading.html
|
||||
*/
|
||||
export function unmountGlobalLoading() {
|
||||
// 查找全局 loading 元素
|
||||
const loadingElement = document.querySelector('#__app-loading__');
|
||||
|
||||
if (loadingElement) {
|
||||
// 添加隐藏类,触发过渡动画
|
||||
loadingElement.classList.add('hidden');
|
||||
|
||||
// 查找所有需要移除的注入 loading 元素
|
||||
const injectLoadingElements = document.querySelectorAll(
|
||||
'[data-app-loading^="inject"]',
|
||||
);
|
||||
|
||||
// 当过渡动画结束时,移除 loading 元素和所有注入的 loading 元素
|
||||
loadingElement.addEventListener(
|
||||
'transitionend',
|
||||
() => {
|
||||
loadingElement.remove(); // 移除 loading 元素
|
||||
injectLoadingElements.forEach((el) => el.remove()); // 移除所有注入的 loading 元素
|
||||
},
|
||||
{ once: true },
|
||||
); // 确保事件只触发一次
|
||||
}
|
||||
}
|
||||
4
packages/utils/src/index.ts
Normal file
4
packages/utils/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './helpers';
|
||||
export * from '@vben-core/shared/cache';
|
||||
export * from '@vben-core/shared/color';
|
||||
export * from '@vben-core/shared/utils';
|
||||
9
packages/utils/tsconfig.json
Normal file
9
packages/utils/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@vben-core/typings/vue-router"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user