1
0
Files
ag-index/nuxt-web/components/AppHeader.vue

265 lines
7.1 KiB
Vue
Raw Normal View History

2026-04-20 09:45:20 +08:00
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { topNavigation } from '~/data/navigation'
import { siteInfo } from '~/data/site'
type NavNode = (typeof topNavigation)[number]
type ChildNode = {
label: string
to?: string
children?: Array<{ label: string; to: string }>
}
const route = useRoute()
const isMenuOpen = ref(false)
const isMobile = ref(false)
const isSticky = ref(false)
const isStickyVisible = ref(false)
const expandedItems = ref<Record<string, boolean>>({})
let lastScrollTop = 0
const stickyThreshold = 1000
const getItemKey = (segments: string[]) => segments.join('::')
const hasChildren = (item: NavNode | ChildNode) => 'children' in item && Boolean(item.children?.length)
const updateStickyState = () => {
if (typeof window === 'undefined') {
return
}
const currentScrollTop = Math.max(window.scrollY, window.pageYOffset, document.documentElement.scrollTop, 0)
if (isMobile.value || currentScrollTop <= stickyThreshold) {
isSticky.value = false
isStickyVisible.value = false
lastScrollTop = currentScrollTop
return
}
isSticky.value = true
isStickyVisible.value = currentScrollTop < lastScrollTop
lastScrollTop = currentScrollTop
}
const updateViewport = () => {
if (typeof window === 'undefined') {
return
}
const mobile = window.innerWidth <= 991
isMobile.value = mobile
if (!mobile) {
isMenuOpen.value = false
expandedItems.value = {}
}
updateStickyState()
}
const setOverlay = (enabled: boolean) => {
if (typeof document === 'undefined') {
return
}
document.querySelector('.page-wrapper')?.classList.toggle('body-overlay', enabled)
}
const openMenu = () => {
isMenuOpen.value = true
}
const closeMenu = () => {
isMenuOpen.value = false
expandedItems.value = {}
}
const toggleExpanded = (key: string) => {
expandedItems.value[key] = !expandedItems.value[key]
}
const isExpanded = (key: string) => Boolean(expandedItems.value[key])
const matchesRoute = (to?: string) => {
if (!to) {
return false
}
if (to === '/') {
return route.path === '/'
}
return route.path === to || route.path.startsWith(`${to}/`)
}
const isItemActive = (item: NavNode | ChildNode) => {
if ('to' in item && matchesRoute(item.to)) {
return true
}
if (hasChildren(item)) {
return item.children.some((child) => isItemActive(child))
}
return false
}
const handleItemClick = (item: NavNode | ChildNode, key: string, event: MouseEvent) => {
if (!isMobile.value) {
if (!hasChildren(item)) {
closeMenu()
}
return
}
if (!hasChildren(item)) {
closeMenu()
return
}
const itemHasRoute = 'to' in item && Boolean(item.to)
const currentlyExpanded = isExpanded(key)
if (!currentlyExpanded) {
event.preventDefault()
toggleExpanded(key)
return
}
if (!itemHasRoute) {
event.preventDefault()
toggleExpanded(key)
}
}
watch(isMenuOpen, setOverlay)
watch(
() => route.fullPath,
() => {
closeMenu()
updateStickyState()
}
)
onMounted(() => {
updateViewport()
updateStickyState()
window.addEventListener('resize', updateViewport)
window.addEventListener('scroll', updateStickyState, { passive: true })
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateViewport)
window.removeEventListener('scroll', updateStickyState)
setOverlay(false)
})
</script>
<template>
<header
id="header"
:class="['site-header', 'header-style-1', { 'site-header--stuck': isSticky, 'site-header--visible': isStickyVisible }]"
>
<nav class="navigation navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="open-btn" @click="openMenu">
<span class="sr-only">打开菜单</span>
<span class="icon-bar" />
<span class="icon-bar" />
<span class="icon-bar" />
</button>
<NuxtLink class="navbar-brand" to="/" @click="closeMenu">
<img :src="siteInfo.headerLogo" :alt="siteInfo.companyName">
</NuxtLink>
</div>
<div
id="navbar"
:class="['navbar-collapse', 'collapse', 'navbar-right', 'navigation-holder', { slideInn: isMenuOpen }]"
>
<button class="close-navbar" @click="closeMenu">
<i class="ti-close" />
</button>
<ul :class="['nav', 'navbar-nav', { 'small-nav': isMobile }]">
<li
v-for="item in topNavigation"
:key="item.label"
:class="[
{
current: isItemActive(item),
'menu-item-has-children': hasChildren(item),
'product-menu': item.label === '产品中心',
'solution-menu': item.label === '解决方案'
}
]"
>
<NuxtLink
v-if="'to' in item && item.to"
:to="item.to"
@click="handleItemClick(item, getItemKey([item.label]), $event)"
>
{{ item.label }}
</NuxtLink>
<a
v-else
href="#"
@click.prevent="handleItemClick(item, getItemKey([item.label]), $event)"
>
{{ item.label }}
</a>
<ul
v-if="hasChildren(item)"
v-show="!isMobile || isExpanded(getItemKey([item.label]))"
class="sub-menu"
>
<li
v-for="child in item.children"
:key="child.label"
:class="[{ current: isItemActive(child), 'menu-item-has-children': hasChildren(child) }]"
>
<NuxtLink
v-if="'to' in child && child.to"
:to="child.to"
@click="handleItemClick(child, getItemKey([item.label, child.label]), $event)"
>
{{ child.label }}
</NuxtLink>
<a
v-else
href="#"
@click.prevent="handleItemClick(child, getItemKey([item.label, child.label]), $event)"
>
{{ child.label }}
</a>
<ul
v-if="hasChildren(child)"
v-show="!isMobile || isExpanded(getItemKey([item.label, child.label]))"
class="sub-menu"
>
<li v-for="leaf in child.children" :key="leaf.label" :class="{ current: matchesRoute(leaf.to) }">
<NuxtLink :to="leaf.to" @click="closeMenu">{{ leaf.label }}</NuxtLink>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<div class="search-contact">
<div class="contact">
<a :href="`tel:${siteInfo.phone}`" class="theme-btn">{{ siteInfo.phone }}</a>
</div>
</div>
<div class="separator" />
</div>
</nav>
</header>
</template>