Hanye官网
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

TheHeader.vue 22KB

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