fix: 菜单切页先亮标签再渲染,减轻顶栏卡顿
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cf4d2c4a3f
commit
df35a90321
@ -117,6 +117,7 @@
|
|||||||
watch(
|
watch(
|
||||||
() => route.fullPath,
|
() => route.fullPath,
|
||||||
async () => {
|
async () => {
|
||||||
|
tabBarStore.clearPreview();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
scrollActiveTagIntoView();
|
scrollActiveTagIntoView();
|
||||||
},
|
},
|
||||||
@ -240,7 +241,7 @@
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: auto;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
height: 0;
|
height: 0;
|
||||||
|
|||||||
@ -108,6 +108,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isActive = computed(() => {
|
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;
|
if (route.fullPath === props.itemData.fullPath) return true;
|
||||||
return (
|
return (
|
||||||
stripEphemeralQuery(route.fullPath) ===
|
stripEphemeralQuery(route.fullPath) ===
|
||||||
@ -117,6 +132,7 @@
|
|||||||
|
|
||||||
const goto = (tag: TagProps) => {
|
const goto = (tag: TagProps) => {
|
||||||
if (isActive.value) return;
|
if (isActive.value) return;
|
||||||
|
tabBarStore.previewNavigate(tag);
|
||||||
// 只带路由必要字段,避免把 title 等无关字段推进 history
|
// 只带路由必要字段,避免把 title 等无关字段推进 history
|
||||||
if (tag.name) {
|
if (tag.name) {
|
||||||
router.push({
|
router.push({
|
||||||
|
|||||||
@ -25,9 +25,10 @@
|
|||||||
trigger="hover"
|
trigger="hover"
|
||||||
position="bl"
|
position="bl"
|
||||||
:popup-offset="0"
|
:popup-offset="0"
|
||||||
:mouse-enter-delay="100"
|
:popup-visible="openedSubmenuKey === menuKey(item)"
|
||||||
:mouse-leave-delay="180"
|
:mouse-enter-delay="80"
|
||||||
@popup-visible-change="onSubmenuPopupVisibleChange"
|
:mouse-leave-delay="navPaintLock ? 0 : 180"
|
||||||
|
@popup-visible-change="(v: boolean) => onSubmenuPopupVisibleChange(menuKey(item), v)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
:ref="(el) => setMenuItemRef(menuKey(item), el)"
|
:ref="(el) => setMenuItemRef(menuKey(item), el)"
|
||||||
@ -55,7 +56,9 @@
|
|||||||
:key="childKey(menuItem, childItem)"
|
:key="childKey(menuItem, childItem)"
|
||||||
class="child-menu-item"
|
class="child-menu-item"
|
||||||
:class="{ active: isSubMenuActive(menuItem) }"
|
:class="{ active: isSubMenuActive(menuItem) }"
|
||||||
|
@pointerenter="prefetchMenuRoute(menuItem)"
|
||||||
@pointerdown.prevent="gotoFromSubmenu(menuItem)"
|
@pointerdown.prevent="gotoFromSubmenu(menuItem)"
|
||||||
|
@click.prevent="gotoFromSubmenu(menuItem)"
|
||||||
>
|
>
|
||||||
<span class="child-menu-item__icon">
|
<span class="child-menu-item__icon">
|
||||||
<component
|
<component
|
||||||
@ -88,9 +91,10 @@
|
|||||||
trigger="hover"
|
trigger="hover"
|
||||||
position="br"
|
position="br"
|
||||||
:popup-offset="0"
|
:popup-offset="0"
|
||||||
:mouse-enter-delay="100"
|
:popup-visible="openedSubmenuKey === MORE_MENU_KEY"
|
||||||
:mouse-leave-delay="180"
|
:mouse-enter-delay="80"
|
||||||
@popup-visible-change="onSubmenuPopupVisibleChange"
|
:mouse-leave-delay="navPaintLock ? 0 : 180"
|
||||||
|
@popup-visible-change="(v: boolean) => onSubmenuPopupVisibleChange(MORE_MENU_KEY, v)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
:ref="(el) => setMenuItemRef(MORE_MENU_KEY, el)"
|
:ref="(el) => setMenuItemRef(MORE_MENU_KEY, el)"
|
||||||
@ -119,7 +123,9 @@
|
|||||||
:key="childKey(child, childIdx)"
|
:key="childKey(child, childIdx)"
|
||||||
class="nav-more-item"
|
class="nav-more-item"
|
||||||
:class="{ active: isSubMenuActive(child) }"
|
:class="{ active: isSubMenuActive(child) }"
|
||||||
|
@pointerenter="prefetchMenuRoute(child)"
|
||||||
@pointerdown.prevent="gotoFromSubmenu(child)"
|
@pointerdown.prevent="gotoFromSubmenu(child)"
|
||||||
|
@click.prevent="gotoFromSubmenu(child)"
|
||||||
>
|
>
|
||||||
<span class="nav-more-item__icon">
|
<span class="nav-more-item__icon">
|
||||||
<component :is="getSubMenuIcon(child, item, menuNameToLocaleKeyMap)" />
|
<component :is="getSubMenuIcon(child, item, menuNameToLocaleKeyMap)" />
|
||||||
@ -321,11 +327,10 @@ async function onTopMenuClick(e: MouseEvent, item: any) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const children = filterPage(item.children);
|
const children = filterPage(item.children);
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
// 有二级:点击一级直接进第一个可见子页(比空点更快)
|
paintNavThenGo(children[0]);
|
||||||
goto(children[0]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
goto(item);
|
paintNavThenGo(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTogglePrintConnection(value: string | number | boolean) {
|
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;
|
if (!visible) return;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const el = document.querySelector(
|
const el = document.querySelector(
|
||||||
'.nav-submenu-panel .child-menu-item.active, .nav-more-panel .nav-more-item.active',
|
'.nav-submenu-panel .child-menu-item.active, .nav-more-panel .nav-more-item.active',
|
||||||
) as HTMLElement | null;
|
) 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);
|
openPortalSideDrawer(portalPanel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (router.currentRoute.value.path === targetPath) {
|
||||||
|
tabBarStore.clearPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 使用路由 path 跳转
|
// 使用路由 path 跳转
|
||||||
router.push(targetPath).catch((error) => {
|
router.push(targetPath).catch((error) => {
|
||||||
|
tabBarStore.clearPreview();
|
||||||
console.error(`路由跳转失败: path=${targetPath}`, error);
|
console.error(`路由跳转失败: path=${targetPath}`, error);
|
||||||
// 尝试使用路由名称跳转(从映射中获取,而不是使用 item.name,因为 item.name 是中文显示名称)
|
// 尝试使用路由名称跳转(从映射中获取,而不是使用 item.name,因为 item.name 是中文显示名称)
|
||||||
const routeName = pathToRouteNameMap[targetPath];
|
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<unknown>).then === 'function') {
|
||||||
|
(result as Promise<unknown>).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) => {
|
const gotoFromSubmenu = (item: any) => {
|
||||||
goto(item);
|
paintNavThenGo(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showBalanceNav = computed(() => rbacUserStore.isCustomerPortal());
|
const showBalanceNav = computed(() => rbacUserStore.isCustomerPortal());
|
||||||
@ -966,14 +1049,6 @@ watch(
|
|||||||
{ flush: 'post' },
|
{ flush: 'post' },
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.fullPath,
|
|
||||||
async () => {
|
|
||||||
await nextTick();
|
|
||||||
await recalcMenuOverflow();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function onWindowResize() {
|
function onWindowResize() {
|
||||||
void recalcMenuOverflow();
|
void recalcMenuOverflow();
|
||||||
}
|
}
|
||||||
@ -990,6 +1065,10 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener(CUSTOMER_BALANCE_CHANGED_EVENT, handleCustomerBalanceChanged);
|
window.removeEventListener(CUSTOMER_BALANCE_CHANGED_EVENT, handleCustomerBalanceChanged);
|
||||||
window.removeEventListener('resize', onWindowResize);
|
window.removeEventListener('resize', onWindowResize);
|
||||||
|
if (navPaintUnlockTimer) {
|
||||||
|
window.clearTimeout(navPaintUnlockTimer);
|
||||||
|
navPaintUnlockTimer = 0;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -62,6 +62,8 @@ const useTabBarStore = defineStore('tabBar', {
|
|||||||
state: (): TabBarState => ({
|
state: (): TabBarState => ({
|
||||||
cacheTabList: new Set([DEFAULT_ROUTE_NAME]),
|
cacheTabList: new Set([DEFAULT_ROUTE_NAME]),
|
||||||
tagList: [DEFAULT_ROUTE],
|
tagList: [DEFAULT_ROUTE],
|
||||||
|
previewingPath: '',
|
||||||
|
previewingName: '',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@ -107,6 +109,28 @@ const useTabBarStore = defineStore('tabBar', {
|
|||||||
this.touchCache(String(route.name));
|
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) {
|
deleteTag(idx: number, tag: TagProps) {
|
||||||
this.tagList.splice(idx, 1);
|
this.tagList.splice(idx, 1);
|
||||||
this.cacheTabList.delete(tag.name);
|
this.cacheTabList.delete(tag.name);
|
||||||
@ -137,6 +161,8 @@ const useTabBarStore = defineStore('tabBar', {
|
|||||||
this.tagList = [DEFAULT_ROUTE];
|
this.tagList = [DEFAULT_ROUTE];
|
||||||
this.cacheTabList.clear();
|
this.cacheTabList.clear();
|
||||||
this.cacheTabList.add(DEFAULT_ROUTE_NAME);
|
this.cacheTabList.add(DEFAULT_ROUTE_NAME);
|
||||||
|
this.previewingPath = '';
|
||||||
|
this.previewingName = '';
|
||||||
},
|
},
|
||||||
delVisitedRoute(path: string) {
|
delVisitedRoute(path: string) {
|
||||||
const idx = this.tagList.findIndex(tag => tag.fullPath === path);
|
const idx = this.tagList.findIndex(tag => tag.fullPath === path);
|
||||||
|
|||||||
@ -9,4 +9,7 @@ export interface TagProps {
|
|||||||
export interface TabBarState {
|
export interface TabBarState {
|
||||||
tagList: TagProps[];
|
tagList: TagProps[];
|
||||||
cacheTabList: Set<string>;
|
cacheTabList: Set<string>;
|
||||||
|
/** 菜单点击后、路由完成前的预亮标签,避免顶栏等页面渲染完才动 */
|
||||||
|
previewingPath: string;
|
||||||
|
previewingName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user