Forráskód Böngészése

搜索结果页分页逻辑修改

fogwind 13 órája
szülő
commit
dd2add1b79

+ 124 - 0
src/app/(public)/search/_components/SearchPagination.tsx

@@ -0,0 +1,124 @@
+"use client";
+
+import { createUrl } from "@/utils/helper";
+import clsx from "clsx";
+import { usePathname, useSearchParams, useRouter } from "next/navigation";
+
+
+export default function SearchPagination({
+    itemsPerPage,
+    itemsTotal,
+    currentPage
+}: {
+    itemsPerPage: number;
+    itemsTotal: number;
+    currentPage: number;
+}) {
+    const router = useRouter();
+    const pathname = usePathname();
+    const currentParams = useSearchParams();
+
+    const pageCount = Math.ceil(itemsTotal / itemsPerPage);
+
+    const handlePageClick = (page: number) => {
+        if (page < 0 || page > pageCount || page === currentPage) return;
+
+        const params = new URLSearchParams(currentParams.toString());
+
+        params.set("page", String(page));
+
+
+        const newUrl = createUrl(pathname, params);
+        router.replace(newUrl);
+    };
+
+
+    const renderDots = () => {
+        const dots = [];
+        const maxVisiblePages = 5;
+
+        if (pageCount <= maxVisiblePages + 1) {
+            for (let i = 1; i <= pageCount; i++) {
+                dots.push(renderPageButton(i));
+            }
+        } else {
+            dots.push(renderPageButton(1));
+            if(currentPage <= 3) {
+                for(let i = 2; i <= 5; i++) {
+                    dots.push(renderPageButton(i));
+                }
+                dots.push(<li key="dot-start" className="text-ly-12">...</li>);
+            } else if(currentPage >=4) {
+                const start = currentPage - 2 >= pageCount - 4 ?  pageCount - 4 : currentPage - 2;
+                const end = currentPage + 2 >= pageCount ? pageCount - 1 : currentPage + 2;
+                if(start > 2 ) {
+                    dots.push(<li key="dot-end" className="text-ly-12">...</li>);
+                }
+              
+                for(let i = start; i <= end; i++) {
+                    dots.push(renderPageButton(i));
+                }
+
+                if(currentPage + 2 <= pageCount - 2) {
+                    dots.push(<li key="dot-end" className="text-ly-12">...</li>);
+                }
+            }
+            dots.push(renderPageButton(pageCount));
+            
+        }
+
+        return dots;
+    };
+
+    const renderPageButton = (pageIndex: number) => (
+        <li key={pageIndex} className="rounded-sm cursor-pointer">
+            <button onClick={() => handlePageClick(pageIndex)}
+                className={clsx(
+                    'flex h-8 w-8 items-center justify-center text-ly-14 rounded-sm duration-300 cursor-pointer',
+                    pageIndex === currentPage && 'border border-ly-middlegreen text-ly-middlegreen'
+                )}
+                aria-label={`Goto Page ${pageIndex}`}
+                aria-current={pageIndex === currentPage}
+            >
+                {pageIndex}
+            </button>
+        </li>
+    );
+
+    return (
+        <ul className="box-border px-4 gap-x-1.5 flex justify-center items-center"
+            role="navigation"
+            aria-label="Pagination"
+        >
+            <li key="prev"
+                className={clsx("flex items-center rounded-sm",currentPage === 1 ? "opacity-30" : "")}
+            >
+                <button onClick={() => handlePageClick(currentPage - 1)}
+                    className="w-6 h-6"
+                    aria-label="Previous page"
+                    disabled={currentPage === 1}
+                >
+                    <svg className="block w-full h-full" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M14.5 18L8.5 12L14.5 6"></path></svg>
+                </button>
+            </li>
+
+            {renderDots()}
+
+            <li
+                key="next"
+                className={clsx(
+                    "flex items-center rounded-sm",
+                    currentPage === pageCount ? "opacity-30" : ""
+                )}
+            >
+                <button onClick={() => handlePageClick(currentPage + 1)}
+                    className="w-6 h-6"
+                    aria-label="Next page"
+                    disabled={currentPage === pageCount}
+                >
+                    <svg className="block w-full h-full" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M9 18L15 12L9 6"></path></svg>
+                </button>
+            </li>
+        </ul>
+    );
+}

+ 27 - 112
src/app/(public)/search/page.tsx

@@ -1,31 +1,24 @@
 import dynamicImport from "next/dynamic";
 import Grid from "@/components/theme/ui/grid/Grid";
-// import NotFound from "@/components/theme/search/not-found";
 import { isArray } from "@/utils/type-guards";
 import { serverGraphqlFetch } from "@utils/bagisto/index";
 import {
   SEARCH_PRODUCTS,
 } from "@/graphql";
-
-import { generateMetadataForPage, buildProductFilters } from "@/utils/helper";
 import SortOrder from "@/components/theme/filters/SortOrder";
 import { SortByFields } from "@/utils/constants";
-// import MobileFilter from "@/components/theme/filters/MobileFilter";
-// import FilterList from "@/components/theme/filters/FilterList";
 import type {
   SearchProductsResponse,
   SearchProductsVariables,
 } from "@/components/catalog/type";
 import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
 import HotSearch from "./_components/HotSearch";
-const Pagination = dynamicImport(
-  () => import("@/components/catalog/Pagination"),
-);
-// const ProductGridItems = dynamicImport(
-//   () => import("@/components/catalog/product/ProductGridItems"),
-// );
 import NewProductGridItems from "@/components/catalog/product/NewProductGridItems";
-export const dynamicParams = true;
+const SearchPagination = dynamicImport(
+  () => import("./_components/SearchPagination"),
+);
+
+
 
 // SEO+分享
 export async function generateMetadata({
@@ -34,15 +27,14 @@ export async function generateMetadata({
   searchParams?: Promise<{ q: string }>;
 }) {
   const params = await searchParams;
-  const searchQuery = params?.q as string | undefined;
+  const searchQuery = params?.q;
 
-  return generateMetadataForPage("search", {
+  return {
     title: searchQuery ? `Search: ${searchQuery}` : "Search Products",
     description: searchQuery
       ? `Search results for "${searchQuery}"`
       : "Search for products in our store",
-    image: "/search-og.jpg",
-  });
+  };
 }
 
 export default async function SearchPage({
@@ -52,71 +44,20 @@ export default async function SearchPage({
     q: string;
     page: string;
     sort: string;
-    cursor: string;
-    before: string;
    }>;
 }) {
   const params = await searchParams;
   const {
     q: searchValue,
     page,
-    cursor,
-    before,
   } = (params || {});
   const itemsPerPage = 8;
-  const currentPage = page ? parseInt(page) - 1 : 0;
+  const currentPage = page ? parseInt(page): 1;
   const sortValue = params?.sort || "name-asc";
-  const selectedSort =
-    SortByFields.find((s) => s.key === sortValue) || SortByFields[0];
-  const afterCursor: string | undefined = cursor;
-  const beforeCursor: string | undefined = before;
-
-  const { filterInput, isFilterApplied } = buildProductFilters(params || {});
-
-  let dataPromise;
-  if (isFilterApplied) {
-    dataPromise = await serverGraphqlFetch<
-      SearchProductsResponse,
-      SearchProductsVariables
-    >({
-      query: SEARCH_PRODUCTS,
-      variables: {
-        suggest: true,
-        query: searchValue || '',
-        filter: filterInput,
-        first: 8,
-        ...(beforeCursor
-          ? { last: itemsPerPage, before: beforeCursor }
-          : { first: itemsPerPage, after: afterCursor }),
-        sortKey: selectedSort.sortKey,
-        reverse: selectedSort.reverse,
-      },
-    });
+  const selectedSort = SortByFields.find((s) => s.key === sortValue) || SortByFields[0];
 
-  } else {
-    dataPromise = (async () => {
-      let currentAfterCursor: string | undefined = afterCursor;
-      //  console.log("currentAfterCursoraaaaa00:",currentAfterCursor);
-      if (currentPage > 0 && !afterCursor) {
-        // 兜底处理 如果用户直接通过url访问且没有在url上带cursor=  会调接口获取cursor值  
-        // 正常走底部分页按钮不会触发该接口调用;
-        const { data: cursorData } = await serverGraphqlFetch<
-          SearchProductsResponse,
-          SearchProductsVariables
-        >({
-          query: SEARCH_PRODUCTS,
-          variables: {
-            suggest: true,
-            query: searchValue || '',
-            first: currentPage * itemsPerPage,
-            sortKey: selectedSort.sortKey,
-            reverse: selectedSort.reverse,
-          },
-        });
 
-        currentAfterCursor = cursorData?.search?.pageInfo?.endCursor;
-        // console.log("currentAfterCursoraaaaa:",currentAfterCursor);
-      }
+  const dataPromise = (async () => {
       return serverGraphqlFetch<
         SearchProductsResponse,
         SearchProductsVariables
@@ -125,28 +66,23 @@ export default async function SearchPage({
         variables: {
           suggest: true,
           query: searchValue || '',
-          ...(beforeCursor
-            ? { last: itemsPerPage, before: beforeCursor }
-            : { first: itemsPerPage, after: currentAfterCursor }),
+          first: itemsPerPage,
+          page: currentPage,
           sortKey: selectedSort.sortKey,
           reverse: selectedSort.reverse,
         },
       });
 
-    })();
-  }
+  })();
+  
+
 
-  // const [{ data }, filterAttributes] = await Promise.all([
-  //   dataPromise,
-  //   getFilterAttributes(),
-  // ]);
   const { data } = await dataPromise;
-  console.log("sssssssssssssssssssssssssss:", data);
 
   const products = data?.search?.edges?.map((e) => e.node) || [];
-  const pageInfo = data?.search?.pageInfo;
+//   const pageInfo = data?.search?.pageInfo;
   const totalCount = data?.search?.totalCount || 0;
-  console.log("productssssssssssssssssssss---------------------",products);
+
   
   return (
     <>
@@ -154,7 +90,6 @@ export default async function SearchPage({
       <HotSearch />
       {isArray(products) ? (
         <div className="flex items-center justify-between gap-4 pt-0 pb-6 md:hidden  mx-auto w-full max-w-screen-2xl px-4 xss:px-7.5">
-          {/* <MobileFilter filterAttributes={filterAttributes} /> */}
           <div>{totalCount||0} items</div>
           <SortOrder sortOrders={SortByFields} title="Sort by" />
         </div>
@@ -173,35 +108,15 @@ export default async function SearchPage({
         </Grid>
       ) : null}
 
-      {!isFilterApplied && isArray(products) && totalCount > itemsPerPage && (
-        <nav
-          aria-label="Collection pagination"
-          className="my-10 block items-center sm:flex"
-        >
-          <Pagination
-            itemsPerPage={itemsPerPage}
-            itemsTotal={totalCount || 0}
-            currentPage={currentPage}
-            nextCursor={pageInfo?.endCursor}
-            prevCursor={pageInfo?.startCursor}
-          />
-        </nav>
-      )}
-
-      {isFilterApplied && isArray(products) && pageInfo?.hasNextPage && (
-        <nav
-          aria-label="Filtered pagination"
-          className="my-10 block items-center sm:flex"
-        >
-          <Pagination
-            itemsPerPage={itemsPerPage}
-            itemsTotal={totalCount || 0}
-            currentPage={currentPage}
-            nextCursor={pageInfo?.endCursor}
-            prevCursor={pageInfo?.startCursor}
-          />
-        </nav>
-      )}
+        {isArray(products) && totalCount > itemsPerPage && (
+            <nav className="mt-4 mb-8">
+                <SearchPagination
+                    itemsPerPage={itemsPerPage}
+                    itemsTotal={totalCount || 0}
+                    currentPage={currentPage}
+                />
+            </nav>
+        )}
     </>
   );
 }

+ 0 - 159
src/components/catalog/Pagination.tsx

@@ -1,159 +0,0 @@
-"use client";
-
-import { createUrl } from "@/utils/helper";
-import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
-import clsx from "clsx";
-import { usePathname, useSearchParams, useRouter } from "next/navigation";
-
-export default function Pagination({
-  itemsPerPage,
-  itemsTotal,
-  currentPage,
-  nextCursor,
-  prevCursor,
-}: {
-  itemsPerPage: number;
-  itemsTotal: number;
-  currentPage: number;
-  nextCursor?: string;
-  prevCursor?: string;
-}) {
-  const router = useRouter();
-  const pathname = usePathname();
-  const currentParams = useSearchParams();
-
-  const pageCount = Math.ceil(itemsTotal / itemsPerPage);
-
-  const handlePageClick = (page: number) => {
-    if (page < 0 || page >= pageCount) return;
-
-    const params = new URLSearchParams(currentParams.toString());
-
-    params.set("page", String(page + 1));
-
-    if (page === currentPage + 1 && nextCursor) {
-      params.set("cursor", nextCursor);
-      params.delete("before");
-    } else if (page === currentPage - 1 && prevCursor) {
-      params.set("before", prevCursor);
-      params.delete("cursor");
-    } else {
-      params.delete("cursor");
-      params.delete("before");
-    }
-
-    const newUrl = createUrl(pathname, params);
-    router.replace(newUrl);
-  };
-
-  const renderDots = () => {
-    const dots = [];
-    const maxVisiblePages = 5;
-
-    if (pageCount <= maxVisiblePages) {
-      for (let i = 0; i < pageCount; i++) {
-        dots.push(renderPageButton(i));
-      }
-    } else {
-      const halfMax = Math.floor(maxVisiblePages / 2);
-      const start = Math.max(0, currentPage - halfMax);
-      const end = Math.min(pageCount, start + maxVisiblePages);
-
-      if (start > 0) {
-        dots.push(renderPageButton(0));
-        if (start > 1) {
-          dots.push(
-            <li key="dot-start" className="pagination-dot">...</li>
-          );
-        }
-      }
-
-      for (let i = start; i < end; i++) {
-        dots.push(renderPageButton(i));
-      }
-
-      if (end < pageCount) {
-        if (end < pageCount - 1) {
-          dots.push(
-            <li key="dot-end" className="pagination-dot">...</li>
-          );
-        }
-        dots.push(renderPageButton(pageCount - 1));
-      }
-    }
-
-    return dots;
-  };
-
-  const renderPageButton = (pageIndex: number) => (
-    <li
-      key={pageIndex}
-      onClick={() => handlePageClick(pageIndex)}
-      className="rounded-sm cursor-pointer"
-    >
-      <button
-        className={`
-          flex h-10 w-10 items-center justify-center text-lg rounded-sm duration-300 cursor-pointer
-          ${pageIndex === currentPage
-            ? "border !border-gray-300 dark:border-gray-700"
-            : "text-gray-500 dark:!text-gray-400 hover:border-gray-500 dark:hover:border-gray-700"
-          }
-        `}
-        aria-label={`Goto Page ${pageIndex + 1}`}
-        aria-current={pageIndex === currentPage}
-      >
-        {pageIndex + 1}
-      </button>
-    </li>
-  );
-
-  return (
-    <ul
-      className="mx-auto h-10 gap-x-2 text-base flex justify-center"
-      role="navigation"
-      aria-label="Pagination"
-    >
-      <li
-        key="prev"
-        onClick={() => handlePageClick(currentPage - 1)}
-        className={clsx(
-          "cursor-pointer rounded-lg hover:text-gray-700",
-          currentPage > 0 ? "block cursor-pointer" : ""
-        )}
-      >
-        <button
-          className={clsx(
-            "ml-0 flex h-10 items-center justify-center px-2 leading-tight",
-            currentPage > 0 ? "cursor-pointer" : "cursor-not-allowed"
-          )}
-          aria-label="Previous page"
-          disabled={currentPage <= 0}
-        >
-          <ChevronLeftIcon className="h-5" />
-        </button>
-      </li>
-
-      {renderDots()}
-
-      <li
-        key="next"
-        onClick={() => handlePageClick(currentPage + 1)}
-        className={clsx(
-          "rounded-lg hover:text-gray-700",
-          currentPage < pageCount - 1 ? "block cursor-pointer" : ""
-        )}
-      >
-        <button
-          className={clsx(
-            "ml-0 flex h-10 items-center justify-center px-2 leading-tight",
-            currentPage < pageCount - 1 ? "cursor-pointer" : "cursor-not-allowed"
-          )}
-          aria-label="Next page"
-          disabled={currentPage >= pageCount - 1}
-        >
-          <ChevronRightIcon className="h-5" />
-        </button>
-      </li>
-    </ul>
-  );
-}

+ 3 - 0
src/components/catalog/type.ts

@@ -233,6 +233,7 @@ export interface SearchProductsVariables {
     filter?: string;
     sortKey?: string;
     reverse?: boolean;
+    page?: number;
     first?: number;
     last?: number;
     after?: string;
@@ -248,6 +249,8 @@ export interface SearchProductsResponse {
       startCursor: string;
       hasNextPage: boolean;
       hasPreviousPage: boolean;
+      currentPage: number;
+      totalPages: number;
     };
     totalCount: number;
 

+ 9 - 0
src/graphql/catalog/queries/SearchProducts.ts

@@ -2,6 +2,11 @@ import { gql,type TypedDocumentNode } from "@apollo/client";
 import { PRODUCT_CORE_FRAGMENT } from "../fragments";
 import type { SearchProductsResponse,SearchProductsVariables } from "@/components/catalog/type";
 
+/**
+ * SearchProducts 支持两种分页,一种是游标分页;一种是页码分页
+ * 游标分页及 first/after,last/before 组合。游标分页适合无限滚动效果,不能实现跳页翻页;
+ * 页面分页是 first/page组合,其中first是每页个数,page是页码
+ */
 export const SEARCH_PRODUCTS: TypedDocumentNode<SearchProductsResponse,SearchProductsVariables> = gql`
   ${PRODUCT_CORE_FRAGMENT}
 
@@ -10,6 +15,7 @@ export const SEARCH_PRODUCTS: TypedDocumentNode<SearchProductsResponse,SearchPro
     $suggest: Boolean
     $sortKey: String
     $reverse: Boolean
+    $page: Int
     $first: Int
     $last: Int
     $after: String
@@ -24,6 +30,7 @@ export const SEARCH_PRODUCTS: TypedDocumentNode<SearchProductsResponse,SearchPro
       filter: $filter
       sortKey: $sortKey
       reverse: $reverse
+      page: $page
       first: $first
       last: $last
       after: $after
@@ -42,6 +49,8 @@ export const SEARCH_PRODUCTS: TypedDocumentNode<SearchProductsResponse,SearchPro
             hasPreviousPage
             startCursor
             endCursor
+            currentPage
+            totalPages
         }
         searchMetadata {
             originalQuery

+ 0 - 63
src/utils/helper.ts

@@ -250,69 +250,6 @@ export function safeParse<T = any>(value: string | null | undefined): T | null {
  * @param params - URL search parameters
  * @returns Object containing filterInput string and isFilterApplied boolean
  */
-export function buildProductFilters(params: {
-  [key: string]: string | string[] | undefined;
-}) {
-  const rawColor = params?.color;
-  const rawSize = params?.size;
-  const rawBrand = params?.brand;
-
-  const colorFilter =
-    typeof rawColor === "string"
-      ? rawColor.split(",")
-      : Array.isArray(rawColor)
-        ? rawColor
-        : [];
-
-  const sizeFilter =
-    typeof rawSize === "string"
-      ? rawSize.split(",")
-      : Array.isArray(rawSize)
-        ? rawSize
-        : [];
-
-  const brandFilter =
-    typeof rawBrand === "string"
-      ? rawBrand.split(",")
-      : Array.isArray(rawBrand)
-        ? rawBrand
-        : [];
-
-  const extractId = (value: string) => {
-    if (/^\d+$/.test(value)) return value;
-    const match = value.match(/\/(\d+)$/);
-    return match ? match[1] : null;
-  };
-
-  const colorIds = colorFilter
-    .map(extractId)
-    .filter((id): id is string => Boolean(id));
-
-  const sizeIds = sizeFilter
-    .map(extractId)
-    .filter((id): id is string => Boolean(id));
-
-  const brandIds = brandFilter
-    .map(extractId)
-    .filter((id): id is string => Boolean(id));
-
-  const filterObject: Record<string, string> = {};
-
-  if (colorIds.length > 0) filterObject.color = colorIds.join(",");
-  if (sizeIds.length > 0) filterObject.size = sizeIds.join(",");
-  if (brandIds.length > 0) filterObject.brand = brandIds.join(",");
-
-  const isFilterApplied = Object.keys(filterObject).length > 0;
-  const filterInput = isFilterApplied
-    ? JSON.stringify(filterObject)
-    : undefined;
-
-  return {
-    filterObject,
-    filterInput,
-    isFilterApplied,
-  };
-}
 export function newBuildProductFilters(params: {
   [key: string]: string | string[] | undefined;
 }) {