Hanye官网
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  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
  14. class="hidden md:flex justify-start items-start gap-1 lg:gap-7 xl:gap-14"
  15. >
  16. <template v-for="item in menuItems" :key="item.label">
  17. <!-- Regular Link -->
  18. <nuxt-link
  19. v-if="!item.isDropdown"
  20. 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"
  21. :to="item.path"
  22. :class="[
  23. route.path === item.path
  24. ? 'font-bold opacity-100 bg-white/15'
  25. : '',
  26. ]"
  27. >
  28. {{ t(item.label) }}
  29. </nuxt-link>
  30. <!-- Dropdown Container -->
  31. <div
  32. v-else-if="item.isDropdown"
  33. class="relative"
  34. @mouseleave="handleMouseLeave"
  35. >
  36. <!-- Dropdown Trigger -->
  37. <div
  38. @mouseenter="handleMouseEnter(item.label)"
  39. 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"
  40. :class="[
  41. route.path.startsWith(item.pathPrefix)
  42. ? 'font-bold opacity-100 bg-white/15'
  43. : '',
  44. ]"
  45. >
  46. <span>{{ t(item.label) }}</span>
  47. </div>
  48. <!-- Dropdown Panel -->
  49. <transition name="fade-down">
  50. <div
  51. v-if="item.isDropdown && openDropdown === item.label"
  52. @mouseenter="handleMouseEnter(item.label)"
  53. 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"
  54. >
  55. <div
  56. v-for="(section, index) in item.children"
  57. :key="index"
  58. class="rounded-none p-2 flex flex-col gap-1"
  59. >
  60. <div class="grid grid-cols-2 gap-8 p-2">
  61. <!-- 企业用户产品组 -->
  62. <div class="relative group/card">
  63. <div class="relative space-y-4">
  64. <div
  65. 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"
  66. >
  67. <div class="flex items-center gap-4">
  68. <div
  69. 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"
  70. >
  71. <i
  72. class="icon-building text-2xl text-blue-400"
  73. ></i>
  74. </div>
  75. <div>
  76. <span
  77. class="block text-lg font-medium tracking-wide text-white"
  78. >{{ t("common.business") }}</span
  79. >
  80. <span
  81. class="text-xs text-gray-400 mt-0.5 block"
  82. >{{ t("common.businessDesc") }}</span
  83. >
  84. </div>
  85. </div>
  86. </div>
  87. <ul class="space-y-2">
  88. <li
  89. v-for="link in getGroupedItems(section.items)
  90. .enterprise"
  91. :key="link.path"
  92. class="group/item"
  93. >
  94. <nuxt-link
  95. :to="link.path"
  96. @click="handleMouseLeave"
  97. 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"
  98. :class="{
  99. 'text-white font-medium bg-white/10':
  100. route.path === link.path,
  101. }"
  102. >
  103. <span
  104. class="relative flex items-center gap-3"
  105. >
  106. <span
  107. class="w-1.5 h-1.5 rounded-full bg-white/30 group-hover/item:bg-white/60 transition-colors duration-200"
  108. ></span>
  109. <span class="text-base">{{
  110. t(link.label)
  111. }}</span>
  112. </span>
  113. </nuxt-link>
  114. </li>
  115. </ul>
  116. </div>
  117. </div>
  118. <!-- 个人用户产品组 -->
  119. <div class="relative group/card">
  120. <div class="relative space-y-4">
  121. <div
  122. 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"
  123. >
  124. <div class="flex items-center gap-4">
  125. <div
  126. 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"
  127. >
  128. <i
  129. class="icon-user text-2xl text-cyan-400"
  130. ></i>
  131. </div>
  132. <div>
  133. <span
  134. class="block text-lg font-medium tracking-wide text-white"
  135. >{{ t("common.personal") }}</span
  136. >
  137. <span
  138. class="text-xs text-gray-400 mt-0.5 block"
  139. >{{ t("common.personalDesc") }}</span
  140. >
  141. </div>
  142. </div>
  143. </div>
  144. <ul class="space-y-2">
  145. <li
  146. v-for="link in getGroupedItems(section.items)
  147. .personal"
  148. :key="link.path"
  149. class="group/item"
  150. >
  151. <nuxt-link
  152. :to="link.path"
  153. @click="handleMouseLeave"
  154. 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"
  155. :class="{
  156. 'text-white font-medium bg-white/10 is-active':
  157. route.path === link.path,
  158. }"
  159. >
  160. <span
  161. class="relative flex items-center gap-3"
  162. >
  163. <span
  164. class="w-1.5 h-1.5 rounded-full bg-white/30 group-hover/item:bg-white/60 transition-colors duration-200"
  165. ></span>
  166. <span class="text-base">{{
  167. t(link.label)
  168. }}</span>
  169. </span>
  170. </nuxt-link>
  171. </li>
  172. </ul>
  173. </div>
  174. </div>
  175. </div>
  176. </div>
  177. </div>
  178. </transition>
  179. </div>
  180. </template>
  181. </nav>
  182. </div>
  183. <div class="flex justify-start items-center gap-4 md:gap-6">
  184. <!-- Search -->
  185. <div
  186. @click="openSearch"
  187. 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"
  188. style="border: 0.5px solid rgba(255, 255, 255, 0.4)"
  189. >
  190. <span
  191. class="flex items-center justify-center w-8 h-8 opacity-80 hover:opacity-100 text-white"
  192. >
  193. <i class="icon-search text-sm"></i>
  194. </span>
  195. <span
  196. class="hidden lg:inline-block ml-1 text-white text-sm opacity-80"
  197. >
  198. {{ t("common.search") }}
  199. </span>
  200. <!-- Input overlay could go here if implementing search -->
  201. </div>
  202. <!-- Language -->
  203. <LanguageSwitcher />
  204. <!-- Mobile Menu Button -->
  205. <div class="md:hidden">
  206. <button
  207. @click="toggleMobileMenu"
  208. 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"
  209. >
  210. <span class="sr-only">Open main menu</span>
  211. <!-- Icon when menu is closed. -->
  212. <svg
  213. v-if="!mobileMenuOpen"
  214. class="block h-6 w-6"
  215. xmlns="http://www.w3.org/2000/svg"
  216. fill="none"
  217. viewBox="0 0 24 24"
  218. stroke="currentColor"
  219. aria-hidden="true"
  220. >
  221. <path
  222. stroke-linecap="round"
  223. stroke-linejoin="round"
  224. stroke-width="2"
  225. d="M4 6h16M4 12h16M4 18h16"
  226. />
  227. </svg>
  228. <!-- Icon when menu is open. -->
  229. <svg
  230. v-else
  231. class="block h-6 w-6"
  232. xmlns="http://www.w3.org/2000/svg"
  233. fill="none"
  234. viewBox="0 0 24 24"
  235. stroke="currentColor"
  236. aria-hidden="true"
  237. >
  238. <path
  239. stroke-linecap="round"
  240. stroke-linejoin="round"
  241. stroke-width="2"
  242. d="M6 18L18 6M6 6l12 12"
  243. />
  244. </svg>
  245. </button>
  246. </div>
  247. </div>
  248. </div>
  249. </div>
  250. <!-- Mobile menu, show/hide based on menu state. -->
  251. <transition name="slide-fade">
  252. <div
  253. v-if="mobileMenuOpen"
  254. class="md:hidden bg-slate-800/90"
  255. id="mobile-menu"
  256. >
  257. <div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
  258. <template v-for="item in menuItems" :key="item.label">
  259. <!-- Mobile Regular Link -->
  260. <nuxt-link
  261. v-if="!item.isDropdown"
  262. :to="item.path"
  263. @click="closeMobileMenu"
  264. class="block px-3 py-2 rounded-md text-base font-medium"
  265. :class="[
  266. route.path === item.path
  267. ? 'bg-gray-900 text-white'
  268. : 'text-gray-300 hover:bg-gray-700 hover:text-white',
  269. ]"
  270. >
  271. {{ t(item.label) }}
  272. </nuxt-link>
  273. <!-- Mobile Dropdown Section -->
  274. <div v-else class="mt-2">
  275. <h3
  276. class="px-3 pt-2 pb-1 text-sm font-semibold text-gray-400 uppercase tracking-wider"
  277. >
  278. {{ t(item.label) }}
  279. </h3>
  280. <div
  281. v-for="section in item.children"
  282. :key="section.title"
  283. class="mt-1"
  284. >
  285. <nuxt-link
  286. v-for="link in section.items"
  287. :key="link.path"
  288. :to="link.path"
  289. @click="closeMobileMenu"
  290. class="block pl-6 pr-3 py-2 rounded-md text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white"
  291. :class="{
  292. 'bg-gray-900 text-white': route.path === link.path,
  293. }"
  294. >
  295. {{ t(link.label) }}
  296. </nuxt-link>
  297. </div>
  298. </div>
  299. </template>
  300. </div>
  301. </div>
  302. </transition>
  303. </header>
  304. <!-- Search Layer (Moved outside header) -->
  305. <transition name="search-fade-scale">
  306. <div
  307. v-if="isSearchOpen"
  308. class="fixed inset-0 z-[60] bg-black/80 backdrop-blur-md flex items-start justify-center pt-20"
  309. @click.self="closeSearch"
  310. >
  311. <div
  312. 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"
  313. >
  314. <button
  315. @click="closeSearch"
  316. class="absolute top-3 right-3 text-gray-500 hover:text-white hover:bg-white/10 rounded-full p-2 transition-colors"
  317. >
  318. <svg
  319. class="h-5 w-5"
  320. fill="none"
  321. viewBox="0 0 24 24"
  322. stroke="currentColor"
  323. >
  324. <path
  325. stroke-linecap="round"
  326. stroke-linejoin="round"
  327. stroke-width="2"
  328. d="M6 18L18 6M6 6l12 12"
  329. />
  330. </svg>
  331. </button>
  332. <h2 class="text-white text-xl mb-6">{{ t("common.search") }}</h2>
  333. <!-- Input with Icon -->
  334. <div class="relative mb-6">
  335. <span class="absolute inset-y-0 left-0 flex items-center pl-3">
  336. <i class="icon-search text-gray-400 text-sm"></i>
  337. </span>
  338. <input
  339. ref="searchInputRef"
  340. v-model="searchQuery"
  341. type="text"
  342. :placeholder="
  343. t('common.searchPlaceholder') || 'Enter search term...'
  344. "
  345. class="w-full p-3 pl-10 pr-10 rounded bg-slate-700 text-white border border-slate-600 focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500"
  346. @input="handleSearch"
  347. />
  348. <!-- Clear Icon -->
  349. <span
  350. v-if="searchQuery"
  351. class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer group"
  352. @click="clearSearch"
  353. >
  354. <button
  355. class="flex items-center justify-center w-8 h-8 rounded-full bg-zinc-700/80 hover:bg-cyan-500/90 text-gray-300 hover:text-white transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500"
  356. :aria-label="t('common.clear')"
  357. tabindex="0"
  358. type="button"
  359. >
  360. <svg
  361. xmlns="http://www.w3.org/2000/svg"
  362. class="h-5 w-5"
  363. fill="none"
  364. viewBox="0 0 24 24"
  365. stroke="currentColor"
  366. stroke-width="2"
  367. >
  368. <path
  369. stroke-linecap="round"
  370. stroke-linejoin="round"
  371. d="M6 18L18 6M6 6l12 12"
  372. />
  373. </svg>
  374. </button>
  375. </span>
  376. </div>
  377. <!-- Search Results -->
  378. <div v-if="isLoading" class="text-gray-400 text-sm">
  379. {{ t("common.searching") }}
  380. </div>
  381. <div v-if="error" class="text-red-400 text-sm">
  382. {{ error }}
  383. </div>
  384. <div
  385. v-if="searchResults.length > 0"
  386. class="mt-4 max-h-[50vh] overflow-y-auto pr-2 space-y-4 search-results"
  387. >
  388. <div
  389. v-for="result in searchResults"
  390. :key="result.id"
  391. class="p-3 bg-slate-700/50 rounded-lg cursor-pointer hover:bg-slate-700 transition-colors group"
  392. >
  393. <nuxt-link
  394. :to="`${homePath}products/${result.id}`"
  395. class="block"
  396. @click="closeSearch"
  397. >
  398. <div
  399. class="text-white font-medium mb-1 group-hover:text-cyan-400 transition-colors"
  400. v-html="highlightText(result.title, searchQuery)"
  401. ></div>
  402. <div
  403. class="text-gray-400 text-sm"
  404. v-html="highlightText(result.description, searchQuery)"
  405. ></div>
  406. <div
  407. class="text-gray-500 text-xs mt-1"
  408. v-html="highlightText(result.summary, searchQuery)"
  409. ></div>
  410. </nuxt-link>
  411. </div>
  412. </div>
  413. <div
  414. v-else-if="searchQuery && !isLoading"
  415. class="text-gray-400 text-sm mt-4"
  416. >
  417. {{ t("common.noResults") }}
  418. </div>
  419. <!-- Hot Keywords Section -->
  420. <div class="mt-6">
  421. <h3 class="text-gray-400 text-sm mb-3">
  422. {{ t("common.hotKeywords") }}
  423. </h3>
  424. <div class="flex flex-wrap gap-3">
  425. <button
  426. v-for="keyword in hotKeywords"
  427. :key="keyword"
  428. @click="searchHotKeyword(keyword)"
  429. class="px-4 py-1.5 bg-slate-700 text-white/80 rounded-full text-sm hover:bg-cyan-600 hover:text-white transition-colors duration-200"
  430. >
  431. {{ keyword }}
  432. </button>
  433. </div>
  434. </div>
  435. </div>
  436. </div>
  437. </transition>
  438. </template>
  439. <script setup lang="ts">
  440. import { ref, computed, watch, nextTick } from "#imports";
  441. import { useI18n } from "vue-i18n";
  442. import { useSearch } from "~/composables/useSearch";
  443. /**
  444. * 页面头部组件
  445. * 包含导航菜单、语言切换和移动端响应式设计
  446. */
  447. const { t, locale } = useI18n();
  448. const config = useRuntimeConfig();
  449. const defaultLocale = config.public.i18n?.defaultLocale || "zh";
  450. const mobileMenuOpen = ref(false);
  451. const isSearchOpen = ref(false);
  452. const searchInputRef = ref<HTMLInputElement | null>(null);
  453. const openDropdown = ref<string | null>(null);
  454. let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mouseleave delay
  455. const route = useRoute();
  456. const searchQuery = ref("");
  457. const { searchResults, isLoading, error, searchProducts, highlightText } =
  458. useSearch();
  459. // 添加热门关键字
  460. const hotKeywords = ref(["SSD", "SD", "DDR4"]);
  461. // 获取产品分类数据
  462. const { data: categoryResponse } = await useAsyncData(
  463. `header-categories-${locale.value}`,
  464. async () => {
  465. try {
  466. // 使用queryCollection从content目录获取数据
  467. const content = await queryCollection("content")
  468. .where("path", "LIKE", `/categories/${locale.value}/%`)
  469. .all();
  470. if (!content || !Array.isArray(content)) {
  471. console.log("No category content found for header");
  472. return [];
  473. }
  474. // 转换为需要的格式
  475. return content
  476. .map((item: any) => {
  477. console.log(item);
  478. return {
  479. label: item.title || "",
  480. path: `/products?category=${encodeURIComponent(item.title)}&audiences=${item.meta.audiences}`,
  481. id: item.meta.id,
  482. audiences: item.meta.audiences,
  483. };
  484. })
  485. .sort((a, b) => (a.id || 0) - (b.id || 0));
  486. } catch (error) {
  487. console.error("Error loading category data for header:", error);
  488. return [];
  489. }
  490. }
  491. );
  492. // 获取产品用途数据
  493. const { data: usageResponse } = await useAsyncData(
  494. `header-usages-${locale.value}`,
  495. async () => {
  496. try {
  497. // 使用queryCollection从content目录获取数据
  498. const content = await queryCollection("content")
  499. .where("path", "LIKE", `/usages/${locale.value}/%`)
  500. .all();
  501. if (!content || !Array.isArray(content)) {
  502. console.log("No usage content found for header");
  503. return [];
  504. }
  505. // 转换为需要的格式
  506. return content
  507. .map((item: any) => {
  508. // 从路径中提取ID - 文件名就是ID
  509. const pathParts = item.path?.split("/");
  510. const idFile = pathParts?.[pathParts.length - 1] || "";
  511. // 从文件名提取ID,去掉可能的扩展名
  512. const id = parseInt(idFile.replace(".md", "")) || 0;
  513. return {
  514. label: item.title,
  515. path: `/products?usage=${encodeURIComponent(item.title)}`,
  516. id: id,
  517. };
  518. })
  519. .sort((a, b) => (a.id || 0) - (b.id || 0));
  520. } catch (error) {
  521. console.error("Error loading usage data for header:", error);
  522. return [];
  523. }
  524. }
  525. );
  526. // 使用计算属性处理产品分类数据
  527. const productCategories = computed(() => {
  528. return categoryResponse.value || [];
  529. });
  530. // 使用计算属性处理产品用途数据
  531. const productUsages = computed(() => {
  532. return usageResponse.value || [];
  533. });
  534. // 使用 computed 来定义 homePath,根据是否为默认语言调整路径
  535. const homePath = computed(() => {
  536. // 如果是默认语言,路径为根路径 '/'
  537. // 否则,路径为 '/<locale>/'
  538. return locale.value === defaultLocale ? "/" : `/${locale.value}/`;
  539. });
  540. // 使用 computed 来定义 menuItems,根据是否为默认语言调整路径
  541. const menuItems = computed(() => {
  542. // 判断当前是否为默认语言
  543. const isDefaultLocale = locale.value === defaultLocale;
  544. // 如果是默认语言,路径前缀为空字符串,否则为 '/<locale>'
  545. const prefix = isDefaultLocale ? "" : `/${locale.value}`;
  546. return [
  547. // 首页路径特殊处理:默认语言为 '/', 其他语言为 '/<locale>/'
  548. { label: "common.home", path: isDefaultLocale ? "/" : `${prefix}/` },
  549. {
  550. label: "common.products",
  551. isDropdown: true,
  552. pathPrefix: `${prefix}/products`,
  553. children: [
  554. {
  555. title: "common.productCategories",
  556. items: productCategories.value.map((category: any) => ({
  557. ...category,
  558. path: `${prefix}${category.path}`,
  559. })),
  560. },
  561. ],
  562. },
  563. { label: "common.faq", path: `${prefix}/faq` },
  564. { label: "common.about", path: `${prefix}/about` },
  565. { label: "common.contact", path: `${prefix}/contact` },
  566. ];
  567. });
  568. /**
  569. * 切换移动端菜单显示状态
  570. */
  571. function toggleMobileMenu() {
  572. mobileMenuOpen.value = !mobileMenuOpen.value;
  573. }
  574. /**
  575. * 关闭移动端菜单
  576. */
  577. function closeMobileMenu() {
  578. mobileMenuOpen.value = false;
  579. }
  580. /**
  581. * 打开搜索层
  582. */
  583. function openSearch() {
  584. isSearchOpen.value = true;
  585. }
  586. /**
  587. * 关闭搜索层
  588. */
  589. function closeSearch() {
  590. isSearchOpen.value = false;
  591. }
  592. /**
  593. * 搜索热门关键字
  594. * @param keyword 关键词
  595. */
  596. function searchHotKeyword(keyword: string) {
  597. searchQuery.value = keyword;
  598. handleSearch();
  599. }
  600. // 监听搜索层状态,打开时自动聚焦输入框
  601. watch(isSearchOpen, (newValue: boolean) => {
  602. if (newValue) {
  603. nextTick(() => {
  604. searchInputRef.value?.focus();
  605. });
  606. }
  607. });
  608. // --- Dropdown Logic ---
  609. function handleMouseEnter(label: string) {
  610. if (leaveTimeout) {
  611. clearTimeout(leaveTimeout);
  612. leaveTimeout = null;
  613. }
  614. openDropdown.value = label;
  615. }
  616. function handleMouseLeave() {
  617. // Delay closing the dropdown slightly
  618. leaveTimeout = setTimeout(() => {
  619. openDropdown.value = null;
  620. }, 150); // 150ms delay
  621. }
  622. // --- End Dropdown Logic ---
  623. // 防抖函数
  624. const debounce = (fn: Function, delay: number) => {
  625. let timer: NodeJS.Timeout;
  626. return (...args: any[]) => {
  627. clearTimeout(timer);
  628. timer = setTimeout(() => fn(...args), delay);
  629. };
  630. };
  631. // 处理搜索输入
  632. const handleSearch = debounce(async () => {
  633. await searchProducts(searchQuery.value);
  634. }, 300);
  635. /**
  636. * 清空搜索
  637. */
  638. const clearSearch = () => {
  639. searchQuery.value = "";
  640. searchResults.value = [];
  641. };
  642. /**
  643. * 将产品按照用户类型分组
  644. * @param items 产品列表
  645. * @returns 分组后的产品对象
  646. */
  647. function getGroupedItems(items: any[]) {
  648. return {
  649. personal: items.filter((item) => item.audiences === 0),
  650. enterprise: items.filter((item) => item.audiences === 1),
  651. };
  652. }
  653. </script>
  654. <style lang="scss" scoped>
  655. header {
  656. user-select: none;
  657. }
  658. /* Brand icon hover effect */
  659. .brand-link {
  660. &:hover {
  661. .icon-brand {
  662. transform: scale(1.05);
  663. filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
  664. }
  665. }
  666. }
  667. /* Base style for icon to apply will-change */
  668. .icon-brand {
  669. will-change: transform, filter; /* Hint for browser optimization */
  670. }
  671. /* Transition for mobile menu */
  672. .slide-fade-enter-active {
  673. transition: all 0.3s ease-out;
  674. }
  675. .slide-fade-leave-active {
  676. transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
  677. }
  678. .slide-fade-enter-from,
  679. .slide-fade-leave-to {
  680. transform: translateY(-20px);
  681. opacity: 0;
  682. }
  683. /* Transition for dropdown */
  684. .fade-down-enter-active,
  685. .fade-down-leave-active {
  686. transition: all 0.2s ease-out;
  687. }
  688. .fade-down-enter-from,
  689. .fade-down-leave-to {
  690. opacity: 0;
  691. transform: translateY(-10px);
  692. }
  693. /* Transition for search overlay */
  694. .search-fade-scale-enter-active,
  695. .search-fade-scale-leave-active {
  696. transition: opacity 0.3s ease-out;
  697. }
  698. .search-fade-scale-enter-from,
  699. .search-fade-scale-leave-to {
  700. opacity: 0;
  701. }
  702. /* 为搜索框内容添加独立的、稍延迟的动画 */
  703. .search-modal-content {
  704. transition: transform 0.3s ease-out 0.05s;
  705. }
  706. .search-fade-scale-enter-from .search-modal-content,
  707. .search-fade-scale-leave-to .search-modal-content {
  708. transform: scale(0.95) translateY(10px);
  709. }
  710. /* Keep the sticky header consistent */
  711. .sticky {
  712. position: sticky;
  713. }
  714. /* 搜索结果高亮样式 */
  715. :deep(.highlight) {
  716. background-color: rgba(59, 130, 246, 0.2);
  717. padding: 0 2px;
  718. border-radius: 2px;
  719. color: #60a5fa;
  720. }
  721. /* 搜索结果动画 */
  722. .search-results-enter-active,
  723. .search-results-leave-active {
  724. transition: all 0.3s ease;
  725. }
  726. .search-results-enter-from,
  727. .search-results-leave-to {
  728. opacity: 0;
  729. transform: translateY(-10px);
  730. }
  731. /* 搜索结果滚动条样式 */
  732. .search-results {
  733. &::-webkit-scrollbar {
  734. width: 6px;
  735. }
  736. &::-webkit-scrollbar-track {
  737. background: rgba(255, 255, 255, 0.1);
  738. border-radius: 3px;
  739. }
  740. &::-webkit-scrollbar-thumb {
  741. background: rgba(255, 255, 255, 0.2);
  742. border-radius: 3px;
  743. &:hover {
  744. background: rgba(255, 255, 255, 0.3);
  745. }
  746. }
  747. }
  748. /* 菜单项激活状态样式 */
  749. .is-active {
  750. position: relative;
  751. &::before {
  752. content: "";
  753. position: absolute;
  754. left: 0;
  755. top: 50%;
  756. transform: translateY(-50%);
  757. width: 2px;
  758. height: 16px;
  759. background: #fff;
  760. border-radius: 0 1px 1px 0;
  761. }
  762. }
  763. /* 菜单组标题样式 */
  764. .group-title {
  765. position: relative;
  766. overflow: hidden;
  767. &::after {
  768. content: "";
  769. position: absolute;
  770. left: 0;
  771. bottom: 0;
  772. width: 100%;
  773. height: 1px;
  774. background: linear-gradient(
  775. 90deg,
  776. transparent,
  777. rgba(255, 255, 255, 0.2),
  778. transparent
  779. );
  780. }
  781. }
  782. /* 添加图标样式 */
  783. .icon-user,
  784. .icon-building {
  785. font-size: 20px;
  786. opacity: 0.8;
  787. transition: opacity 0.2s ease;
  788. }
  789. /* 优化过渡动画 */
  790. .transition-all {
  791. transition-property: all;
  792. transition-timing-function: ease;
  793. }
  794. /* 添加阴影效果 */
  795. .shadow-2xl {
  796. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
  797. 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  798. }
  799. /* 添加玻璃态效果 */
  800. .backdrop-blur-sm {
  801. backdrop-filter: blur(8px);
  802. }
  803. /* 移除复杂的动画效果 */
  804. .bg-gradient-to-br {
  805. background-size: 100% 100%;
  806. }
  807. /* 移除波纹效果 */
  808. .group\/item:hover {
  809. &::before {
  810. display: none;
  811. }
  812. }
  813. </style>