Files
BigStickNewOfficialWebsite/components/SiteHeader.vue
T
2026-07-10 18:23:41 +08:00

248 lines
7.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { brand, navItems } from "~/data/navigation";
const props = defineProps({
announcementVisible: {
type: Boolean,
default: true,
},
});
const route = useRoute();
const mobileOpen = ref(false);
const expandedItem = ref("");
const headerRef = ref(null);
const barRef = ref(null);
let ctx;
let scrollTrigger;
const topOffset = computed(() => (props.announcementVisible ? "59px" : "0px"));
const contactHref = computed(() => `${route.path === "/" ? "" : route.path}#contact`);
const isActive = (item) => {
if (item.href === "/") {
return route.path === "/";
}
return route.path === item.href || route.path.startsWith(`${item.href}/`);
};
const closeMobile = () => {
mobileOpen.value = false;
expandedItem.value = "";
};
const toggleMobileItem = (label) => {
expandedItem.value = expandedItem.value === label ? "" : label;
};
watch(
() => props.announcementVisible,
() => {
closeMobile();
},
);
watch(
() => route.fullPath,
() => {
closeMobile();
},
);
onMounted(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
return;
}
const { $gsap, $ScrollTrigger } = useNuxtApp();
if (!headerRef.value || !barRef.value || !$gsap || !$ScrollTrigger) {
return;
}
ctx = $gsap.context(() => {
scrollTrigger = $ScrollTrigger.create({
start: 40,
end: 99999,
onToggle: (self) => {
$gsap.to(barRef.value, {
height: self.isActive ? 64 : 75,
duration: 0.28,
ease: "power2.out",
});
$gsap.to(headerRef.value, {
boxShadow: self.isActive
? "0 18px 50px rgba(0, 0, 0, 0.08)"
: "0 0 0 rgba(0, 0, 0, 0)",
duration: 0.28,
ease: "power2.out",
});
},
});
}, headerRef.value);
});
onBeforeUnmount(() => {
scrollTrigger?.kill?.();
ctx?.revert();
});
</script>
<template>
<header
ref="headerRef"
class="fixed inset-x-0 z-[999] border-b border-gm-border bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/85"
:style="{ top: topOffset }"
>
<div ref="barRef" class="container-gm flex h-[75px] items-center justify-between">
<NuxtLink to="/" class="shrink-0" :aria-label="brand.name">
<span class="block text-[18px] font-semibold leading-none text-[#1f2430] md:text-[20px]">
{{ brand.shortName }}
</span>
<span class="mt-1 block text-[11px] leading-none text-[#666]">
{{ brand.productName }}
</span>
</NuxtLink>
<nav class="hidden items-center gap-5 lg:flex xl:gap-7">
<div
v-for="item in navItems"
:key="item.label"
class="group relative py-6"
>
<NuxtLink
:to="item.href"
class="inline-flex items-center gap-2 text-[14px] transition hover:text-gm-green"
:class="isActive(item) ? 'text-gm-green' : 'text-[#1f2430]'"
>
{{ item.label }}
<span v-if="item.children?.length" class="text-[10px]" aria-hidden="true"></span>
</NuxtLink>
<div
v-if="item.children?.length"
class="pointer-events-none absolute left-0 top-full w-[260px] translate-y-2 rounded-b-[6px] border border-gm-border bg-white p-2 opacity-0 shadow-lg transition-all duration-200 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100"
>
<NuxtLink
v-for="child in item.children"
:key="child.label"
:to="child.href"
class="block rounded px-4 py-3 text-sm text-[#555] transition hover:bg-gm-light hover:text-gm-green"
>
{{ child.label }}
</NuxtLink>
</div>
</div>
</nav>
<div class="flex items-center gap-3">
<NuxtLink :to="contactHref" class="gm-btn gm-btn-primary hidden md:inline-flex">
获取方案
</NuxtLink>
<button
type="button"
class="inline-flex size-11 items-center justify-center rounded-full border border-gm-border text-gm-text transition hover:border-gm-green hover:text-gm-green lg:hidden"
aria-label="打开菜单"
@click="mobileOpen = true"
>
<span class="space-y-1.5">
<span class="block h-0.5 w-5 rounded-full bg-current" />
<span class="block h-0.5 w-5 rounded-full bg-current" />
<span class="block h-0.5 w-5 rounded-full bg-current" />
</span>
</button>
</div>
</div>
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="mobileOpen"
class="fixed inset-0 z-[1001] bg-white lg:hidden"
:style="{ top: topOffset }"
>
<div class="container-gm flex h-[75px] items-center justify-between border-b border-gm-border">
<div>
<span class="block text-lg font-semibold text-[#1f2430]">{{ brand.shortName }}</span>
<span class="text-xs text-[#666]">{{ brand.productName }}</span>
</div>
<button
type="button"
class="inline-flex size-11 items-center justify-center rounded-full border border-gm-border text-gm-text transition hover:border-gm-green hover:text-gm-green"
aria-label="关闭菜单"
@click="closeMobile"
>
<span aria-hidden="true" class="text-2xl leading-none">×</span>
</button>
</div>
<nav class="container-gm h-[calc(100vh-75px)] overflow-y-auto py-8">
<div
v-for="item in navItems"
:key="item.label"
class="border-b border-gm-border py-2"
>
<div class="flex items-center justify-between gap-4">
<NuxtLink
:to="item.href"
class="block flex-1 py-4 text-xl text-[#1f2430]"
@click="closeMobile"
>
{{ item.label }}
</NuxtLink>
<button
v-if="item.children?.length"
type="button"
class="flex size-10 items-center justify-center rounded-full border border-gm-border text-gm-text"
:aria-label="`展开${item.label}菜单`"
@click="toggleMobileItem(item.label)"
>
<span
class="transition-transform duration-200"
:class="expandedItem === item.label ? 'rotate-180' : ''"
aria-hidden="true"
>
</span>
</button>
</div>
<div
v-if="item.children?.length && expandedItem === item.label"
class="space-y-2 pb-4"
>
<NuxtLink
v-for="child in item.children"
:key="child.label"
:to="child.href"
class="block rounded bg-gm-light px-4 py-3 text-sm text-[#555]"
@click="closeMobile"
>
{{ child.label }}
</NuxtLink>
</div>
</div>
<NuxtLink
:to="contactHref"
class="mt-8 inline-flex h-12 items-center justify-center rounded-full bg-gm-green px-6 text-base font-medium text-white"
@click="closeMobile"
>
获取方案
</NuxtLink>
</nav>
</div>
</transition>
</header>
</template>