From fdff8c7fe7a8fcc0801eb918a8994d31d60110a7 Mon Sep 17 00:00:00 2001 From: XiaoDaiGua-Ray <443547225@qq.com> Date: Sun, 10 Dec 2023 14:53:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96menu=20store=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E5=88=B7=E6=96=B0=E3=80=81=E8=B7=B3=E8=BD=AC=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 + src/components/RTable/src/components/C.tsx | 2 +- src/router/appRouteModules.ts | 2 +- src/router/routes.ts | 2 +- src/store/modules/menu/index.ts | 115 ++++++++++++--------- src/utils/basic.ts | 82 +++++++++++++++ 6 files changed, 152 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ded944..25130593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ - `changeMenuModelValue` - 现在方法支持第三个参数配置跳转时,是否携带参数 - 避免递归查找的时候,一些不必要的操作,优化性能 +- 核心模块 `Menu` 的优化细节 + - 使用 `router.getRoutes` 方法替代以前的递归查找(`updateMenuKeyWhenRouteUpdate` 方法) + - 优化当菜单更新时、url 地址更新时都会重复检查的问题,现在检查是惰性的 ## Fixes diff --git a/src/components/RTable/src/components/C.tsx b/src/components/RTable/src/components/C.tsx index dd1a3462..0f808896 100644 --- a/src/components/RTable/src/components/C.tsx +++ b/src/components/RTable/src/components/C.tsx @@ -33,7 +33,7 @@ import type { MaybeArray } from '@/types/modules/utils' type FixedClick = (type: 'left' | 'right', option: C, index: number) => void const renderSwitcherIcon = () => ( - + ) const RowIconRender = ({ diff --git a/src/router/appRouteModules.ts b/src/router/appRouteModules.ts index be151799..e5bf1991 100644 --- a/src/router/appRouteModules.ts +++ b/src/router/appRouteModules.ts @@ -28,4 +28,4 @@ import { expandRoutes } from '@/router/helper/expandRoutes' export const getAppRawRoutes = () => orderRoutes(combineRawRouteModules()) /** 获取所有平铺展开的路由 */ -export const appExpandRoutes = expandRoutes(getAppRawRoutes()) +export const appExpandRoutes = () => expandRoutes(getAppRawRoutes()) diff --git a/src/router/routes.ts b/src/router/routes.ts index 406fec65..b7e7db28 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -25,7 +25,7 @@ export default async () => { name: 'layout', redirect: getRootPath.value, component: Layout, - children: appExpandRoutes, + children: appExpandRoutes(), }, ] } diff --git a/src/store/modules/menu/index.ts b/src/store/modules/menu/index.ts index 91392e46..ad3a3928 100644 --- a/src/store/modules/menu/index.ts +++ b/src/store/modules/menu/index.ts @@ -35,7 +35,6 @@ import { } from './helper' import { useI18n } from '@/hooks/web' import { getAppRawRoutes } from '@/router/appRouteModules' -import { useVueRouter } from '@/hooks/web' import { throttle } from 'lodash-es' import { useKeepAliveActions } from '@/store' @@ -47,7 +46,7 @@ import type { LocationQuery } from 'vue-router' export const piniaMenuStore = defineStore( 'menu', () => { - const { router } = useVueRouter() + const router = useRouter() const route = useRoute() const { t } = useI18n() const { setKeepAliveInclude } = useKeepAliveActions() @@ -61,6 +60,40 @@ export const piniaMenuStore = defineStore( currentMenuOption: null, // 当前激活菜单项 }) const isSetupAppMenuLock = ref(true) + const isRootPathReg = new RegExp('/', 'g') + + const resolveOption = (option: AppMenuOption) => { + const { meta } = option + + /** 设置 label, i18nKey 优先级最高 */ + const label = computed(() => + meta?.i18nKey ? t(`${meta!.i18nKey}`) : meta?.noLocalTitle, + ) + /** 拼装菜单项 */ + const route = { + ...option, + key: option.path, + label: () => + h(NEllipsis, null, { + default: () => label.value, + }), + breadcrumbLabel: label.value, + /** 检查该菜单项是否展示 */ + } as AppMenuOption + /** 合并 icon */ + const attr: AppMenuOption = Object.assign({}, route, { + icon: hasMenuIcon(option), + }) + + if (option.path === getCatchMenuKey()) { + /** 设置标签页(初始化时执行设置一次, 避免含有平级路由模式情况时出现不能正确设置标签页的情况) */ + setMenuTagOptionsWhenMenuValueChange(option.path, attr) + } + + attr.show = validMenuItemShow(attr) + + return attr + } /** * @@ -138,9 +171,21 @@ export const piniaMenuStore = defineStore( * * @param key 菜单更新后的 key * @param option 菜单当前 option 项 + * @param query 路由参数 * * @remark 修改 `menu key` 后的回调函数 * @remark 修改后, 缓存当前选择 key 并且存储标签页与跳转页面(router push 操作) + * + * 如果 windowOpen 存在, 则直接打开新窗口,不会更新当前菜单状态,也不会做其他的操作 + * 如果 sameLevel 存在,则会追加一层面包屑,并不会触发菜单更新与标签页更新 + * + * 在执行更新操作后会做一些缓存操作 + * + * 该方法是整个模板的核心驱动: 菜单、标签页、面包屑、浏览器标题等等的更新方法 + * + * @example + * changeMenuModelValue('/dashboard',{ dashboard option }) // 跳转页面至 dashboard,并且更新菜单状态、标签页、面包屑、浏览器标题等等 + * changeMenuModelValue('/dashboard', { dashboard option }, { id: 1 }) // 执行更新操作,并且传递参数 */ const changeMenuModelValue = ( key: string | number, @@ -178,7 +223,7 @@ export const piniaMenuStore = defineStore( } /** 检查是否为根路由 */ - const count = (path.match(new RegExp('/', 'g')) || []).length + const count = (path.match(isRootPathReg) || []).length /** 更新缓存队列 */ setKeepAliveInclude(option as unknown as AppMenuOption) @@ -225,23 +270,24 @@ export const piniaMenuStore = defineStore( combinePath = splitPath[splitPath.length - 1] } - const findMenuOption = (pathKey: string, options: AppMenuOption[]) => { - for (const curr of options) { - if (curr.children?.length) { - findMenuOption(pathKey, curr.children) - - continue - } - - if (pathKey === curr.key && !curr?.children?.length) { - changeMenuModelValue(pathKey, curr, query) - - break - } - } + // 如果当前菜单 key 与路由地址相同,说明不是手动更新 url, 则不会触发更新 + if (combinePath === menuState.menuKey) { + return } - findMenuOption(combinePath, menuState.options) + const findMenuOption = router + .getRoutes() + .find((curr) => + count > 1 ? path === curr.path : combinePath === curr.path, + ) + + if (findMenuOption) { + changeMenuModelValue( + count > 1 ? combinePath : path, + resolveOption(findMenuOption as unknown as AppMenuOption), + query, + ) + } } /** @@ -251,39 +297,6 @@ export const piniaMenuStore = defineStore( */ const setupAppMenu = () => { return new Promise((resolve) => { - const resolveOption = (option: AppMenuOption) => { - const { meta } = option - - /** 设置 label, i18nKey 优先级最高 */ - const label = computed(() => - meta?.i18nKey ? t(`${meta!.i18nKey}`) : meta?.noLocalTitle, - ) - /** 拼装菜单项 */ - const route = { - ...option, - key: option.path, - label: () => - h(NEllipsis, null, { - default: () => label.value, - }), - breadcrumbLabel: label.value, - /** 检查该菜单项是否展示 */ - } as AppMenuOption - /** 合并 icon */ - const attr: AppMenuOption = Object.assign({}, route, { - icon: hasMenuIcon(option), - }) - - if (option.path === getCatchMenuKey()) { - /** 设置标签页(初始化时执行设置一次, 避免含有平级路由模式情况时出现不能正确设置标签页的情况) */ - setMenuTagOptionsWhenMenuValueChange(option.path, attr) - } - - attr.show = validMenuItemShow(attr) - - return attr - } - const resolveRoutes = (routes: AppMenuOption[], index: number) => { const catchArr: AppMenuOption[] = [] diff --git a/src/utils/basic.ts b/src/utils/basic.ts index 1ea97b7e..d4948f07 100644 --- a/src/utils/basic.ts +++ b/src/utils/basic.ts @@ -6,6 +6,7 @@ import type { ValidateValueType, DownloadAnyFileDataType, BasicTypes, + AnyFC, } from '@/types/modules/utils' import type { BasicTarget, TargetValue } from '@/types/modules/vue' @@ -256,3 +257,84 @@ export const omit = , K extends keyof T>( return targetObject } + +/** + * + * @param value 待判断的值 + * + * 判断是否为 Promise 函数 + * + * @example + * isPromise(Promise.resolve(123)) => true + * isPromise(() => {}) => false + * isPromise(123) => false + */ +export const isPromise = (value: unknown): value is Promise => { + return ( + !!value && + (typeof value === 'object' || typeof value === 'function') && + typeof (value as Promise).then === 'function' + ) +} + +/** + * + * @param fc 正常执行的函数 + * @param errorCallback 错误回调 + * @param args 当前传递函数参数 + * + * 用于捕获函数执行时的错误,如果有错误,则执行错误回调 + * + * @example + * callWithErrorHandling((x: number) => { return x }, () => {}, [123]) => 123 + * callWithErrorHandling((x: number) => { throw new Error('error') }, (error) => { console.log(error) }, [123]) => undefined + */ +export const callWithErrorHandling = ( + fc: T, + errorCallback: AnyFC, + args?: Parameters, +) => { + let result: ReturnType | undefined + + try { + result = args ? fc(...args) : fc() + } catch (error) { + errorCallback(error as E) + } + + return result +} + +/** + * + * @param fn 正常执行的函数 + * @param errorCallback 错误回调 + * @param args 当前传递函数参数 + * + * 用于捕获异步函数执行时的错误,如果有错误,则执行错误回调 + * + * @example + * callWithAsyncErrorHandling(async () => { console.log('A') }, () => {}, []) => Promise { undefined } + * callWithAsyncErrorHandling(() => { throw new Error('error') }, (error) => { console.log(error) }, []) => undefined + * callWithAsyncErrorHandling(async () => { return Promise.resolve('hello') }, () => {}, []) => Promise { 'hello' } + */ +export const callWithAsyncErrorHandling = async < + T extends AnyFC, + E extends Error, +>( + fc: T, + errorCallback: (error: E) => void, + args?: Parameters, +) => { + try { + if (!isPromise(fc)) { + return Promise.resolve(callWithErrorHandling(fc, errorCallback, args)) + } + + return await fc(...(args as Parameters)) + } catch (error) { + errorCallback(error as E) + + return void 0 + } +}