瀏覽代碼

Merge branch 'zzf-search'

zhangzf 2 周之前
父節點
當前提交
6a81ab0940

+ 10 - 10
src/app/(public)/product/_components/youmaylike/Recommend.tsx

@@ -3,7 +3,10 @@
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Navigation } from "swiper/modules";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
-
+interface RecommendProps {
+  paddingClassName?: string;
+  titleClassName?: string;
+}
 
 // 模拟产品数据(12个产品,分2块,每块6个)
 const productData = [
@@ -120,18 +123,15 @@ const productData = [
   },
 ];
 
-// 拆分数据为2个块(每块6个)
-// const productBlocks = [
-//   productData.slice(0, 6), // 第一块
-//   productData.slice(6, 12), // 第二块
-// ];
-
-const Recommend = () => {
+const Recommend = ({
+  paddingClassName = "py-8",
+  titleClassName = "text-2xl font-bold",
+}: RecommendProps) => {
   return (
-    <div className="w-full max-w-[1200px] mx-auto  py-8">
+    <div className={`w-full max-w-[1200px] mx-auto ${paddingClassName}`}>
       {/* 标题 */}
   <div className="flex items-center justify-between mb-6 relative">
-    <h2 className="text-2xl font-bold">You May Also Like </h2>
+    <h2 className={titleClassName}>You May Also Like </h2>
     {/* 箭头组,两个按钮并排 */}
     <div className="flex gap-3">
       <div className="recommend-swiper-button-prev  flex items-center justify-center cursor-pointer z-10">

+ 2 - 2
src/app/(public)/search/_components/HotSearch.tsx

@@ -41,7 +41,7 @@ const hotTags = [
 export default function HotSearch() {
 
   return (
-    <div className="w-full max-w-screen-2xl mx-auto px-4 py-6">
+    <div className="w-full max-w-[1200px] mx-auto  py-6 pb-8 ">
       <h2 className="text-base font-medium text-black mb-4">Hot Search</h2>
       <div className="flex flex-wrap items-center justify-start gap-3">
         {hotTags.map((tag) => {
@@ -50,7 +50,7 @@ export default function HotSearch() {
             <Link
               key={tag.name}
               href={tag.href}
-              className="bg-[#F2F2F2FF] rounded-sm px-[15px] py-1.5 flex items-center justify-center text-xs text-black font-normal"
+              className="bg-[#F2F2F2FF] rounded-sm px-[15px] py-2 flex items-center justify-center text-sm text-black font-normal"
             >
               {tag.name}
             </Link>

File diff suppressed because it is too large
+ 63 - 13
src/app/(public)/search/page.tsx


+ 1 - 1
src/components/common/skeleton/SearchSkeleton.tsx

@@ -2,7 +2,7 @@ import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
 
 export const SearchSkeleton = () => {
   return (
-    <div className="max-w-[550px] relative w-full md:min-w-[386px] xl:min-w-[516px]">
+    <div className="max-w-[1200px] relative w-full md:min-w-[386px] xl:min-w-[516px]">
       <input
         className="input w-full rounded-lg border border-neutral-200 bg-white py-2 pl-3 pr-10 text-sm text-black outline-none placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400 md:pl-4"
         placeholder="Search for products..."

+ 1 - 1
src/components/layout/navbar/MobileSearch.tsx

@@ -14,7 +14,7 @@ export const MobileSearchBar = ({
 }) => {
   return (
     <>
-      <div className="block w-full justify-center px-4 mt-3 z-10">
+      <div className="block w-full justify-center mx-auto  mt-8 z-10 max-w-screen-2xl px-4">
         <Suspense fallback={<SearchSkeleton />}>
           <Search search={false} onClose={onClose} />
         </Suspense>

+ 63 - 25
src/components/layout/navbar/NavbarSearchIcon.tsx

@@ -1,36 +1,74 @@
 "use client";
+
 import { useState } from "react";
 import SearchPop from "./SearchPop";
+
 export default function NavbarSearchIcon() {
   const [searchOpen, setSearchOpen] = useState(false);
+
+  const handleClick = () => {
+    setSearchOpen((prev) => !prev);
+  };
+
   return (
     <>
-      <div
-        className="w-6 h-6 flex items-center justify-center"
-        onClick={() => setSearchOpen(true)}
+      <button
+        type="button"
+        aria-label={searchOpen ? "Close search" : "Open search"}
+        onClick={handleClick}
+        className="flex h-6 w-6 cursor-pointer items-center justify-center"
       >
-        <svg
-          xmlns="http://www.w3.org/2000/svg"
-          width="17.7802734375"
-          height="18.7802734375"
-          viewBox="0 0 17.7802734375 18.7802734375"
-          fill="none"
-        >
-          <circle
-            cx="8.75"
-            cy="8.75"
-            r="8"
-            stroke="rgba(0, 0, 0, 1)"
-            strokeWidth="1.5"
-          ></circle>
-          <path
-            stroke="rgba(0, 0, 0, 1)"
-            strokeWidth="1.5"
-            d="M14.25 15.25L17.25 18.25"
-          ></path>
-        </svg>
-      </div>
-      <SearchPop open={searchOpen} onClose={() => setSearchOpen(false)} />
+        {searchOpen ? (
+          /* Close Icon */
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="18"
+            height="18"
+            viewBox="0 0 18 18"
+            fill="none"
+          >
+            <path
+              d="M2 2L16 16"
+              stroke="black"
+              strokeWidth="1.5"
+              strokeLinecap="round"
+            />
+            <path
+              d="M16 2L2 16"
+              stroke="black"
+              strokeWidth="1.5"
+              strokeLinecap="round"
+            />
+          </svg>
+        ) : (
+          /* Search Icon */
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="17.7802734375"
+            height="18.7802734375"
+            viewBox="0 0 17.7802734375 18.7802734375"
+            fill="none"
+          >
+            <circle
+              cx="8.75"
+              cy="8.75"
+              r="8"
+              stroke="black"
+              strokeWidth="1.5"
+            />
+            <path
+              d="M14.25 15.25L17.25 18.25"
+              stroke="black"
+              strokeWidth="1.5"
+            />
+          </svg>
+        )}
+      </button>
+
+      <SearchPop
+        open={searchOpen}
+        onClose={() => setSearchOpen(false)}
+      />
     </>
   );
 }

+ 324 - 105
src/components/layout/navbar/SearchPop.tsx

@@ -1,177 +1,396 @@
 "use client";
-import { useState, useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
+// import dynamicImport from "next/dynamic";
+import Link from "next/link";
 import { useSearchProduct } from "@/utils/hooks/useSearchProduct";
 import Grid from "@/components/theme/ui/grid/Grid";
-import dynamicImport from "next/dynamic";
-import HotSearch from "@/app/(public)/search/_components/HotSearch"
-import { useMediaQuery } from "@utils/hooks/useMediaQueryHook";
-import Link from "next/link";
-const ProductGridItems = dynamicImport(
-  () => import("@/components/catalog/product/ProductGridItems"),
-);
+import HotSearch from "@/app/(public)/search/_components/HotSearch";
+import NewProductGridItems from "@components/catalog/product/NewProductGridItems";
+import Recommend from "@/app/(public)/product/_components/youmaylike/Recommend";
+// const ProductGridItems = dynamicImport(
+//   () => import("@/components/catalog/product/ProductGridItems"),
+// );
 
-// 定义弹窗入参类型
 interface SearchPopProps {
   open: boolean;
   onClose: () => void;
 }
 
-export default function SearchPop({ open, onClose }: SearchPopProps) {
+const placeholderList = [
+  "Search for products...",
+  "Find best sellers",
+  "Search new arrivals",
+  "Search discount items",
+  "Search trending products",
+];
+
+export default function SearchPop({
+  open,
+  onClose,
+}: SearchPopProps) {
   const { data, error, searchProducts } = useSearchProduct();
+
   const [keyword, setKeyword] = useState("");
+  const [placeholderIndex, setPlaceholderIndex] = useState(0);
+
   const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
 
-  const products = data?.edges?.map((e) => e.node) || [];
-  const isDesktop = useMediaQuery("(min-width: 1024px)");
+  const inputRef = useRef<HTMLInputElement>(null);
 
-  // 执行搜索接口
+  const products = data?.edges?.map((e) => e.node) ?? [];
+
+  const hasKeyword = keyword.trim().length > 0;
+
+  const hasSearchResults = hasKeyword && products.length > 0;
+
+  /**
+   * 两种情况展示 You May Like:
+   *
+   * 1. 刚打开,还没有输入
+   * 2. 输入了,但是没有搜到产品
+   */
+  const showYouMayLike = !hasKeyword || (hasKeyword && products.length === 0);
+
+  // =========================
+  // 搜索接口
+  // =========================
   const triggerSearch = async (searchWord: string) => {
-    if (!searchWord.trim()) return;
+    const value = searchWord.trim();
+
+    if (!value) return;
+
     const params = {
       suggest: true,
-      query: searchWord,
+      query: value,
       first: 8,
     };
+
     console.log("【接口入参 variables】", params);
+
     const res = await searchProducts(params);
+
     console.log("【接口完整返回结果】", res);
+
     console.log("【商品列表数据】", res.data?.search);
   };
 
-  // 防抖输入
+  // =========================
+  // 输入 + 防抖
+  // =========================
   const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const value = e.target.value;
+
     setKeyword(value);
-    if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
-    debounceTimerRef.current = setTimeout(() => triggerSearch(value), 300);
+
+    if (debounceTimerRef.current) {
+      clearTimeout(debounceTimerRef.current);
+    }
+
+    if (!value.trim()) {
+      return;
+    }
+
+    debounceTimerRef.current = setTimeout(() => {
+      triggerSearch(value);
+    }, 300);
   };
 
-  // 点击搜索/回车搜索
+  // =========================
+  // 点击 Search / Enter
+  // =========================
   const handleSearch = () => {
-    if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
+    if (debounceTimerRef.current) {
+      clearTimeout(debounceTimerRef.current);
+    }
+
     triggerSearch(keyword);
   };
 
-  // 销毁清除定时器
+  // =========================
+  // 关闭
+  // =========================
+  const handleClose = () => {
+    if (debounceTimerRef.current) {
+      clearTimeout(debounceTimerRef.current);
+    }
+
+    setKeyword("");
+    onClose();
+  };
+
+  // =========================
+  // placeholder 轮播
+  // =========================
   useEffect(() => {
+    const interval = window.setInterval(() => {
+      setPlaceholderIndex((prev) => (prev + 1) % placeholderList.length);
+    }, 3000);
+
+    return () => window.clearInterval(interval);
+  }, []);
+
+  // =========================
+  // 打开自动 focus
+  // =========================
+  useEffect(() => {
+    if (!open) return;
+
+    const timer = window.setTimeout(() => {
+      inputRef.current?.focus();
+    }, 100);
+
+    return () => window.clearTimeout(timer);
+  }, [open]);
+
+  // =========================
+  // 打开时锁定页面滚动
+  // =========================
+  useEffect(() => {
+    if (!open) return;
+
+    const oldOverflow = document.body.style.overflow;
+
+    document.body.style.overflow = "hidden";
+
     return () => {
-      if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
+      document.body.style.overflow = oldOverflow;
     };
-  }, []);
+  }, [open]);
 
-  // 轮播占位文案
-  const placeholderList = [
-    "Search for products...",
-    "Find best sellers",
-    "Search new arrivals",
-    "Search discount items",
-    "Search trending products",
-  ];
-  const [placeholderIndex, setPlaceholderIndex] = useState(0);
+  // =========================
+  // Esc 关闭
+  // =========================
   useEffect(() => {
-    const interval = setInterval(() => {
-      setPlaceholderIndex((prev) => (prev + 1) % placeholderList.length);
-    }, 3000);
-    return () => clearInterval(interval);
-  }, [placeholderList.length]);
-  const currentPlaceholder = placeholderList[placeholderIndex];
+    if (!open) return;
 
-  // 关闭弹窗并清空关键词
-  const handleClose = () => {
-    setKeyword("");
-    onClose();
-  };
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === "Escape") {
+        handleClose();
+      }
+    };
+
+    window.addEventListener("keydown", handleKeyDown);
+
+    return () => {
+      window.removeEventListener("keydown", handleKeyDown);
+    };
+  }, [open]);
+  useEffect(() => {
+    return () => {
+      if (debounceTimerRef.current) {
+        clearTimeout(debounceTimerRef.current);
+      }
+    };
+  }, []);
 
-  // 弹窗未打开直接返回null
   if (!open) return null;
 
+  const currentPlaceholder = placeholderList[placeholderIndex];
+
   return (
-    // 全屏弹窗根容器,纯div无多余大括号
-    <div className="fixed inset-0 z-[9999] bg-white overflow-y-auto">
-      {/* 顶部搜索栏区域 */}
-      <div className="relative flex items-center justify-center px-4 py-4 border-b border-neutral-200 pt-10">
-        {/* 右上角关闭按钮 严格24px w-6 h-6 */}
-        <button
-          onClick={handleClose}
-          className="absolute right-4 top-4 w-6 h-6 flex items-center justify-center cursor-pointer"
-        >
-          <svg
-            xmlns="http://www.w3.org/2000/svg"
-            fill="none"
-            viewBox="0 0 24 24"
-            strokeWidth={1.8}
-            stroke="currentColor"
-            className="w-full h-full text-neutral-700"
-          >
-            <path
-              strokeLinecap="round"
-              strokeLinejoin="round"
-              d="M6 18L18 6M6 6l12 12"
-            />
-          </svg>
-        </button>
-      </div>
-      {/* 搜索输入框容器 */}
-      <div className="block w-full justify-center px-4 mt-3 z-10">
+    <div
+      className="
+        fixed
+        inset-x-0
+        bottom-0
+        top-[151px]
+        z-[9998]
+        bg-black/70
+      "
+      onMouseDown={handleClose}
+    >
+      <div
+        className="
+          mx-auto
+          w-[75%]
+          max-w-[1060px]
+          overflow-y-auto
+          bg-white
+          px-[90px]
+          pb-8
+          pt-6
+        "
+        onMouseDown={(e) => e.stopPropagation()}
+      >
         <div
-          className={`${isDesktop ? "max-w-[550px]" : ""} relative w-full mx-auto xl:min-w-[516px] outline-none hover:outline-none`}
+          className="
+            flex
+            h-[42px]
+            w-full
+            items-stretch
+            border
+            border-[#333333]
+          "
         >
-          <input
-            onChange={handleKeywordChange}
-            onKeyDown={(e) => e.key === "Enter" && handleSearch()}
-            autoComplete="off"
-            className="input w-full rounded-lg border border-neutral-200 bg-white py-2 pl-3 pr-10 text-sm text-black outline-none placeholder:text-neutral-500 focus:ring-2 focus:ring-neutral-300 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400 md:pl-4"
-            name="search"
-            placeholder={currentPlaceholder}
-            type="text"
-            value={keyword}
-          />
-          {/* 搜索图标按钮 */}
           <div
-            onClick={handleSearch}
-            className="absolute bottom-0 right-1 top-0 flex w-9 cursor-pointer items-center justify-center border-l border-neutral-200 dark:border-neutral-700 md:border-0"
+            className="
+              flex
+              min-w-0
+              flex-1
+              items-center
+              gap-3
+              px-4
+            "
           >
             <svg
               xmlns="http://www.w3.org/2000/svg"
-              fill="none"
+              width="18"
+              height="18"
               viewBox="0 0 24 24"
-              strokeWidth="1.5"
-              className="size-5 stroke-neutral-500 dark:stroke-white md:stroke-black"
+              fill="none"
             >
+              <circle
+                cx="11"
+                cy="11"
+                r="7"
+                stroke="#222222"
+                strokeWidth="1.5"
+              />
+
               <path
+                d="M16.5 16.5L21 21"
+                stroke="#222222"
+                strokeWidth="1.5"
                 strokeLinecap="round"
-                strokeLinejoin="round"
-                d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
               />
             </svg>
+
+            <input
+              ref={inputRef}
+              value={keyword}
+              type="text"
+              name="search"
+              autoComplete="off"
+              placeholder={currentPlaceholder}
+              onChange={handleKeywordChange}
+              onKeyDown={(e) => {
+                if (e.key === "Enter") {
+                  handleSearch();
+                }
+              }}
+              className="
+                h-full
+                min-w-0
+                flex-1
+                bg-transparent
+                text-[14px]
+                text-black
+                outline-none
+                placeholder:text-[#777777]
+              "
+            />
           </div>
+          <button
+            type="button"
+            onClick={handleSearch}
+            className="
+              m-[5px]
+              flex
+              w-[84px]
+              shrink-0
+              items-center
+              justify-center
+              rounded-[4px]
+              bg-[#036141]
+              text-[13px]
+              font-medium
+              text-white
+              transition-opacity
+              hover:opacity-90
+            "
+          >
+            Search
+          </button>
         </div>
-      </div>
-      {/* hot search */}
-      <HotSearch />
-      {/* 商品列表内容区 */}
-      <div >
-        {error && <p className="text-red-500 my-2 text-center">{error}</p>}
-        {data?.edges?.length === 0 && keyword && (
-          <p className="text-center text-gray-500 my-6">No matching products</p>
+        <div className="mt-6">
+          <HotSearch />
+        </div>
+        {error && (
+          <p
+            className="
+              my-4
+              text-center
+              text-sm
+              text-red-500
+            "
+          >
+            {error}
+          </p>
         )}
-        {keyword && data?.edges?.length !== 0 && (
-          <div>
-            <Grid className="grid grid-flow-row grid-cols-2 gap-5 lg:gap-11.5 w-full max-w-screen-2xl mx-auto md:grid-cols-3 lg:grid-cols-4 px-4 xss:px-7.5">
-              <ProductGridItems products={products} />
+        {hasSearchResults && (
+          <div className="">
+            <Grid className="grid grid-cols-4 gap-8 ">
+                <NewProductGridItems products={products} />
             </Grid>
 
-            <div className="flex justify-center mt-10 mb-16">
+            {/* See All Results */}
+            <div
+              className="
+                mb-2
+                mt-8
+                flex
+                justify-center
+              "
+            >
               <Link
                 href={`/search?q=${encodeURIComponent(keyword)}`}
                 onClick={handleClose}
-                className="inline-block border border-black rounded-full px-12 py-4 text-xl font-normal text-black hover:bg-gray-100 transition-colors"
+                className="
+                  inline-flex
+                  h-[30px]
+                  min-w-[140px]
+                  items-center
+                  justify-center
+                  rounded-full
+                  border
+                  border-[#333333]
+                  px-7
+                  text-[12px]
+                  text-[#222222]
+                  transition-colors
+                  hover:bg-[#F5F5F5]
+                "
               >
-                View More
+                See All Results
               </Link>
             </div>
           </div>
         )}
+
+        {showYouMayLike && (
+          <div className="">
+            {/* <h2
+              className="
+                mb-5
+                text-[18px]
+                font-semibold
+                text-[#111111]
+              "
+            >
+              You May Like
+            </h2>
+
+            {youMayLikeProducts.length > 0 ? (
+              <Grid
+                className="
+                  grid
+                  w-full
+                  grid-cols-4
+                  gap-x-4
+                  gap-y-7
+                "
+              >
+                <ProductGridItems products={youMayLikeProducts} />
+              </Grid>
+            ) :
+            null} */}
+            <p className="text-center font-medium text-base leading-[24px] mb-9">Sorry, we couldn't find any results</p>
+            <Recommend 
+              paddingClassName="py-0"
+              titleClassName="text-base font-semibold"
+            />
+          </div>
+        )}
       </div>
     </div>
   );

+ 6 - 6
src/components/layout/navbar/index.tsx

@@ -11,8 +11,9 @@ import {
 import { serverGraphqlFetch } from "@utils/bagisto/index";
 import NavbarSearchIcon from "./NavbarSearchIcon"
 import type { GetTreefrontMenusResult } from "@/types/theme/menus";
-import { authOptions } from "@utils/auth";
-import { getServerSession } from "next-auth";
+// import { authOptions } from "@utils/auth";
+// import { getServerSession } from "next-auth";
+import { auth } from "@/utils/auth/auth-helper";
 import TopSwiperBanner from "./TopSwiperBanner"
 import type { AnnouncementItem } from "./TopSwiperBanner";
 import { restApiFetch } from "@utils/bagisto/index";
@@ -39,8 +40,7 @@ export default async function Navbar() {
     console.error("解析 translation.options 失败", err);
     topBarData = [];
   }
- 
-    const session = await getServerSession(authOptions);// 游客是null
+    const session = await auth();// 游客是null
     const isGuest = session === null;
     const { data: menudata } = await serverGraphqlFetch<
       GetTreefrontMenusResult
@@ -54,8 +54,8 @@ export default async function Navbar() {
     <NavbarErrorBoundary>
       <header className="sticky top-0 z-10">
         <TopSwiperBanner items={topBarData} /> 
-        <nav className="relative flex flex-col items-center justify-between gap-4 bg-neutral-50 p-4 pt-3 pb-3 dark:bg-neutral-900 md:flex-row lg:px-6 lg:py-4">
-          <div className="flex w-full items-center justify-between gap-0 sm:gap-4">
+         <nav className="relative flex flex-col items-center justify-between gap-4 bg-neutral-50  pt-3 pb-3 dark:bg-neutral-900 md:flex-row ">
+          <div className="flex w-full items-center justify-between max-w-[1440px] m-auto gap-0 sm:gap-4">
             {/* 1. THE STATIC SHELL (Visible Instantly) */}
             <div className="flex items-center max-w-fit gap-5 xl:gap-6">
               {/* 2. STATIC HOLE: Categories (Suspended) */}

+ 112 - 0
src/components/theme/filters/SearchSortOrder.tsx

@@ -0,0 +1,112 @@
+"use client";
+import { type FC, useRef, useEffect, useCallback } from "react";
+import { useDisclosure } from "@heroui/use-disclosure";
+import { SORT, type SortOrderTypes } from "@/utils/constants";
+import { createUrl } from "@/utils/helper";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
+
+const ChevronDownIcon = ({ className = "" }: { className?: string }) => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="20"
+    height="20"
+    viewBox="0 0 24 24"
+    fill="none"
+    stroke="currentColor"
+    strokeWidth="2"
+    strokeLinecap="round"
+    strokeLinejoin="round"
+    className={className}
+  >
+    <path d="m6 9 6 6 6-6" />
+  </svg>
+);
+
+interface SearchSortOrderProps {
+  sortOrders: SortOrderTypes[];
+  title?: string;
+}
+const SearchSortOrder: FC<SearchSortOrderProps> = ({ sortOrders }) => {
+  const searchParams = useSearchParams();
+  const pathname = usePathname();
+  const router = useRouter();
+
+  // 从url拿sort参数
+  const sortValue = searchParams.get(SORT) || sortOrders[0]?.value;
+
+  const { isOpen, onOpen, onClose } = useDisclosure();
+  const toggleDropdown = useCallback(() => {
+    if (isOpen) {
+      onClose();
+    } else {
+      onOpen();
+    }
+  }, [isOpen, onOpen, onClose]);
+
+  const containerRef = useRef<HTMLDivElement>(null);
+  // 找到当前激活项
+  const activeOrder = sortOrders.find((item) => item.value === sortValue);
+  const displayLabel = activeOrder?.title || sortOrders[0]?.title;
+
+  // ✅ 新逻辑:修改URL query,替换原来onChange回调
+  const handleSortChange = (value: string) => {
+    const newParams = new URLSearchParams(searchParams.toString());
+    if (value) newParams.set(SORT, value);
+    const newUrl = createUrl(pathname, newParams);
+    router.replace(newUrl);
+    onClose(); // 选中关闭下拉
+  };
+
+  // 点击空白关闭
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
+        if (isOpen) onClose();
+      }
+    };
+    document.addEventListener("mousedown", handleClickOutside);
+    return () => document.removeEventListener("mousedown", handleClickOutside);
+  }, [isOpen, onClose]);
+
+  return (
+    <div className="flex flex-wrap mb-6 justify-end">
+      <div ref={containerRef} className="relative inline-block">
+        <button
+          type="button"
+          onClick={toggleDropdown}
+          className="inline-flex items-center justify-between w-full gap-1.5 text-base font-outfit tracking-wide bg-neutral-100 dark:bg-neutral-800 px-4 py-2 rounded-lg"
+        >
+          <span>{displayLabel}</span>
+          <ChevronDownIcon
+            className={`transition-transform duration-300 ease ${
+              isOpen ? "rotate-180" : "rotate-0"
+            }`}
+          />
+        </button>
+        {isOpen && (
+          <div className="absolute top-full left-0 mt-1 w-full bg-white dark:bg-neutral-800 rounded-lg shadow-lg z-30 ">
+            <div className="flex flex-col">
+              {sortOrders.map((order) => {
+                const isActive = sortValue === order.value;
+                return (
+                  <button
+                    key={order.value}
+                    onClick={() => handleSortChange(order.value)}
+                    className={`w-full  px-4 py-3 text-base transition-colors text-center ${
+                      isActive
+                        ? "bg-neutral-100 dark:bg-neutral-700 font-semibold"
+                        : "hover:bg-neutral-50 dark:hover:bg-neutral-700/50"
+                    }`}
+                  >
+                    {order.title}
+                  </button>
+                );
+              })}
+            </div>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+export default SearchSortOrder;