Hanye官网
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

TheHeader.vue 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. <template>
  2. <!-- Header -->
  3. <header class="fixed top-0 z-50 w-full bg-slate-900/70 backdrop-blur-[50px]">
  4. <div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10">
  5. <div class="h-[55px] flex justify-between items-center sm:h-[72px]">
  6. <div class="flex justify-start items-center gap-12 lg:gap-24">
  7. <nuxt-link :to="homePath" class="brand-link mt-[5px] flex-shrink-0">
  8. <i
  9. class="icon-brand text-white text-1xl sm:text-2xl block transition-[transform,filter] duration-500 ease-in-out"
  10. ></i>
  11. </nuxt-link>
  12. <!-- Desktop Menu -->
  13. <nav class="hidden md:flex justify-start items-start gap-7 lg:gap-14">
  14. <template v-for="item in menuItems" :key="item.label">
  15. <!-- Regular Link -->
  16. <nuxt-link
  17. v-if="!item.isDropdown"
  18. 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"
  19. :to="item.path"
  20. :class="[
  21. $route.path === item.path
  22. ? 'font-bold opacity-100 bg-white/15'
  23. : '',
  24. ]"
  25. >
  26. {{ $t(item.label) }}
  27. </nuxt-link>
  28. <!-- Dropdown Container -->
  29. <div
  30. v-else-if="item.isDropdown"
  31. class="relative"
  32. @mouseleave="handleMouseLeave"
  33. >
  34. <!-- Dropdown Trigger -->
  35. <div
  36. @mouseenter="handleMouseEnter(item.label)"
  37. class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2"
  38. :class="{
  39. 'text-white font-bold opacity-100':
  40. openDropdown === item.label ||
  41. $route.path.startsWith(item.pathPrefix),
  42. }"
  43. >
  44. <span>{{ $t(item.label) }}</span>
  45. <!-- Dropdown Arrow -->
  46. <svg
  47. class="h-3 w-3 text-white/60"
  48. fill="none"
  49. viewBox="0 0 24 24"
  50. stroke="currentColor"
  51. >
  52. <path
  53. stroke-linecap="round"
  54. stroke-linejoin="round"
  55. stroke-width="3"
  56. d="M19 9l-7 7-7-7"
  57. />
  58. </svg>
  59. </div>
  60. <!-- Dropdown Panel -->
  61. <transition name="fade-down">
  62. <div
  63. v-if="item.isDropdown && openDropdown === item.label"
  64. @mouseenter="handleMouseEnter(item.label)"
  65. class="absolute left-0 top-full mt-1 w-max min-w-[450px] bg-slate-800/90 backdrop-blur-md rounded-lg shadow-xl p-4 z-10 grid grid-cols-2 gap-4"
  66. >
  67. <div
  68. v-for="(section, index) in item.children"
  69. :key="index"
  70. class="bg-black/10 p-4 rounded-md"
  71. >
  72. <h3
  73. class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-4 flex items-center gap-2"
  74. >
  75. <i
  76. :class="[
  77. index === 0 ? 'icon-tag' : 'icon-target',
  78. 'text-gray-400',
  79. ]"
  80. ></i>
  81. <span>{{ $t(section.title) }}</span>
  82. </h3>
  83. <ul class="space-y-3">
  84. <li v-for="link in section.items" :key="link.path">
  85. <nuxt-link
  86. :to="link.path"
  87. @click="handleMouseLeave"
  88. class="block text-base text-gray-200 hover:text-white hover:bg-white/10 transition-all duration-150 rounded px-2 py-1"
  89. :class="{
  90. 'text-white font-bold bg-white/5':
  91. $route.path === link.path,
  92. }"
  93. >
  94. {{ $t(link.label) }}
  95. </nuxt-link>
  96. </li>
  97. </ul>
  98. </div>
  99. </div>
  100. </transition>
  101. </div>
  102. </template>
  103. </nav>
  104. </div>
  105. <div class="flex justify-start items-center gap-4 md:gap-6">
  106. <!-- Search -->
  107. <div
  108. @click="openSearch"
  109. 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"
  110. style="border: 0.5px solid rgba(255, 255, 255, 0.4)"
  111. >
  112. <span
  113. class="flex items-center justify-center w-8 h-8 opacity-80 hover:opacity-100 text-white"
  114. >
  115. <i class="icon-search text-sm"></i>
  116. </span>
  117. <span
  118. class="hidden lg:inline-block ml-1 text-white text-sm opacity-80"
  119. >
  120. {{ $t("common.search") }}
  121. </span>
  122. <!-- Input overlay could go here if implementing search -->
  123. </div>
  124. <!-- Language -->
  125. <LanguageSwitcher />
  126. <!-- Mobile Menu Button -->
  127. <div class="md:hidden">
  128. <button
  129. @click="toggleMobileMenu"
  130. 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"
  131. >
  132. <span class="sr-only">Open main menu</span>
  133. <!-- Icon when menu is closed. -->
  134. <svg
  135. v-if="!mobileMenuOpen"
  136. class="block h-6 w-6"
  137. xmlns="http://www.w3.org/2000/svg"
  138. fill="none"
  139. viewBox="0 0 24 24"
  140. stroke="currentColor"
  141. aria-hidden="true"
  142. >
  143. <path
  144. stroke-linecap="round"
  145. stroke-linejoin="round"
  146. stroke-width="2"
  147. d="M4 6h16M4 12h16M4 18h16"
  148. />
  149. </svg>
  150. <!-- Icon when menu is open. -->
  151. <svg
  152. v-else
  153. class="block h-6 w-6"
  154. xmlns="http://www.w3.org/2000/svg"
  155. fill="none"
  156. viewBox="0 0 24 24"
  157. stroke="currentColor"
  158. aria-hidden="true"
  159. >
  160. <path
  161. stroke-linecap="round"
  162. stroke-linejoin="round"
  163. stroke-width="2"
  164. d="M6 18L18 6M6 6l12 12"
  165. />
  166. </svg>
  167. </button>
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. <!-- Mobile menu, show/hide based on menu state. -->
  173. <transition name="slide-fade">
  174. <div
  175. v-if="mobileMenuOpen"
  176. class="md:hidden bg-slate-800/90"
  177. id="mobile-menu"
  178. >
  179. <div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
  180. <template v-for="item in menuItems" :key="item.label">
  181. <!-- Mobile Regular Link -->
  182. <nuxt-link
  183. v-if="!item.isDropdown"
  184. :to="item.path"
  185. @click="closeMobileMenu"
  186. class="block px-3 py-2 rounded-md text-base font-medium"
  187. :class="[
  188. $route.path === item.path
  189. ? 'bg-gray-900 text-white'
  190. : 'text-gray-300 hover:bg-gray-700 hover:text-white',
  191. ]"
  192. >
  193. {{ $t(item.label) }}
  194. </nuxt-link>
  195. <!-- Mobile Dropdown Section -->
  196. <div v-else class="mt-2">
  197. <h3
  198. class="px-3 pt-2 pb-1 text-sm font-semibold text-gray-400 uppercase tracking-wider"
  199. >
  200. {{ $t(item.label) }}
  201. </h3>
  202. <div
  203. v-for="section in item.children"
  204. :key="section.title"
  205. class="mt-1"
  206. >
  207. <!-- Optional: Section title for mobile? -->
  208. <!-- <h4 class="px-3 pt-1 text-xs font-medium text-gray-500">{{ $t(section.title) }}</h4> -->
  209. <nuxt-link
  210. v-for="link in section.items"
  211. :key="link.path"
  212. :to="link.path"
  213. @click="closeMobileMenu"
  214. class="block pl-6 pr-3 py-2 rounded-md text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white"
  215. :class="{
  216. 'bg-gray-900 text-white': $route.path === link.path,
  217. }"
  218. >
  219. {{ $t(link.label) }}
  220. </nuxt-link>
  221. </div>
  222. </div>
  223. </template>
  224. </div>
  225. </div>
  226. </transition>
  227. </header>
  228. <!-- Search Layer (Moved outside header) -->
  229. <transition name="search-fade-scale">
  230. <div
  231. v-if="isSearchOpen"
  232. class="fixed inset-0 z-[60] bg-black/80 backdrop-blur-md flex items-start justify-center pt-20"
  233. @click.self="closeSearch"
  234. >
  235. <div
  236. class="bg-gradient-to-br from-slate-800 to-slate-900 p-8 rounded-lg relative w-full max-w-2xl mx-4 search-modal-content shadow-xl"
  237. >
  238. <button
  239. @click="closeSearch"
  240. class="absolute top-3 right-3 text-gray-500 hover:text-white hover:bg-white/10 rounded-full p-2 transition-colors"
  241. >
  242. <svg
  243. class="h-5 w-5"
  244. fill="none"
  245. viewBox="0 0 24 24"
  246. stroke="currentColor"
  247. >
  248. <path
  249. stroke-linecap="round"
  250. stroke-linejoin="round"
  251. stroke-width="2"
  252. d="M6 18L18 6M6 6l12 12"
  253. />
  254. </svg>
  255. </button>
  256. <h2 class="text-white text-xl mb-6">{{ $t("common.search") }}</h2>
  257. <!-- Input with Icon -->
  258. <div class="relative mb-6">
  259. <span class="absolute inset-y-0 left-0 flex items-center pl-3">
  260. <i class="icon-search text-gray-400 text-sm"></i>
  261. </span>
  262. <input
  263. ref="searchInputRef"
  264. type="text"
  265. :placeholder="
  266. $t('common.searchPlaceholder') || 'Enter search term...'
  267. "
  268. class="w-full p-3 pl-10 pr-10 rounded bg-slate-700 text-white border border-slate-600 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
  269. />
  270. <!-- Enter Icon -->
  271. <span
  272. class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer group"
  273. >
  274. <i
  275. class="icon-enter text-gray-400 group-hover:text-blue-400 transition-colors"
  276. ></i>
  277. </span>
  278. </div>
  279. <!-- Search results could go here -->
  280. <!-- Hot Keywords Section -->
  281. <div class="mt-6">
  282. <h3 class="text-gray-400 text-sm mb-3">
  283. {{ $t("common.hotKeywords") || "热门搜索" }}
  284. </h3>
  285. <div class="flex flex-wrap gap-3">
  286. <button
  287. v-for="keyword in hotKeywords"
  288. :key="keyword"
  289. @click="searchHotKeyword(keyword)"
  290. class="px-4 py-1.5 bg-slate-700 text-white/80 rounded-full text-sm hover:bg-blue-600 hover:text-white transition-colors duration-200"
  291. >
  292. {{ keyword }}
  293. </button>
  294. </div>
  295. </div>
  296. </div>
  297. </div>
  298. </transition>
  299. </template>
  300. <script setup lang="ts">
  301. import { ref, computed, watch, nextTick } from "#imports";
  302. import { useI18n } from "vue-i18n";
  303. /**
  304. * 页面头部组件
  305. * 包含导航菜单、语言切换和移动端响应式设计
  306. */
  307. const { t, locale } = useI18n();
  308. const config = useRuntimeConfig();
  309. // 从运行时配置获取默认语言,如果未配置则默认为 'en'
  310. const defaultLocale = config.public.i18n?.defaultLocale || "en";
  311. const mobileMenuOpen = ref(false);
  312. const isSearchOpen = ref(false);
  313. const searchInputRef = ref<HTMLInputElement | null>(null);
  314. const openDropdown = ref<string | null>(null);
  315. let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mouseleave delay
  316. // 添加热门关键字
  317. const hotKeywords = ref(["SSD", "SD", "DDR4"]);
  318. // 使用 computed 来定义 homePath,根据是否为默认语言调整路径
  319. const homePath = computed(() => {
  320. // 如果是默认语言,路径为根路径 '/'
  321. // 否则,路径为 '/<locale>/'
  322. return locale.value === defaultLocale ? "/" : `/${locale.value}/`;
  323. });
  324. // 使用 computed 来定义 menuItems,根据是否为默认语言调整路径
  325. const menuItems = computed(() => {
  326. // 判断当前是否为默认语言
  327. const isDefaultLocale = locale.value === defaultLocale;
  328. // 如果是默认语言,路径前缀为空字符串,否则为 '/<locale>'
  329. const prefix = isDefaultLocale ? "" : `/${locale.value}`;
  330. return [
  331. // 首页路径特殊处理:默认语言为 '/', 其他语言为 '/<locale>/'
  332. { label: "common.home", path: isDefaultLocale ? "/" : `${prefix}/` },
  333. {
  334. label: "common.products",
  335. isDropdown: true,
  336. pathPrefix: `${prefix}/products`,
  337. children: [
  338. {
  339. title: "common.productCategories",
  340. items: [
  341. { label: "SSD", path: `${prefix}/products?category=ssd` },
  342. { label: "DRAM", path: `${prefix}/products?category=dram` },
  343. { label: "NAND", path: `${prefix}/products?category=nand` },
  344. ],
  345. },
  346. {
  347. title: "common.byUsage",
  348. items: [
  349. {
  350. label: "Enterprise",
  351. path: `${prefix}/products?usage=enterprise`,
  352. },
  353. { label: "Consumer", path: `${prefix}/products?usage=consumer` },
  354. {
  355. label: "Industrial",
  356. path: `${prefix}/products?usage=industrial`,
  357. },
  358. ],
  359. },
  360. ],
  361. },
  362. { label: "common.faq", path: `${prefix}/faq` },
  363. { label: "common.about", path: `${prefix}/about` },
  364. { label: "common.contact", path: `${prefix}/contact` },
  365. ];
  366. });
  367. /**
  368. * 切换移动端菜单显示状态
  369. */
  370. function toggleMobileMenu() {
  371. mobileMenuOpen.value = !mobileMenuOpen.value;
  372. }
  373. /**
  374. * 关闭移动端菜单
  375. */
  376. function closeMobileMenu() {
  377. mobileMenuOpen.value = false;
  378. }
  379. /**
  380. * 打开搜索层
  381. */
  382. function openSearch() {
  383. isSearchOpen.value = true;
  384. }
  385. /**
  386. * 关闭搜索层
  387. */
  388. function closeSearch() {
  389. isSearchOpen.value = false;
  390. }
  391. /**
  392. * 搜索热门关键字 (示例)
  393. * @param keyword
  394. */
  395. function searchHotKeyword(keyword: string) {
  396. console.log("Searching for hot keyword:", keyword);
  397. // 可以在这里实现填充输入框或直接执行搜索的逻辑
  398. // 例如: searchInputValue.value = keyword;
  399. closeSearch(); // 点击后可以关闭搜索层
  400. }
  401. // 监听搜索层状态,打开时自动聚焦输入框
  402. watch(isSearchOpen, (newValue: boolean) => {
  403. if (newValue) {
  404. nextTick(() => {
  405. searchInputRef.value?.focus();
  406. });
  407. }
  408. });
  409. // --- Dropdown Logic ---
  410. function handleMouseEnter(label: string) {
  411. if (leaveTimeout) {
  412. clearTimeout(leaveTimeout);
  413. leaveTimeout = null;
  414. }
  415. openDropdown.value = label;
  416. }
  417. function handleMouseLeave() {
  418. // Delay closing the dropdown slightly
  419. leaveTimeout = setTimeout(() => {
  420. openDropdown.value = null;
  421. }, 150); // 150ms delay
  422. }
  423. // --- End Dropdown Logic ---
  424. </script>
  425. <style lang="scss" scoped>
  426. header {
  427. user-select: none;
  428. }
  429. /* Brand icon hover effect */
  430. .brand-link {
  431. &:hover {
  432. .icon-brand {
  433. transform: scale(1.05);
  434. filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
  435. }
  436. }
  437. }
  438. /* Base style for icon to apply will-change */
  439. .icon-brand {
  440. will-change: transform, filter; /* Hint for browser optimization */
  441. }
  442. /* Transition for mobile menu */
  443. .slide-fade-enter-active {
  444. transition: all 0.3s ease-out;
  445. }
  446. .slide-fade-leave-active {
  447. transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
  448. }
  449. .slide-fade-enter-from,
  450. .slide-fade-leave-to {
  451. transform: translateY(-20px);
  452. opacity: 0;
  453. }
  454. /* Transition for dropdown */
  455. .fade-down-enter-active,
  456. .fade-down-leave-active {
  457. transition: all 0.2s ease-out;
  458. }
  459. .fade-down-enter-from,
  460. .fade-down-leave-to {
  461. opacity: 0;
  462. transform: translateY(-10px);
  463. }
  464. /* Transition for search overlay */
  465. .search-fade-scale-enter-active,
  466. .search-fade-scale-leave-active {
  467. transition: opacity 0.3s ease-out;
  468. }
  469. .search-fade-scale-enter-from,
  470. .search-fade-scale-leave-to {
  471. opacity: 0;
  472. }
  473. /* 为搜索框内容添加独立的、稍延迟的动画 */
  474. .search-modal-content {
  475. transition: transform 0.3s ease-out 0.05s;
  476. }
  477. .search-fade-scale-enter-from .search-modal-content,
  478. .search-fade-scale-leave-to .search-modal-content {
  479. transform: scale(0.95) translateY(10px);
  480. }
  481. /* Keep the sticky header consistent */
  482. .sticky {
  483. position: sticky;
  484. }
  485. </style>