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:
@@ -88,48 +88,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 移动端菜单 -->
|
<!-- 移动端菜单 -->
|
||||||
<div
|
<Teleport to="body">
|
||||||
v-if="mobileMenuOpen"
|
<div
|
||||||
class="lg:hidden bg-white border-t border-gray-100"
|
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 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">
|
<div v-if="item.children && item.children.length > 0">
|
||||||
{{ item.title }}
|
<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>
|
</div>
|
||||||
|
<!-- 无子菜单 -->
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="child in item.children"
|
v-else
|
||||||
:key="child.navigationId"
|
:to="getNavLink(item)"
|
||||||
:to="getChildPath(child)"
|
:target="item.target === '_blank' ? '_blank' : undefined"
|
||||||
class="block px-8 py-2 text-sm text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
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"
|
@click="mobileMenuOpen = false"
|
||||||
>
|
>
|
||||||
{{ child.title }}
|
{{ item.title }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</template>
|
||||||
<!-- 无子菜单 -->
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-else
|
to="/contact"
|
||||||
:to="getNavLink(item)"
|
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"
|
||||||
: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"
|
@click="mobileMenuOpen = false"
|
||||||
>
|
>
|
||||||
{{ item.title }}
|
在线咨询
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</div>
|
||||||
<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>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -143,6 +145,23 @@ import type { CmsNavigation } from '~/types'
|
|||||||
const { siteInfo, siteName, siteLogo, siteIcon, navigations, showHeaderSearch, fetchSiteInfo } = useSite()
|
const { siteInfo, siteName, siteLogo, siteIcon, navigations, showHeaderSearch, fetchSiteInfo } = useSite()
|
||||||
const mobileMenuOpen = ref(false)
|
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 冗余拼接) */
|
/** 子导航链接统一走 app/utils 的 getNavLink(自动避免 /page/4598?navId=4598 冗余拼接) */
|
||||||
function getChildPath(child: CmsNavigation): string {
|
function getChildPath(child: CmsNavigation): string {
|
||||||
return getNavLink(child)
|
return getNavLink(child)
|
||||||
|
|||||||
Reference in New Issue
Block a user