265 lines
7.1 KiB
Vue
265 lines
7.1 KiB
Vue
|
|
<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>
|