248 lines
7.7 KiB
Vue
248 lines
7.7 KiB
Vue
|
|
<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>
|