Files

301 lines
11 KiB
Vue
Raw Permalink Normal View History

<script setup lang="ts">
2026-08-04 19:18:35 +08:00
import type { NavItem, Product } from '~/types/site';
2026-07-31 19:02:00 +08:00
const { t, locale, locales } = useI18n();
2026-07-31 19:02:00 +08:00
const localePath = useLocalePath();
const switchLocalePath = useSwitchLocalePath();
2026-07-31 19:02:00 +08:00
const route = useRoute();
const siteStore = useSiteStore();
const appStore = useAppStore();
const { navItems, navAppearance, productSeries, site } = storeToRefs(siteStore);
2026-08-10 18:50:14 +08:00
// ===== 多语言:按站点配置决定可切换的语言 =====
// 启用的语言代码;未配置时默认全部语言
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));
}
);
2026-07-31 19:02:00 +08:00
// 滚动状态:滚动后强制毛玻璃背景 + 深色文字
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;
});
2026-07-31 19:02:00 +08:00
// 判断是否为「产品」入口(悬浮展开 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;
2026-08-04 19:18:35 +08:00
hoveredProduct.value = null;
2026-07-31 19:02:00 +08:00
}, 120);
}
2026-08-04 19:18:35 +08:00
// 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
);
2026-07-31 19:02:00 +08:00
// 移动端:关闭抽屉
function closeMobile() {
appStore.toggleMobileMenu(false);
}
</script>
<template>
2026-07-31 19:02:00 +08:00
<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">
2026-08-03 19:09:03 +08:00
<!-- Logo点击返回首页 -->
<NuxtLink :to="localePath('/')" class="flex items-center gap-8px shrink-0" @click="closeMobile">
2026-07-31 19:02:00 +08:00
<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>
2026-07-31 19:02:00 +08:00
<!-- 桌面端导航 -->
<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)"
2026-08-03 19:09:03 +08:00
class="text-base font-medium transition-colors flex items-center gap-4px"
2026-07-31 19:02:00 +08:00
: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"
>
2026-08-04 19:18:35 +08:00
<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>
2026-07-31 19:02:00 +08:00
</div>
</div>
2026-08-04 19:18:35 +08:00
<!-- 右侧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>
2026-07-31 19:02:00 +08:00
</div>
</div>
</transition>
</div>
<!-- 普通入口外部链接 -->
<a
v-else-if="isExternal(item.link)"
:href="item.link"
:target="item.open_new_tab ? '_blank' : undefined"
2026-08-03 19:09:03 +08:00
class="text-base font-medium transition-colors"
2026-07-31 19:02:00 +08:00
: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"
2026-08-03 19:09:03 +08:00
class="text-base font-medium transition-colors"
2026-07-31 19:02:00 +08:00
:class="lightText ? 'text-white/90 hover:text-white' : 'text-gray-700 hover:text-primary dark:text-gray-200'"
>
{{ siteStore.navLabel(item) }}
</NuxtLink>
</template>
</nav>
2026-07-31 19:02:00 +08:00
<!-- 右侧语言切换 + 移动端汉堡 -->
<div class="flex items-center gap-12px">
2026-08-10 18:50:14 +08:00
<div v-if="showLocaleSwitch" class="hidden md:flex items-center gap-4px">
2026-07-31 19:02:00 +08:00
<NuxtLink
2026-08-10 18:50:14 +08:00
v-for="loc in activeLocales"
2026-07-31 19:02:00 +08:00
: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
2026-07-31 19:02:00 +08:00
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"
>
2026-07-31 19:02:00 +08:00
{{ siteStore.navLabel(item) }}
</NuxtLink>
2026-07-31 19:02:00 +08:00
2026-08-10 18:50:14 +08:00
<div v-if="showLocaleSwitch" class="flex items-center gap-8px mt-12px px-12px">
2026-07-31 19:02:00 +08:00
<NuxtLink
2026-08-10 18:50:14 +08:00
v-for="loc in activeLocales"
2026-07-31 19:02:00 +08:00
: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>
2026-07-31 19:02:00 +08:00
</transition>
</header>
</template>
2026-07-31 19:02:00 +08:00
<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>