123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884 |
- <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"
- :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-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"
- >
- <div
- v-for="(section, index) in item.children"
- :key="index"
- class="rounded-none p-2 flex flex-col gap-1"
- >
- <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>
- </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
- 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
- >
- </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
- 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>
- </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 -->
- </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>
-
- <!-- Mobile menu, show/hide based on menu state. -->
- <transition name="slide-fade">
- <div
- v-if="mobileMenuOpen"
- class="md:hidden bg-slate-800/90"
- id="mobile-menu"
- >
- <div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
- <template v-for="item in menuItems" :key="item.label">
- <!-- Mobile Regular Link -->
- <nuxt-link
- v-if="!item.isDropdown"
- :to="item.path"
- @click="closeMobileMenu"
- class="block px-3 py-2 rounded-md text-base font-medium"
- :class="[
- route.path === item.path
- ? 'bg-gray-900 text-white'
- : 'text-gray-300 hover:bg-gray-700 hover:text-white',
- ]"
- >
- {{ t(item.label) }}
- </nuxt-link>
-
- <!-- Mobile Dropdown Section -->
- <div v-else class="mt-2">
- <h3
- class="px-3 pt-2 pb-1 text-sm font-semibold text-gray-400 uppercase tracking-wider"
- >
- {{ t(item.label) }}
- </h3>
- <div
- v-for="section in item.children"
- :key="section.title"
- class="mt-1"
- >
- <nuxt-link
- v-for="link in section.items"
- :key="link.path"
- :to="link.path"
- @click="closeMobileMenu"
- class="block pl-6 pr-3 py-2 rounded-md text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white"
- :class="{
- 'bg-gray-900 text-white': route.path === link.path,
- }"
- >
- {{ t(link.label) }}
- </nuxt-link>
- </div>
- </div>
- </template>
- </div>
- </div>
- </transition>
- </header>
-
- <!-- Search Layer (Moved outside header) -->
- <transition name="search-fade-scale">
- <div
- v-if="isSearchOpen"
- class="fixed inset-0 z-[60] bg-black/80 backdrop-blur-md flex items-start justify-center pt-20"
- @click.self="closeSearch"
- >
- <div
- 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"
- >
- <button
- @click="closeSearch"
- class="absolute top-3 right-3 text-gray-500 hover:text-white hover:bg-white/10 rounded-full p-2 transition-colors"
- >
- <svg
- class="h-5 w-5"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M6 18L18 6M6 6l12 12"
- />
- </svg>
- </button>
- <h2 class="text-white text-xl mb-6">{{ t("common.search") }}</h2>
-
- <!-- Input with Icon -->
- <div class="relative mb-6">
- <span class="absolute inset-y-0 left-0 flex items-center pl-3">
- <i class="icon-search text-gray-400 text-sm"></i>
- </span>
- <input
- ref="searchInputRef"
- v-model="searchQuery"
- type="text"
- :placeholder="
- t('common.searchPlaceholder') || 'Enter search term...'
- "
- 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"
- @input="handleSearch"
- />
- <!-- Clear Icon -->
- <span
- v-if="searchQuery"
- class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer group"
- @click="clearSearch"
- >
- <button
- 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"
- :aria-label="t('common.clear')"
- tabindex="0"
- type="button"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-5 w-5"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- stroke-width="2"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="M6 18L18 6M6 6l12 12"
- />
- </svg>
- </button>
- </span>
- </div>
-
- <!-- Search Results -->
- <div v-if="isLoading" class="text-gray-400 text-sm">
- {{ t("common.searching") }}
- </div>
- <div v-if="error" class="text-red-400 text-sm">
- {{ error }}
- </div>
- <div
- v-if="searchResults.length > 0"
- class="mt-4 max-h-[50vh] overflow-y-auto pr-2 space-y-4 search-results"
- >
- <div
- v-for="result in searchResults"
- :key="result.id"
- class="p-3 bg-slate-700/50 rounded-lg cursor-pointer hover:bg-slate-700 transition-colors group"
- >
- <nuxt-link
- :to="`${homePath}products/${result.name}`"
- class="block"
- @click="closeSearch"
- >
- <div
- class="text-white font-medium mb-1 group-hover:text-cyan-400 transition-colors"
- v-html="highlightText(result.title, searchQuery)"
- ></div>
- <div
- class="text-gray-400 text-sm"
- v-html="highlightText(result.description, searchQuery)"
- ></div>
- <div
- class="text-gray-500 text-xs mt-1"
- v-html="highlightText(result.summary, searchQuery)"
- ></div>
- </nuxt-link>
- </div>
- </div>
- <div
- v-else-if="searchQuery && !isLoading"
- class="text-gray-400 text-sm mt-4"
- >
- {{ t("common.noResults") }}
- </div>
-
- <!-- Hot Keywords Section -->
- <div class="mt-6">
- <h3 class="text-gray-400 text-sm mb-3">
- {{ t("common.hotKeywords") }}
- </h3>
- <div class="flex flex-wrap gap-3">
- <button
- v-for="keyword in hotKeywords"
- :key="keyword"
- @click="searchHotKeyword(keyword)"
- 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"
- >
- {{ keyword }}
- </button>
- </div>
- </div>
- </div>
- </div>
- </transition>
- </template>
-
- <script setup lang="ts">
- import { ref, computed, watch, nextTick } from "#imports";
- import { useI18n } from "vue-i18n";
- import { useSearch } from "~/composables/useSearch";
-
- /**
- * 页面头部组件
- * 包含导航菜单、语言切换和移动端响应式设计
- */
- const { t, locale } = useI18n();
- const config = useRuntimeConfig();
- const defaultLocale = config.public.i18n?.defaultLocale || "zh";
- const mobileMenuOpen = ref(false);
- const isSearchOpen = ref(false);
- const searchInputRef = ref<HTMLInputElement | null>(null);
- const openDropdown = ref<string | null>(null);
- let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mouseleave delay
-
- const route = useRoute();
- const searchQuery = ref("");
- const { searchResults, isLoading, error, searchProducts, highlightText } =
- useSearch();
-
- // 添加热门关键字
- const hotKeywords = ref(["SSD", "SD", "DDR4"]);
-
- // 获取产品分类数据
- const { data: categoryResponse } = await useAsyncData(
- `header-categories-${locale.value}`,
- async () => {
- try {
- // 使用queryCollection从content目录获取数据
- const content = await queryCollection("content")
- .where("path", "LIKE", `/categories/${locale.value}/%`)
- .all();
-
- if (!content || !Array.isArray(content)) {
- console.log("No category content found for header");
- return [];
- }
-
- // 转换为需要的格式
- return content
- .map((item: any) => {
- console.log(item);
- return {
- label: item.title,
- path: `/products?category=${encodeURIComponent(
- item.title
- )}&audiences=${item.meta.audiences}`,
- id: item.meta.id,
- audiences: item.meta.audiences,
- };
- })
- .sort((a, b) => (a.id || 0) - (b.id || 0));
- } catch (error) {
- console.error("Error loading category data for header:", error);
- return [];
- }
- }
- );
-
- // 获取产品用途数据
- const { data: usageResponse } = await useAsyncData(
- `header-usages-${locale.value}`,
- async () => {
- try {
- // 使用queryCollection从content目录获取数据
- const content = await queryCollection("content")
- .where("path", "LIKE", `/usages/${locale.value}/%`)
- .all();
-
- if (!content || !Array.isArray(content)) {
- console.log("No usage content found for header");
- return [];
- }
-
- // 转换为需要的格式
- return content
- .map((item: any) => {
- // 从路径中提取ID - 文件名就是ID
- const pathParts = item.path?.split("/");
- const idFile = pathParts?.[pathParts.length - 1] || "";
- // 从文件名提取ID,去掉可能的扩展名
- const id = parseInt(idFile.replace(".md", "")) || 0;
-
- return {
- label: item.title,
- path: `/products?usage=${encodeURIComponent(item.title)}`,
- id: id,
- };
- })
- .sort((a, b) => (a.id || 0) - (b.id || 0));
- } catch (error) {
- console.error("Error loading usage data for header:", error);
- return [];
- }
- }
- );
-
- // 使用计算属性处理产品分类数据
- const productCategories = computed(() => {
- return categoryResponse.value || [];
- });
-
- // 使用计算属性处理产品用途数据
- const productUsages = computed(() => {
- return usageResponse.value || [];
- });
-
- // 使用 computed 来定义 homePath,根据是否为默认语言调整路径
- const homePath = computed(() => {
- // 如果是默认语言,路径为根路径 '/'
- // 否则,路径为 '/<locale>/'
- return locale.value === defaultLocale ? "/" : `/${locale.value}/`;
- });
-
- // 使用 computed 来定义 menuItems,根据是否为默认语言调整路径
- const menuItems = computed(() => {
- // 判断当前是否为默认语言
- const isDefaultLocale = locale.value === defaultLocale;
- // 如果是默认语言,路径前缀为空字符串,否则为 '/<locale>'
- const prefix = isDefaultLocale ? "" : `/${locale.value}`;
- return [
- // 首页路径特殊处理:默认语言为 '/', 其他语言为 '/<locale>/'
- { label: "common.home", path: isDefaultLocale ? "/" : `${prefix}/` },
- {
- label: "common.products",
- isDropdown: true,
- pathPrefix: `${prefix}/products`,
- children: [
- {
- title: "common.productCategories",
- items: productCategories.value.map((category: any) => ({
- ...category,
- path: `${prefix}${category.path}`,
- })),
- },
- ],
- },
- { label: "common.faq", path: `${prefix}/faq` },
- { label: "common.about", path: `${prefix}/about` },
- { label: "common.contact", path: `${prefix}/contact` },
- ];
- });
-
- /**
- * 切换移动端菜单显示状态
- */
- function toggleMobileMenu() {
- mobileMenuOpen.value = !mobileMenuOpen.value;
- }
-
- /**
- * 关闭移动端菜单
- */
- function closeMobileMenu() {
- mobileMenuOpen.value = false;
- }
-
- /**
- * 打开搜索层
- */
- function openSearch() {
- isSearchOpen.value = true;
- }
-
- /**
- * 关闭搜索层
- */
- function closeSearch() {
- isSearchOpen.value = false;
- }
-
- /**
- * 搜索热门关键字
- * @param keyword 关键词
- */
- function searchHotKeyword(keyword: string) {
- searchQuery.value = keyword;
- handleSearch();
- }
-
- // 监听搜索层状态,打开时自动聚焦输入框
- watch(isSearchOpen, (newValue: boolean) => {
- if (newValue) {
- nextTick(() => {
- searchInputRef.value?.focus();
- });
- }
- });
-
- // --- Dropdown Logic ---
- function handleMouseEnter(label: string) {
- if (leaveTimeout) {
- clearTimeout(leaveTimeout);
- leaveTimeout = null;
- }
- openDropdown.value = label;
- }
-
- function handleMouseLeave() {
- // Delay closing the dropdown slightly
- leaveTimeout = setTimeout(() => {
- openDropdown.value = null;
- }, 150); // 150ms delay
- }
- // --- End Dropdown Logic ---
-
- // 防抖函数
- const debounce = (fn: Function, delay: number) => {
- let timer: NodeJS.Timeout;
- return (...args: any[]) => {
- clearTimeout(timer);
- timer = setTimeout(() => fn(...args), delay);
- };
- };
-
- // 处理搜索输入
- const handleSearch = debounce(async () => {
- await searchProducts(searchQuery.value);
- }, 300);
-
- /**
- * 清空搜索
- */
- const clearSearch = () => {
- searchQuery.value = "";
- searchResults.value = [];
- };
-
- /**
- * 将产品按照用户类型分组
- * @param items 产品列表
- * @returns 分组后的产品对象
- */
- function getGroupedItems(items: any[]) {
- return {
- personal: items.filter((item) => item.audiences === 0),
- enterprise: items.filter((item) => item.audiences === 1),
- };
- }
- </script>
-
- <style lang="scss" scoped>
- header {
- user-select: none;
- }
-
- /* Brand icon hover effect */
- .brand-link {
- &:hover {
- .icon-brand {
- transform: scale(1.05);
- filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
- }
- }
- }
-
- /* Base style for icon to apply will-change */
- .icon-brand {
- will-change: transform, filter; /* Hint for browser optimization */
- }
-
- /* Transition for mobile menu */
- .slide-fade-enter-active {
- transition: all 0.3s ease-out;
- }
-
- .slide-fade-leave-active {
- transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
- }
-
- .slide-fade-enter-from,
- .slide-fade-leave-to {
- transform: translateY(-20px);
- opacity: 0;
- }
-
- /* Transition for dropdown */
- .fade-down-enter-active,
- .fade-down-leave-active {
- transition: all 0.2s ease-out;
- }
-
- .fade-down-enter-from,
- .fade-down-leave-to {
- opacity: 0;
- transform: translateY(-10px);
- }
-
- /* Transition for search overlay */
- .search-fade-scale-enter-active,
- .search-fade-scale-leave-active {
- transition: opacity 0.3s ease-out;
- }
-
- .search-fade-scale-enter-from,
- .search-fade-scale-leave-to {
- opacity: 0;
- }
-
- /* 为搜索框内容添加独立的、稍延迟的动画 */
- .search-modal-content {
- transition: transform 0.3s ease-out 0.05s;
- }
-
- .search-fade-scale-enter-from .search-modal-content,
- .search-fade-scale-leave-to .search-modal-content {
- transform: scale(0.95) translateY(10px);
- }
-
- /* Keep the sticky header consistent */
- .sticky {
- position: sticky;
- }
-
- /* 搜索结果高亮样式 */
- :deep(.highlight) {
- background-color: rgba(59, 130, 246, 0.2);
- padding: 0 2px;
- border-radius: 2px;
- color: #60a5fa;
- }
-
- /* 搜索结果动画 */
- .search-results-enter-active,
- .search-results-leave-active {
- transition: all 0.3s ease;
- }
-
- .search-results-enter-from,
- .search-results-leave-to {
- opacity: 0;
- transform: translateY(-10px);
- }
-
- /* 搜索结果滚动条样式 */
- .search-results {
- &::-webkit-scrollbar {
- width: 6px;
- }
-
- &::-webkit-scrollbar-track {
- background: rgba(255, 255, 255, 0.1);
- border-radius: 3px;
- }
-
- &::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.2);
- border-radius: 3px;
-
- &:hover {
- background: rgba(255, 255, 255, 0.3);
- }
- }
- }
-
- /* 菜单项激活状态样式 */
- .is-active {
- position: relative;
-
- &::before {
- content: "";
- position: absolute;
- left: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 2px;
- height: 16px;
- background: #fff;
- border-radius: 0 1px 1px 0;
- }
- }
-
- /* 菜单组标题样式 */
- .group-title {
- position: relative;
- overflow: hidden;
-
- &::after {
- content: "";
- position: absolute;
- left: 0;
- bottom: 0;
- width: 100%;
- height: 1px;
- background: linear-gradient(
- 90deg,
- transparent,
- rgba(255, 255, 255, 0.2),
- transparent
- );
- }
- }
-
- /* 添加图标样式 */
- .icon-user,
- .icon-building {
- font-size: 20px;
- opacity: 0.8;
- transition: opacity 0.2s ease;
- }
-
- /* 优化过渡动画 */
- .transition-all {
- transition-property: all;
- transition-timing-function: ease;
- }
-
- /* 添加阴影效果 */
- .shadow-2xl {
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
- 0 2px 4px -1px rgba(0, 0, 0, 0.06);
- }
-
- /* 添加玻璃态效果 */
- .backdrop-blur-sm {
- backdrop-filter: blur(8px);
- }
-
- /* 移除复杂的动画效果 */
- .bg-gradient-to-br {
- background-size: 100% 100%;
- }
-
- /* 移除波纹效果 */
- .group\/item:hover {
- &::before {
- display: none;
- }
- }
- </style>
|