SearchPop.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. "use client";
  2. import { useState, useEffect, useRef } from "react";
  3. import { useSearchProduct } from "@/utils/hooks/useSearchProduct";
  4. import Grid from "@/components/theme/ui/grid/Grid";
  5. // import dynamicImport from "next/dynamic";
  6. import HotSearch from "@/app/(public)/search/_components/HotSearch"
  7. import { useMediaQuery } from "@utils/hooks/useMediaQueryHook";
  8. import Link from "next/link";
  9. // import { usePathname } from "next/navigation";
  10. // const ProductGridItems = dynamicImport(
  11. // () => import("@/components/catalog/product/ProductGridItems"),
  12. // );
  13. import NewProductGridItems from "@/components/catalog/product/NewProductGridItems";
  14. // 定义弹窗入参类型
  15. interface SearchPopProps {
  16. open: boolean;
  17. onClose: () => void;
  18. }
  19. export default function SearchPop({ open, onClose }: SearchPopProps) {
  20. const { data, error, searchProducts } = useSearchProduct();
  21. const [keyword, setKeyword] = useState("");
  22. const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
  23. const products = data?.edges?.map((e) => e.node) || [];
  24. const isDesktop = useMediaQuery("(min-width: 1024px)");
  25. // const pathname = usePathname();
  26. // useEffect(() => {
  27. // // 打开状态下,路由变化就关闭弹窗
  28. // if (open) {
  29. // handleClose();
  30. // }
  31. // }, [pathname]);
  32. // 执行搜索接口
  33. const triggerSearch = async (searchWord: string) => {
  34. if (!searchWord.trim()) return;
  35. const params = {
  36. suggest: true,
  37. query: searchWord,
  38. first: 8,
  39. };
  40. console.log("【接口入参 variables】", params);
  41. const res = await searchProducts(params);
  42. console.log("【接口完整返回结果】", res);
  43. console.log("【商品列表数据】", res.data?.search);
  44. };
  45. // 防抖输入
  46. const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  47. const value = e.target.value;
  48. setKeyword(value);
  49. if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
  50. debounceTimerRef.current = setTimeout(() => triggerSearch(value), 300);
  51. };
  52. // 点击搜索/回车搜索
  53. const handleSearch = () => {
  54. if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
  55. triggerSearch(keyword);
  56. };
  57. // 销毁清除定时器
  58. useEffect(() => {
  59. return () => {
  60. if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
  61. };
  62. }, []);
  63. // 轮播占位文案
  64. const placeholderList = [
  65. "Search for products...",
  66. "Find best sellers",
  67. "Search new arrivals",
  68. "Search discount items",
  69. "Search trending products",
  70. ];
  71. const [placeholderIndex, setPlaceholderIndex] = useState(0);
  72. useEffect(() => {
  73. const interval = setInterval(() => {
  74. setPlaceholderIndex((prev) => (prev + 1) % placeholderList.length);
  75. }, 3000);
  76. return () => clearInterval(interval);
  77. }, [placeholderList.length]);
  78. const currentPlaceholder = placeholderList[placeholderIndex];
  79. // 关闭弹窗并清空关键词
  80. const handleClose = () => {
  81. setKeyword("");
  82. onClose();
  83. };
  84. // 弹窗未打开直接返回null
  85. if (!open) return null;
  86. return (
  87. // 全屏弹窗根容器,纯div无多余大括号
  88. <div className="fixed inset-0 z-[9999] bg-white overflow-y-auto">
  89. {/* 顶部搜索栏区域 */}
  90. <div className="relative flex items-center justify-center px-4 py-4 border-b border-neutral-200 pt-10">
  91. {/* 右上角关闭按钮 严格24px w-6 h-6 */}
  92. <button
  93. onClick={handleClose}
  94. className="absolute right-4 top-4 w-6 h-6 flex items-center justify-center cursor-pointer"
  95. >
  96. <svg
  97. xmlns="http://www.w3.org/2000/svg"
  98. fill="none"
  99. viewBox="0 0 24 24"
  100. strokeWidth={1.8}
  101. stroke="currentColor"
  102. className="w-full h-full text-neutral-700"
  103. >
  104. <path
  105. strokeLinecap="round"
  106. strokeLinejoin="round"
  107. d="M6 18L18 6M6 6l12 12"
  108. />
  109. </svg>
  110. </button>
  111. </div>
  112. {/* 搜索输入框容器 */}
  113. <div className="block w-full justify-center md:hidden px-4 mt-3 z-10">
  114. <div
  115. className={`${isDesktop ? "max-w-[550px]" : ""} relative w-full mx-auto xl:min-w-[516px] outline-none hover:outline-none`}
  116. >
  117. <input
  118. onChange={handleKeywordChange}
  119. onKeyDown={(e) => e.key === "Enter" && handleSearch()}
  120. autoComplete="off"
  121. 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"
  122. name="search"
  123. placeholder={currentPlaceholder}
  124. type="text"
  125. value={keyword}
  126. />
  127. {/* 搜索图标按钮 */}
  128. <div
  129. onClick={handleSearch}
  130. 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"
  131. >
  132. <svg
  133. xmlns="http://www.w3.org/2000/svg"
  134. fill="none"
  135. viewBox="0 0 24 24"
  136. strokeWidth="1.5"
  137. className="size-5 stroke-neutral-500 dark:stroke-white md:stroke-black"
  138. >
  139. <path
  140. strokeLinecap="round"
  141. strokeLinejoin="round"
  142. 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"
  143. />
  144. </svg>
  145. </div>
  146. </div>
  147. </div>
  148. {/* hot search */}
  149. <HotSearch />
  150. {/* 商品列表内容区 */}
  151. <div >
  152. {error && <p className="text-red-500 my-2 text-center">{error}</p>}
  153. {data?.edges?.length === 0 && keyword && (
  154. <p className="text-center text-gray-500 my-6">No matching products</p>
  155. )}
  156. {keyword && data?.edges?.length !== 0 && (
  157. <div>
  158. <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">
  159. <NewProductGridItems products={products} />
  160. </Grid>
  161. <div className="flex justify-center mt-10 mb-16">
  162. <Link
  163. href={`/search?q=${encodeURIComponent(keyword)}`}
  164. onClick={handleClose}
  165. className="inline-block border border-black rounded-full px-12 py-4 text-xl font-normal text-black hover:bg-gray-100 transition-colors"
  166. >
  167. View More
  168. </Link>
  169. </div>
  170. </div>
  171. )}
  172. </div>
  173. </div>
  174. );
  175. }