fix(template-07): 移动端顶部菜单展开后无法滚动

菜单面板留在 sticky 的 <header> 内,踩了两个叠加的 CSS 陷阱:

1) <header> 带 sticky(position:sticky + z-50)会自建层叠上下文,
   面板在其中 z 写多大都只能在 header 内部排序,整体仍被 DOM 靠后的
   <main>(Hero 为 relative,同样自建上下文)盖住。
2) sticky 的 header 会成为后代 fixed 的包含块——fixed 定位基准从视口
   退化成 header 的盒子并被其高度裁剪。原实现面板是普通流式元素,高度
   由内容撑开(汇吉采 6 个带子菜单的栏目约 1000px)直接溢出屏幕,而
   header 内没有可滚动容器,于是「展开即滚不动」,底部栏目够不着。

修复:面板 Teleport 到 body 脱离 header 的层叠上下文;面板改 fixed 并
四边锚定(inset-x-0 top-16 bottom-0 + overflow-y-auto + overscroll-contain),
top/bottom 同时钉住后高度才由视口决定,长菜单在面板内滚动。

实测(390x844 设备模拟):面板可滚到底,header 固定、背景页不动;
桌面 1440x900 无回归。约束与「只写 top-16 不够」的警告已写进组件注释。

Refs: .scratch/hjc-web-mobile-menu-scroll/issues/01-fix-mobile-menu-scroll.md
This commit is contained in:
2026-10-08 16:35:24 +08:00
parent 623316865b
commit 1b3e020800
+51 -32
View File
@@ -88,48 +88,50 @@
</div>
<!-- 移动端菜单 -->
<div
v-if="mobileMenuOpen"
class="lg:hidden bg-white border-t border-gray-100"
>
<div class="container mx-auto px-4 py-4 space-y-1">
<template v-for="item in navItems" :key="item.navigationId || item.path">
<!-- 有子菜单 -->
<div v-if="item.children && item.children.length > 0">
<div class="px-4 py-3 text-base font-semibold text-gray-900">
{{ item.title }}
<Teleport to="body">
<div
v-if="mobileMenuOpen"
class="fixed inset-x-0 top-16 bottom-0 z-[60] overflow-y-auto overscroll-contain lg:hidden bg-white border-t border-gray-100"
>
<div class="container mx-auto px-4 py-4 space-y-1">
<template v-for="item in navItems" :key="item.navigationId || item.path">
<!-- 有子菜单 -->
<div v-if="item.children && item.children.length > 0">
<div class="px-4 py-3 text-base font-semibold text-gray-900">
{{ item.title }}
</div>
<NuxtLink
v-for="child in item.children"
:key="child.navigationId"
:to="getChildPath(child)"
class="block px-8 py-2 text-sm text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
@click="mobileMenuOpen = false"
>
{{ child.title }}
</NuxtLink>
</div>
<!-- 无子菜单 -->
<NuxtLink
v-for="child in item.children"
:key="child.navigationId"
:to="getChildPath(child)"
class="block px-8 py-2 text-sm text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
v-else
:to="getNavLink(item)"
:target="item.target === '_blank' ? '_blank' : undefined"
class="block px-4 py-3 text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
active-class="text-blue-600 bg-blue-50"
@click="mobileMenuOpen = false"
>
{{ child.title }}
{{ item.title }}
</NuxtLink>
</div>
<!-- 无子菜单 -->
</template>
<NuxtLink
v-else
:to="getNavLink(item)"
:target="item.target === '_blank' ? '_blank' : undefined"
class="block px-4 py-3 text-base font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
active-class="text-blue-600 bg-blue-50"
to="/contact"
class="block px-4 py-3 mt-2 text-center bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors"
@click="mobileMenuOpen = false"
>
{{ item.title }}
在线咨询
</NuxtLink>
</template>
<NuxtLink
to="/contact"
class="block px-4 py-3 mt-2 text-center bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors"
@click="mobileMenuOpen = false"
>
在线咨询
</NuxtLink>
</div>
</div>
</div>
</Teleport>
</header>
</template>
@@ -143,6 +145,23 @@ import type { CmsNavigation } from '~/types'
const { siteInfo, siteName, siteLogo, siteIcon, navigations, showHeaderSearch, fetchSiteInfo } = useSite()
const mobileMenuOpen = ref(false)
/*
* 移动端菜单(模板里 `v-if="mobileMenuOpen"` 那块)为什么长这样——两个坑都踩过,别改回去:
*
* 1) 必须 <Teleport to="body">,不能留在 <header> 内。
* <header> 带 `sticky`(position:sticky + z-50),这会让它**自建层叠上下文**;
* 面板留在里面时,z 写多大都只在 header 内部排序,整体仍被 DOM 靠后的
* <main>(Hero 是 relative,同样自建上下文)盖住 → 菜单"展开了却看不见"。
*
* 2) 面板必须是 `fixed` 且四边锚定(inset-x-0 top-16 bottom-0 + overflow-y-auto)。
* <header> 是 sticky 时会成为后代 fixed 的**包含块**,fixed 的定位基准从视口
* 退化成 header 的盒子并被其高度裁剪。菜单项多起来时(汇吉采 6 个带子菜单的栏目,
* 展开约 1000px)面板会撑出屏幕,而 header 里没有可滚动容器 →
* 「展开即滚不动」,底部栏目在任何机型上都够不着(原缺陷)。
* top/bottom 同时钉住后高度才由视口决定,长菜单才能在面板内滚动。
* ⚠️ 只写 `top-16` 不够:高度仍锁在 header 的 h-16 内,症状原样复现。
*/
/** 子导航链接统一走 app/utils 的 getNavLink(自动避免 /page/4598?navId=4598 冗余拼接) */
function getChildPath(child: CmsNavigation): string {
return getNavLink(child)