Files
admin/www/app/components/AppHeader.vue
T
2026-07-31 19:02:00 +08:00

257 lines
9.0 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 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>