257 lines
9.0 KiB
Vue
257 lines
9.0 KiB
Vue
<script setup lang="ts">
|
||
import type { NavItem } from '~/types/site';
|
||
|
||
const { t, locale, locales } = useI18n();
|
||
const localePath = useLocalePath();
|
||
const switchLocalePath = useSwitchLocalePath();
|
||
const route = useRoute();
|
||
|
||
const siteStore = useSiteStore();
|
||
const appStore = useAppStore();
|
||
|
||
const { navItems, navAppearance, productSeries, site } = storeToRefs(siteStore);
|
||
|
||
// 滚动状态:滚动后强制毛玻璃背景 + 深色文字
|
||
const { y: scrollY } = useWindowScroll();
|
||
const isScrolled = computed(() => scrollY.value > 24);
|
||
|
||
// 是否首页
|
||
const isHome = computed(() => route.path === localePath('/') || route.path === '/');
|
||
|
||
// 导航栏是否使用透明风格(未滚动时)
|
||
const useTransparent = computed(() => {
|
||
if (isScrolled.value) return false;
|
||
const style = isHome.value ? navAppearance.value?.homepage_style : navAppearance.value?.non_homepage_style;
|
||
return style === 'transparent';
|
||
});
|
||
|
||
// 文字/Logo 颜色:透明风格下用浅色(适配深色首屏),否则用深色
|
||
const lightText = computed(() => useTransparent.value);
|
||
|
||
const logoUrl = computed(() => {
|
||
if (!site.value) return '';
|
||
return lightText.value ? site.value.logo_light_url : site.value.logo_dark_url;
|
||
});
|
||
|
||
// 判断是否为「产品」入口(悬浮展开 Mega Menu)
|
||
function isProductsEntry(item: NavItem): boolean {
|
||
return item.link.includes('/products');
|
||
}
|
||
|
||
// 解析导航链接:外部 URL 原样返回,内部路由加语言前缀
|
||
function resolveLink(link: string): string {
|
||
if (/^https?:\/\//.test(link)) return link;
|
||
return localePath(link);
|
||
}
|
||
|
||
function isExternal(link: string): boolean {
|
||
return /^https?:\/\//.test(link);
|
||
}
|
||
|
||
// Mega Menu 悬浮状态
|
||
const megaOpen = ref(false);
|
||
let megaTimer: ReturnType<typeof setTimeout> | null = null;
|
||
|
||
function openMega() {
|
||
if (megaTimer) clearTimeout(megaTimer);
|
||
megaOpen.value = true;
|
||
}
|
||
|
||
function closeMega() {
|
||
megaTimer = setTimeout(() => {
|
||
megaOpen.value = false;
|
||
}, 120);
|
||
}
|
||
|
||
// 移动端:关闭抽屉
|
||
function closeMobile() {
|
||
appStore.toggleMobileMenu(false);
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<header
|
||
class="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
|
||
:class="useTransparent ? 'bg-transparent' : 'bg-white/80 backdrop-blur-md border-b border-gray-100 dark:bg-dark-900/80 dark:border-dark-700'"
|
||
>
|
||
<div class="section-container h-64px flex items-center justify-between">
|
||
<!-- Logo -->
|
||
<NuxtLink :to="switchLocalePath(locale)" class="flex items-center gap-8px shrink-0" @click="closeMobile">
|
||
<img v-if="logoUrl" :src="logoUrl" :alt="site?.site_title || 'Luxsin'" class="h-32px w-auto" />
|
||
<span
|
||
v-else
|
||
class="text-xl font-bold transition-colors"
|
||
:class="lightText ? 'text-white' : 'text-primary'"
|
||
>
|
||
{{ site?.site_title || t('app.name') }}
|
||
</span>
|
||
</NuxtLink>
|
||
|
||
<!-- 桌面端导航 -->
|
||
<nav class="hidden md:flex items-center gap-32px">
|
||
<template v-for="item in navItems" :key="item.id">
|
||
<!-- 产品入口:悬浮 Mega Menu -->
|
||
<div
|
||
v-if="isProductsEntry(item)"
|
||
class="relative h-64px flex items-center"
|
||
@mouseenter="openMega"
|
||
@mouseleave="closeMega"
|
||
>
|
||
<NuxtLink
|
||
:to="resolveLink(item.link)"
|
||
class="text-sm font-medium transition-colors flex items-center gap-4px"
|
||
:class="lightText ? 'text-white/90 hover:text-white' : 'text-gray-700 hover:text-primary dark:text-gray-200'"
|
||
>
|
||
{{ siteStore.navLabel(item) }}
|
||
<span class="text-10px opacity-60">▾</span>
|
||
</NuxtLink>
|
||
|
||
<!-- Mega Menu 面板 -->
|
||
<transition name="mega">
|
||
<div
|
||
v-if="megaOpen"
|
||
class="absolute top-64px left-50% -translate-x-50% min-w-640px bg-white/95 backdrop-blur-xl rounded-12px shadow-xl border border-gray-100 p-24px dark:bg-dark-900/95 dark:border-dark-700"
|
||
>
|
||
<div class="flex gap-40px">
|
||
<div v-for="series in productSeries" :key="series.id" class="min-w-200px">
|
||
<p class="text-12px font-semibold text-gray-400 mb-12px uppercase tracking-wide">
|
||
{{ locale === 'en' ? series.name_en : series.name_zh }}
|
||
</p>
|
||
<div class="flex flex-col gap-8px">
|
||
<NuxtLink
|
||
v-for="product in series.products"
|
||
:key="product.id"
|
||
:to="localePath(`/products/${product.slug}`)"
|
||
class="flex items-center gap-12px p-8px rounded-8px hover:bg-gray-50 transition-colors dark:hover:bg-dark-800"
|
||
@click="megaOpen = false"
|
||
>
|
||
<img
|
||
v-if="product.cover_url"
|
||
:src="product.cover_url"
|
||
:alt="product.name_zh"
|
||
class="w-48px h-36px object-cover rounded-6px bg-gray-100"
|
||
/>
|
||
<span class="text-sm text-gray-800 dark:text-gray-100">
|
||
{{ locale === 'en' ? product.name_en : product.name_zh }}
|
||
</span>
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
|
||
<!-- 普通入口:外部链接 -->
|
||
<a
|
||
v-else-if="isExternal(item.link)"
|
||
:href="item.link"
|
||
:target="item.open_new_tab ? '_blank' : undefined"
|
||
class="text-sm font-medium transition-colors"
|
||
:class="lightText ? 'text-white/90 hover:text-white' : 'text-gray-700 hover:text-primary dark:text-gray-200'"
|
||
>
|
||
{{ siteStore.navLabel(item) }}
|
||
</a>
|
||
|
||
<!-- 普通入口:站内路由 -->
|
||
<NuxtLink
|
||
v-else
|
||
:to="resolveLink(item.link)"
|
||
:target="item.open_new_tab ? '_blank' : undefined"
|
||
class="text-sm font-medium transition-colors"
|
||
:class="lightText ? 'text-white/90 hover:text-white' : 'text-gray-700 hover:text-primary dark:text-gray-200'"
|
||
>
|
||
{{ siteStore.navLabel(item) }}
|
||
</NuxtLink>
|
||
</template>
|
||
</nav>
|
||
|
||
<!-- 右侧:语言切换 + 移动端汉堡 -->
|
||
<div class="flex items-center gap-12px">
|
||
<div class="hidden md:flex items-center gap-4px">
|
||
<NuxtLink
|
||
v-for="loc in locales"
|
||
:key="loc.code"
|
||
:to="switchLocalePath(loc.code)"
|
||
class="px-8px py-4px rounded text-sm transition-colors"
|
||
:class="
|
||
locale === loc.code
|
||
? lightText ? 'bg-white/20 text-white' : 'bg-primary text-white'
|
||
: lightText ? 'text-white/70 hover:text-white' : 'text-gray-500 hover:text-primary'
|
||
"
|
||
>
|
||
{{ loc.code === 'zh' ? '中' : 'EN' }}
|
||
</NuxtLink>
|
||
</div>
|
||
|
||
<!-- 汉堡按钮 -->
|
||
<button
|
||
class="md:hidden w-40px h-40px flex-center rounded-8px transition-colors"
|
||
:class="lightText ? 'text-white hover:bg-white/10' : 'text-gray-700 hover:bg-gray-100 dark:text-gray-200'"
|
||
aria-label="menu"
|
||
@click="appStore.toggleMobileMenu()"
|
||
>
|
||
<span class="text-xl">{{ appStore.mobileMenuOpen ? '✕' : '☰' }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 移动端抽屉 -->
|
||
<transition name="drawer">
|
||
<div
|
||
v-if="appStore.mobileMenuOpen"
|
||
class="md:hidden bg-white dark:bg-dark-900 border-t border-gray-100 dark:border-dark-700 px-16px py-16px flex flex-col gap-4px"
|
||
>
|
||
<NuxtLink
|
||
v-for="item in navItems"
|
||
:key="item.id"
|
||
:to="resolveLink(item.link)"
|
||
class="px-12px py-12px rounded-8px text-base text-gray-800 hover:bg-gray-50 dark:text-gray-100 dark:hover:bg-dark-800"
|
||
@click="closeMobile"
|
||
>
|
||
{{ siteStore.navLabel(item) }}
|
||
</NuxtLink>
|
||
|
||
<div class="flex items-center gap-8px mt-12px px-12px">
|
||
<NuxtLink
|
||
v-for="loc in locales"
|
||
:key="loc.code"
|
||
:to="switchLocalePath(loc.code)"
|
||
class="px-12px py-6px rounded text-sm transition-colors"
|
||
:class="locale === loc.code ? 'bg-primary text-white' : 'text-gray-500 hover:text-primary'"
|
||
@click="closeMobile"
|
||
>
|
||
{{ loc.code === 'zh' ? '中文' : 'English' }}
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</header>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.mega-enter-active,
|
||
.mega-leave-active {
|
||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||
}
|
||
.mega-enter-from,
|
||
.mega-leave-to {
|
||
opacity: 0;
|
||
transform: translate(-50%, -8px);
|
||
}
|
||
.mega-enter-to,
|
||
.mega-leave-from {
|
||
opacity: 1;
|
||
transform: translate(-50%, 0);
|
||
}
|
||
|
||
.drawer-enter-active,
|
||
.drawer-leave-active {
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
.drawer-enter-from,
|
||
.drawer-leave-to {
|
||
opacity: 0;
|
||
}
|
||
</style>
|