Explorar el Código

refactor: 优化头部组件结构和样式

- 重构头部组件,调整模板结构,提升可读性和维护性。
- 更新下拉菜单动画效果,增加背景渐变,优化用户体验。
- 调整响应式布局,确保在不同屏幕尺寸下的显示效果良好。
- 添加标签支持,增强产品分类的展示功能。
master
lizhuang hace 1 semana
padre
commit
0c621287c4
Se han modificado 1 ficheros con 257 adiciones y 227 borrados
  1. 257
    227
      components/TheHeader.vue

+ 257
- 227
components/TheHeader.vue Ver fichero

@@ -1,254 +1,220 @@
<template>
<!-- Header -->
<header class="fixed top-0 z-50 w-full bg-slate-900/70 backdrop-blur-[50px]">
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10">
<div class="h-[55px] flex justify-between items-center sm:h-[72px]">
<div class="flex justify-start items-center gap-12 lg:gap-24">
<nuxt-link :to="homePath" class="brand-link mt-[5px] flex-shrink-0">
<i
class="icon-brand text-white text-1xl sm:text-2xl block transition-[transform,filter] duration-500 ease-in-out"
></i>
</nuxt-link>
<!-- Desktop Menu -->
<nav
class="hidden md:flex justify-start items-start gap-1 lg:gap-7 xl:gap-14"
>
<template v-for="item in menuItems" :key="item.label">
<!-- Regular Link -->
<nuxt-link
v-if="!item.isDropdown"
class="main-nav-link relative inline-block justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity py-2 px-3 rounded-md"
:to="item.path"
:class="[
route.path === item.path
? 'font-bold opacity-100 bg-white/15'
: '',
]"
>
{{ t(item.label) }}
</nuxt-link>

<!-- Dropdown Container -->
<div
v-else-if="item.isDropdown"
class="relative"
@mouseleave="handleMouseLeave"
>
<!-- Dropdown Trigger -->
<div
@mouseenter="handleMouseEnter(item.label)"
class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2 px-3 rounded-md"
<!-- 导航容器 -->
<div class="w-full">
<!-- 内容居中容器 -->
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10">
<div class="h-[55px] flex justify-between items-center sm:h-[72px]">
<div class="flex justify-start items-center gap-12 lg:gap-24">
<nuxt-link :to="homePath" class="brand-link mt-[5px] flex-shrink-0">
<i
class="icon-brand text-white text-1xl sm:text-2xl block transition-[transform,filter] duration-500 ease-in-out"
></i>
</nuxt-link>
<!-- Desktop Menu -->
<nav
class="hidden md:flex justify-start items-start gap-1 lg:gap-7 xl:gap-14"
>
<template v-for="item in menuItems" :key="item.label">
<!-- Regular Link -->
<nuxt-link
v-if="!item.isDropdown"
class="main-nav-link relative inline-block justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity py-2 px-3 rounded-md"
:to="item.path"
:class="[
route.path.startsWith(item.pathPrefix)
route.path === item.path
? 'font-bold opacity-100 bg-white/15'
: '',
]"
>
<span>{{ t(item.label) }}</span>
</div>
{{ t(item.label) }}
</nuxt-link>

<!-- Dropdown Panel -->
<transition name="fade-down">
<!-- Dropdown Container -->
<div
v-else-if="item.isDropdown"
class="relative"
@mouseleave="handleMouseLeave"
>
<!-- Dropdown Trigger -->
<div
v-if="item.isDropdown && openDropdown === item.label"
@mouseenter="handleMouseEnter(item.label)"
class="absolute left-[-15px] top-full mt-4 w-[550px] bg-slate-900/95 backdrop-blur-[50px] rounded-none border-none shadow-none p-0 z-10 gap-0 transition-all duration-300"
class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2 px-3 rounded-md"
:class="[route.path.startsWith(item.pathPrefix) ? 'font-bold opacity-100 bg-white/15' : '']"
>
<span>{{ t(item.label) }}</span>
</div>

<!-- Dropdown Panel -->
<transition name="fade-down">
<div
v-for="(section, index) in item.children"
:key="index"
class="rounded-none p-2 flex flex-col gap-1"
v-if="item.isDropdown && openDropdown === item.label"
@mouseenter="handleMouseEnter(item.label)"
class="fixed left-0 top-[70px] w-screen bg-slate-900/95 backdrop-blur-[50px] border-t border-b border-slate-700/20 shadow-2xl z-10"
>
<div class="grid grid-cols-2 gap-8 p-2">
<!-- 企业用户产品组 -->
<div class="relative group/card">
<div class="relative space-y-4">
<div
class="px-6 py-4 text-sm font-semibold text-gray-200 bg-gradient-to-br from-slate-800/90 to-slate-900/90 rounded-xl border border-slate-700/30 shadow-2xl backdrop-blur-sm group-hover/card:shadow-blue-500/10 transition-all duration-500"
>
<nuxt-link
:to="`${homePath}products?audiences=1`"
class="flex items-center gap-4"
@click="handleMouseLeave"
>
<div
class="p-2 rounded-lg bg-gradient-to-br from-blue-500/20 to-indigo-500/20 group-hover/card:from-blue-500/30 group-hover/card:to-indigo-500/30 transition-all duration-500"
>
<i
class="icon-building text-2xl text-blue-400"
></i>
</div>
<div>
<span
class="block text-lg font-medium tracking-wide text-white"
>{{ t("home.business.title") }}</span
<!-- 内容居中容器 -->
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10 py-12">
<!-- 主内容网格 -->
<div>
<!-- 产品分类列 (原有的分类逻辑和数据) -->
<div>
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-4">
{{ t("common.productCategories") }}
</h3>
<div class="flex gap-x-32 gap-y-8">
<!-- 企业用户产品组 -->
<div>
<h4 class="text-base font-semibold text-white mb-3">
<nuxt-link
:to="`${homePath}products?audiences=1`"
class="flex items-center gap-2 hover:text-cyan-400 transition-colors"
@click="handleMouseLeave"
>
</div>
</nuxt-link>
</div>
<ul class="space-y-2">
<li
v-for="link in getGroupedItems(section.items)
.enterprise"
:key="link.path"
class="group/item"
>
<nuxt-link
:to="link.path"
@click="handleMouseLeave"
class="block text-base text-gray-200 rounded-lg py-2.5 px-4 transition-all duration-200 hover:text-white hover:bg-white/5 relative"
:class="{
'text-white font-medium bg-white/10':
route.path === link.path,
}"
>
<span
class="relative flex items-center gap-3"
<span>{{ t("home.business.title") }}</span>
</nuxt-link>
</h4>
<ul class="space-y-2">
<li
v-for="link in getGroupedItems(item.children[0].items).enterprise"
:key="link.path"
>
<span
class="w-1.5 h-1.5 rounded-full bg-white/30 group-hover/item:bg-white/60 transition-colors duration-200"
></span>
<span class="text-base">{{
t(link.label)
}}</span>
</span>
</nuxt-link>
</li>
</ul>
</div>
</div>

<!-- 个人用户产品组 -->
<div class="relative group/card">
<div class="relative space-y-4">
<div
class="px-6 py-4 text-sm font-semibold text-gray-200 bg-gradient-to-br from-slate-800/90 to-slate-900/90 rounded-xl border border-slate-700/30 shadow-2xl backdrop-blur-sm group-hover/card:shadow-cyan-500/10 transition-all duration-500"
>
<nuxt-link
:to="`${homePath}products?audiences=0`"
class="flex items-center gap-4"
@click="handleMouseLeave"
>
<div
class="p-2 rounded-lg bg-gradient-to-br from-cyan-500/20 to-blue-500/20 group-hover/card:from-cyan-500/30 group-hover/card:to-blue-500/30 transition-all duration-500"
>
<i
class="icon-user text-2xl text-cyan-400"
></i>
</div>
<div>
<span
class="block text-lg font-medium tracking-wide text-white"
>{{ t("home.personal.title") }}</span
<nuxt-link
:to="link.path"
@click="handleMouseLeave"
class="block text-[16px] text-gray-300 hover:text-white transition-colors"
>
{{ t(link.label) }}
</nuxt-link>
<ul v-if="link.tags && link.tags.length" class="mt-1">
<li v-for="tag in link.tags" :key="tag">
<nuxt-link
:to="`${link.path}&tag=${tag}`"
@click.stop="handleMouseLeave"
class="text-sm text-gray-400 hover:text-white transition-colors duration-200"
>
{{ tag }}
</nuxt-link>
</li>
</ul>
</li>
</ul>
</div>

<!-- 个人用户产品组 -->
<div class="flex-1">
<h4 class="text-base font-semibold text-white mb-3">
<nuxt-link
:to="`${homePath}products?audiences=0`"
class="flex items-center gap-2 hover:text-cyan-400 transition-colors"
@click="handleMouseLeave"
>
</div>
</nuxt-link>
</div>
<ul class="space-y-2">
<li
v-for="link in getGroupedItems(section.items)
.personal"
:key="link.path"
class="group/item"
>
<nuxt-link
:to="link.path"
@click="handleMouseLeave"
class="block text-base text-gray-200 rounded-lg py-2.5 px-4 transition-all duration-200 hover:text-white hover:bg-white/5 relative"
:class="{
'text-white font-medium bg-white/10 is-active':
route.path === link.path,
}"
>
<span
class="relative flex items-center gap-3"
<span>{{ t("home.personal.title") }}</span>
</nuxt-link>
</h4>
<ul class="flex gap-12">
<li
v-for="link in getGroupedItems(item.children[0].items).personal"
:key="link.path"
>
<span
class="w-1.5 h-1.5 rounded-full bg-white/30 group-hover/item:bg-white/60 transition-colors duration-200"
></span>
<span class="text-base">{{
t(link.label)
}}</span>
</span>
</nuxt-link>
</li>
</ul>
<nuxt-link
:to="link.path"
@click="handleMouseLeave"
class="block text-[16px] text-gray-300 hover:text-white transition-colors"
>
{{ t(link.label) }}
</nuxt-link>
<ul v-if="link.tags && link.tags.length" class="mt-1">
<li v-for="tag in link.tags" :key="tag">
<nuxt-link
:to="`${link.path}&tag=${tag}`"
@click.stop="handleMouseLeave"
class="text-sm text-gray-400 hover:text-white transition-colors duration-200"
>
{{ tag }}
</nuxt-link>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
</nav>
</div>

<div class="flex justify-start items-center gap-4 md:gap-6">
<!-- Search -->
<div
@click="openSearch"
class="w-auto h-8 relative items-center opacity-40 rounded-2xl pr-4 hover:opacity-100 transition-opacity duration-300 hidden md:flex cursor-pointer"
style="border: 0.5px solid rgba(255, 255, 255, 0.4)"
>
<span
class="flex items-center justify-center w-8 h-8 opacity-80 hover:opacity-100 text-white"
>
<i class="icon-search text-sm"></i>
</span>
<span
class="hidden lg:inline-block ml-1 text-white text-sm opacity-80"
>
{{ t("common.search") }}
</span>
<!-- Input overlay could go here if implementing search -->
</transition>
</div>
</template>
</nav>
</div>

<!-- Language -->
<LanguageSwitcher />

<!-- Mobile Menu Button -->
<div class="md:hidden">
<button
@click="toggleMobileMenu"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 bg-white/10 hover:text-white hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
<div class="flex justify-start items-center gap-4 md:gap-6">
<!-- Search -->
<div
@click="openSearch"
class="w-auto h-8 relative items-center opacity-40 rounded-2xl pr-4 hover:opacity-100 transition-opacity duration-300 hidden md:flex cursor-pointer"
style="border: 0.5px solid rgba(255, 255, 255, 0.4)"
>
<span class="sr-only">Open main menu</span>
<!-- Icon when menu is closed. -->
<svg
v-if="!mobileMenuOpen"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
<span
class="flex items-center justify-center w-8 h-8 opacity-80 hover:opacity-100 text-white"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Icon when menu is open. -->
<svg
v-else
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
<i class="icon-search text-sm"></i>
</span>
<span
class="hidden lg:inline-block ml-1 text-white text-sm opacity-80"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{{ t("common.search") }}
</span>
<!-- Input overlay could go here if implementing search -->
</div>

<!-- Language -->
<LanguageSwitcher />

<!-- Mobile Menu Button -->
<div class="md:hidden">
<button
@click="toggleMobileMenu"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 bg-white/10 hover:text-white hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
>
<span class="sr-only">Open main menu</span>
<!-- Icon when menu is closed. -->
<svg
v-if="!mobileMenuOpen"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Icon when menu is open. -->
<svg
v-else
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
@@ -453,6 +419,7 @@
import { ref, computed, watch, nextTick } from "#imports";
import { useI18n } from "vue-i18n";
import { useSearch } from "~/composables/useSearch";
import { defineComponent } from "vue";

/**
* 页面头部组件
@@ -493,7 +460,6 @@ const { data: categoryResponse } = await useAsyncData(
// 转换为需要的格式
return content
.map((item: any) => {
console.log(item);
return {
label: item.title,
path: `/products?category=${encodeURIComponent(
@@ -501,6 +467,7 @@ const { data: categoryResponse } = await useAsyncData(
)}&audiences=${item.meta.audiences}`,
id: item.meta.id,
audiences: item.meta.audiences,
tags: item.meta.tags,
};
})
.sort((a, b) => (a.id || 0) - (b.id || 0));
@@ -731,13 +698,13 @@ header {
/* Transition for dropdown */
.fade-down-enter-active,
.fade-down-leave-active {
transition: all 0.2s ease-out;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.fade-down-enter-from,
.fade-down-leave-to {
opacity: 0;
transform: translateY(-10px);
transform: translateY(-12px);
}

/* Transition for search overlay */
@@ -881,4 +848,67 @@ header {
display: none;
}
}

/* 优化下拉菜单动画 */
.fade-down-enter-active,
.fade-down-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.fade-down-enter-from,
.fade-down-leave-to {
opacity: 0;
transform: translateY(-12px);
}

/* 添加下拉菜单背景渐变 */
.bg-slate-900\/95 {
background: linear-gradient(to bottom, rgba(15, 23, 42, 0.95), rgba(15, 23, 42, 0.98));
}

/* 响应式布局 */
@media (max-width: 1280px) {
/* 调整主网格为 3 列,产品分类占 1 列,帮助/主题占 2 列 */
.md\\:grid-cols-4 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.md\\:col-span-2 {
grid-column: span 1 / span 1; /* 产品分类在 1280px 以下只占 1 列 */
}
/* 帮助和实用主题的父容器 */
.md\\:col-span-2.grid.grid-cols-1.sm\\:grid-cols-2 {
grid-column: span 2 / span 2; /* 帮助/主题在 1280px 以下占 2 列 */
}
}

@media (max-width: 1024px) {
/* 主网格在 1024px 以下变为 2 列 */
.md\\:grid-cols-4 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* 产品分类在 1024px 以下仍然占 1 列 */
.md\\:col-span-2 {
grid-column: span 1 / span 1;
}
/* 帮助和实用主题的父容器在 1024px 以下仍然占 1 列 */
.md\\:col-span-2.grid.grid-cols-1.sm\\:grid-cols-2 {
grid-column: span 1 / span 1;
}
/* 产品分类和帮助主题内部的 sm:grid-cols-2 变为单列 */
.sm\\:grid-cols-2 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}

@media (max-width: 768px) {
/* 在移动端,所有列都变为单列堆叠 */
.md\\:grid-cols-4,
.md\\:col-span-2,
.md\\:col-span-2.grid,
.sm\\:grid-cols-2 {
grid-template-columns: repeat(1, minmax(0, 1fr));
grid-column: span 1 / span 1;
}
}
</style>


Cargando…
Cancelar
Guardar