From df35a90321204f56ec4386504075c46bc57a0e9e Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Sun, 30 Aug 2026 18:09:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=8F=9C=E5=8D=95=E5=88=87=E9=A1=B5?= =?UTF-8?q?=E5=85=88=E4=BA=AE=E6=A0=87=E7=AD=BE=E5=86=8D=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=EF=BC=8C=E5=87=8F=E8=BD=BB=E9=A1=B6=E6=A0=8F=E5=8D=A1=E9=A1=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/components/tab-bar/index.vue | 3 +- src/components/tab-bar/tab-item.vue | 16 ++++ src/components/top-nav/index.vue | 121 +++++++++++++++++++++++----- src/store/modules/tab-bar/index.ts | 26 ++++++ src/store/modules/tab-bar/types.ts | 3 + 5 files changed, 147 insertions(+), 22 deletions(-) diff --git a/src/components/tab-bar/index.vue b/src/components/tab-bar/index.vue index dfef6e6..db8bd3a 100644 --- a/src/components/tab-bar/index.vue +++ b/src/components/tab-bar/index.vue @@ -117,6 +117,7 @@ watch( () => route.fullPath, async () => { + tabBarStore.clearPreview(); await nextTick(); scrollActiveTagIntoView(); }, @@ -240,7 +241,7 @@ overflow-y: hidden; display: flex; align-items: center; - scroll-behavior: smooth; + scroll-behavior: auto; &::-webkit-scrollbar { height: 0; diff --git a/src/components/tab-bar/tab-item.vue b/src/components/tab-bar/tab-item.vue index bca1dc8..b9201e6 100644 --- a/src/components/tab-bar/tab-item.vue +++ b/src/components/tab-bar/tab-item.vue @@ -108,6 +108,21 @@ } const isActive = computed(() => { + const previewPath = tabBarStore.previewingPath; + if (previewPath || tabBarStore.previewingName) { + if ( + previewPath && + (props.itemData.fullPath === previewPath || + stripEphemeralQuery(props.itemData.fullPath) === + stripEphemeralQuery(previewPath)) + ) { + return true; + } + return ( + !!tabBarStore.previewingName && + props.itemData.name === tabBarStore.previewingName + ); + } if (route.fullPath === props.itemData.fullPath) return true; return ( stripEphemeralQuery(route.fullPath) === @@ -117,6 +132,7 @@ const goto = (tag: TagProps) => { if (isActive.value) return; + tabBarStore.previewNavigate(tag); // 只带路由必要字段,避免把 title 等无关字段推进 history if (tag.name) { router.push({ diff --git a/src/components/top-nav/index.vue b/src/components/top-nav/index.vue index 398b951..0928bcd 100644 --- a/src/components/top-nav/index.vue +++ b/src/components/top-nav/index.vue @@ -25,9 +25,10 @@ trigger="hover" position="bl" :popup-offset="0" - :mouse-enter-delay="100" - :mouse-leave-delay="180" - @popup-visible-change="onSubmenuPopupVisibleChange" + :popup-visible="openedSubmenuKey === menuKey(item)" + :mouse-enter-delay="80" + :mouse-leave-delay="navPaintLock ? 0 : 180" + @popup-visible-change="(v: boolean) => onSubmenuPopupVisibleChange(menuKey(item), v)" > @@ -321,11 +327,10 @@ async function onTopMenuClick(e: MouseEvent, item: any) { e.preventDefault(); const children = filterPage(item.children); if (children.length > 0) { - // 有二级:点击一级直接进第一个可见子页(比空点更快) - goto(children[0]); + paintNavThenGo(children[0]); return; } - goto(item); + paintNavThenGo(item); } async function onTogglePrintConnection(value: string | number | boolean) { @@ -655,14 +660,23 @@ function isSubMenuActive(item: any) { } /** 弹出二级/更多菜单时,把当前页对应项滚入可视区 */ -function onSubmenuPopupVisibleChange(visible: boolean) { +const openedSubmenuKey = ref(''); +const navPaintLock = ref(false); +let navPaintUnlockTimer = 0; + +function onSubmenuPopupVisibleChange(key: string, visible: boolean) { + if (navPaintLock.value) { + openedSubmenuKey.value = ''; + return; + } + openedSubmenuKey.value = visible ? key : ''; if (!visible) return; nextTick(() => { requestAnimationFrame(() => { const el = document.querySelector( '.nav-submenu-panel .child-menu-item.active, .nav-more-panel .nav-more-item.active', ) as HTMLElement | null; - el?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + el?.scrollIntoView({ block: 'nearest', behavior: 'auto' }); }); }); } @@ -870,9 +884,15 @@ const goto = (item: any) => { openPortalSideDrawer(portalPanel); return; } + + if (router.currentRoute.value.path === targetPath) { + tabBarStore.clearPreview(); + return; + } // 使用路由 path 跳转 router.push(targetPath).catch((error) => { + tabBarStore.clearPreview(); console.error(`路由跳转失败: path=${targetPath}`, error); // 尝试使用路由名称跳转(从映射中获取,而不是使用 item.name,因为 item.name 是中文显示名称) const routeName = pathToRouteNameMap[targetPath]; @@ -893,9 +913,72 @@ const goto = (item: any) => { }); }; -/** 子菜单点击后跳转(路由变化会关闭弹层) */ +function prefetchMenuRoute(item: any) { + const rawPath = item?.path || item?.route; + if (!rawPath || regexUrl.test(rawPath)) return; + const targetPath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`; + try { + const resolved = router.resolve(targetPath); + const rec = resolved.matched[resolved.matched.length - 1]; + const loader = rec?.components?.default as unknown; + if (typeof loader === 'function') { + const result = (loader as () => unknown)(); + if (result && typeof (result as Promise).then === 'function') { + (result as Promise).catch(() => undefined); + } + } + } catch { + // 预取失败不影响点击跳转 + } +} + +function previewMenuNavigation(item: any) { + const rawPath = item?.path || item?.route; + if (!rawPath || regexUrl.test(rawPath)) return; + const targetPath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`; + if (resolvePortalDrawerPanel(targetPath)) return; + try { + const resolved = router.resolve(targetPath); + const name = String(resolved.name || ''); + if (!name) return; + tabBarStore.previewNavigate({ + title: formatMenuTitle(item) || name, + name, + fullPath: resolved.fullPath || targetPath, + query: resolved.query, + }); + } catch { + // 解析失败则等路由完成后再亮标签 + } +} + +/** 先关弹层、亮标签,下一帧再走路由,避免页面重渲染把顶栏一起卡住 */ +let navGoingTo = ''; +function paintNavThenGo(item: any) { + const rawPath = String(item?.path || item?.route || ''); + if (navGoingTo && navGoingTo === rawPath) return; + navGoingTo = rawPath; + openedSubmenuKey.value = ''; + navPaintLock.value = true; + if (navPaintUnlockTimer) { + window.clearTimeout(navPaintUnlockTimer); + } + previewMenuNavigation(item); + nextTick(() => { + requestAnimationFrame(() => { + goto(item); + navPaintUnlockTimer = window.setTimeout(() => { + navPaintLock.value = false; + navPaintUnlockTimer = 0; + navGoingTo = ''; + }, 280); + }); + }); +} + +/** 子菜单点击后跳转(先关弹层,路由变化不再等页面) */ const gotoFromSubmenu = (item: any) => { - goto(item); + paintNavThenGo(item); }; const showBalanceNav = computed(() => rbacUserStore.isCustomerPortal()); @@ -966,14 +1049,6 @@ watch( { flush: 'post' }, ); -watch( - () => route.fullPath, - async () => { - await nextTick(); - await recalcMenuOverflow(); - }, -); - function onWindowResize() { void recalcMenuOverflow(); } @@ -990,6 +1065,10 @@ onMounted(async () => { onUnmounted(() => { window.removeEventListener(CUSTOMER_BALANCE_CHANGED_EVENT, handleCustomerBalanceChanged); window.removeEventListener('resize', onWindowResize); + if (navPaintUnlockTimer) { + window.clearTimeout(navPaintUnlockTimer); + navPaintUnlockTimer = 0; + } }); diff --git a/src/store/modules/tab-bar/index.ts b/src/store/modules/tab-bar/index.ts index f3b4e8a..2781fca 100644 --- a/src/store/modules/tab-bar/index.ts +++ b/src/store/modules/tab-bar/index.ts @@ -62,6 +62,8 @@ const useTabBarStore = defineStore('tabBar', { state: (): TabBarState => ({ cacheTabList: new Set([DEFAULT_ROUTE_NAME]), tagList: [DEFAULT_ROUTE], + previewingPath: '', + previewingName: '', }), getters: { @@ -107,6 +109,28 @@ const useTabBarStore = defineStore('tabBar', { this.touchCache(String(route.name)); } }, + /** 菜单/标签点击立刻高亮,不等路由 afterEach */ + previewNavigate(tag: TagProps) { + if (!tag?.fullPath && !tag?.name) return; + this.previewingPath = tag.fullPath || ''; + this.previewingName = tag.name || ''; + const exists = this.tagList.some( + (t) => + (tag.fullPath && t.fullPath === tag.fullPath) || + (tag.name && t.name === tag.name), + ); + if (!exists && tag.name) { + this.tagList.push(tag); + if (!tag.ignoreCache) { + this.touchCache(tag.name); + } + } + }, + clearPreview() { + if (!this.previewingPath && !this.previewingName) return; + this.previewingPath = ''; + this.previewingName = ''; + }, deleteTag(idx: number, tag: TagProps) { this.tagList.splice(idx, 1); this.cacheTabList.delete(tag.name); @@ -137,6 +161,8 @@ const useTabBarStore = defineStore('tabBar', { this.tagList = [DEFAULT_ROUTE]; this.cacheTabList.clear(); this.cacheTabList.add(DEFAULT_ROUTE_NAME); + this.previewingPath = ''; + this.previewingName = ''; }, delVisitedRoute(path: string) { const idx = this.tagList.findIndex(tag => tag.fullPath === path); diff --git a/src/store/modules/tab-bar/types.ts b/src/store/modules/tab-bar/types.ts index 29c1c53..4b2e365 100644 --- a/src/store/modules/tab-bar/types.ts +++ b/src/store/modules/tab-bar/types.ts @@ -9,4 +9,7 @@ export interface TagProps { export interface TabBarState { tagList: TagProps[]; cacheTabList: Set; + /** 菜单点击后、路由完成前的预亮标签,避免顶栏等页面渲染完才动 */ + previewingPath: string; + previewingName: string; }