Files
2026-08-10 18:50:14 +08:00

301 lines
11 KiB
Vue
Raw Permalink 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 lang="ts">
import type { NavItem, Product } 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 enabledLocaleCodes = computed<string[]>(() => {
const all: string[] = (unref(locales) || []).map(l => l.code);
const raw = site.value?.enabled_locales;
if (!raw) return all;
const list = raw.split(',').map(s => s.trim()).filter(c => all.includes(c));
return list.length ? list : all;
});
// 实际展示的切换按钮 = i18n locales ∩ 启用列表
const activeLocales = computed(() => (unref(locales) || []).filter(l => enabledLocaleCodes.value.includes(l.code)));
// 只启用一种语言时不显示切换按钮
const showLocaleSwitch = computed(() => activeLocales.value.length > 1);
// 当前语言被停用(如只启用中文时访问 /en/*):跳转到首个启用语言的对应页面
watch(
[enabledLocaleCodes, () => locale.value],
([codes]) => {
if (!site.value || codes.includes(locale.value)) return;
const target = codes[0];
if (target) navigateTo(switchLocalePath(target as typeof locale.value));
}
);
// 滚动状态:滚动后强制毛玻璃背景 + 深色文字
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;
hoveredProduct.value = null;
}, 120);
}
// Mega Menu 右侧大图预览:hover 某产品时展示其封面,默认展示第一个产品
const hoveredProduct = ref<Product | null>(null);
const previewProduct = computed<Product | null>(
() => hoveredProduct.value ?? productSeries.value.find(s => s.products?.length)?.products[0] ?? null
);
// 移动端:关闭抽屉
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="localePath('/')" 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-base 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-32px">
<!-- 左侧系列/产品列表 -->
<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="p-8px rounded-8px hover:bg-gray-50 transition-colors dark:hover:bg-dark-800"
@click="megaOpen = false"
@mouseenter="hoveredProduct = product"
>
<span class="text-sm text-gray-800 dark:text-gray-100">
{{ locale === 'en' ? product.name_en : product.name_zh }}
</span>
</NuxtLink>
</div>
</div>
</div>
<!-- 右侧hover 产品封面大图预览 -->
<div v-if="previewProduct" class="w-360px shrink-0 border-l border-gray-100 pl-32px dark:border-dark-700">
<img
v-if="previewProduct.cover_url"
:src="previewProduct.cover_url"
:alt="locale === 'en' ? previewProduct.name_en : previewProduct.name_zh"
class="w-full h-240px object-contain rounded-8px bg-gray-50 dark:bg-dark-800"
/>
<p class="mt-12px text-lg font-bold text-gray-900 dark:text-gray-100">
{{ locale === 'en' ? previewProduct.name_en : previewProduct.name_zh }}
</p>
</div>
</div>
</div>
</transition>
</div>
<!-- 普通入口外部链接 -->
<a
v-else-if="isExternal(item.link)"
:href="item.link"
:target="item.open_new_tab ? '_blank' : undefined"
class="text-base 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-base 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 v-if="showLocaleSwitch" class="hidden md:flex items-center gap-4px">
<NuxtLink
v-for="loc in activeLocales"
: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 v-if="showLocaleSwitch" class="flex items-center gap-8px mt-12px px-12px">
<NuxtLink
v-for="loc in activeLocales"
: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>