Explorar o código

分类产品页部分调整

zhangzf hai 3 días
pai
achega
2578890bd8

+ 4 - 4
src/app/(public)/category/[slug]/[id]/_components/CategoryDesc.tsx

@@ -41,18 +41,18 @@ const CategoryDesc: FC<CategoryDescProps> = ({ title, description }) => {
   );
 
   return (
-    <div className="px-4">
-      <h2 className="text-ly-18 leading-ly-24 font-medium mt-8 mb-4">
+    <div className="">
+      <h2 className="text-ly-24 leading-ly-26 font-medium mt-8 mb-4">
         {title}
       </h2>
 
       <p
         ref={textRef}
-        className={`overflow-hidden text-ly-12 leading-ly-20 font-normal transition-[max-height] text-[#666] duration-300 ease-in-out ${
+        className={`overflow-hidden text-ly-18 leading-ly-26 font-normal transition-[max-height] text-[#666] duration-300 ease-in-out ${
           isExpand ? "" : "line-clamp-2"
         }`}
       >
-        {description}
+        <div dangerouslySetInnerHTML={{ __html: description }} />
       </p>
 
       {showMore && (

+ 41 - 37
src/app/(public)/category/[slug]/[id]/_components/ProductListing.tsx

@@ -13,6 +13,7 @@ import { useRouter, usePathname } from "next/navigation";
 import {  useApolloClient } from "@apollo/client/react";
 import type{ CategoryProductsResult,PageInfo,CategoryProductNode,CategoryAttrFilterItem } from "@components/catalog/type";
 import {CATEGORY_PRODUCTS} from "@/graphql";
+import {transformFiltersForApi} from "@utils/helper";
 interface QueryState {
   // search: string;
 
@@ -65,6 +66,8 @@ export default function ProductListing({
 
   const fetchProducts = async (currentQuery: QueryState) => {
     try {
+        // 转换
+    const finalFilterObj = transformFiltersForApi(currentQuery.filters);
       const res = await apolloClient.query<CategoryProductsResult>({
         query: CATEGORY_PRODUCTS,
         variables: 
@@ -80,14 +83,14 @@ export default function ProductListing({
         
           sortKey: currentQuery.sortKey,
           reverse: currentQuery.reverse,
-          filter:JSON.stringify( currentQuery.filters),
+          filter:JSON.stringify( finalFilterObj),
 
           first: 15,
 
           after: null,
         },
       });
-      console.log("res----------------------------------:",res,currentQuery);
+      console.log("res----------------------------------:",res,currentQuery,finalFilterObj);
       
       // const res = await fetch("/api/products", {
       //   method: "POST",
@@ -217,8 +220,8 @@ export default function ProductListing({
       }
   };
   return (
-    <>
-      <div className="flex items-center justify-between gap-4 py-8 md:hidden w-full max-w-screen-2xl mx-auto px-4">
+    <> 
+      <div className="flex  justify-between  py-8  w-full mx-auto ">
         <NewMobileFilter
           filterAttributes={filterAttributes}
           filters={query.filters}
@@ -232,40 +235,41 @@ export default function ProductListing({
             });
           }}
         />
-        <NewSortOrder
-          sortOrders={newSortByFields}
-          title="Sort by"
-          value={query.sortValue}
-          onChange={(sortValue,sortKey,reverse) => {
-            updateQuery({
-              ...query,
-              sortValue,
-              sortKey,
-              reverse:reverse,
-              cursor: undefined,
-            });
-          }}
-        />
-      </div>
-
-      {isArray(products) && products.length > 0 ? (
-        <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} />
-        </Grid>
-      ) : (
-        <div className="px-4">
-          <div className="flex h-40 items-center justify-center rounded-lg border border-dashed border-neutral-300">
-            <p className="text-neutral-500">
-              No products found in this category.
-            </p>
-          </div>
+        <div>
+              <NewSortOrder
+                sortOrders={newSortByFields}
+                title="Sort by"
+                value={query.sortValue}
+                onChange={(sortValue,sortKey,reverse) => {
+                  updateQuery({
+                    ...query,
+                    sortValue,
+                    sortKey,
+                    reverse:reverse,
+                    cursor: undefined,
+                  });
+                }}
+              />
+              {isArray(products) && products.length > 0 ? (
+                 <Grid className="grid grid-cols-4 gap-8">
+                   <ProductGridItems products={products} />
+                 </Grid>
+               ) : (
+                 <div className="px-4">
+                   <div className="flex h-40 items-center justify-center rounded-lg border border-dashed border-neutral-300">
+                     <p className="text-neutral-500">
+                       No products found in this category.
+                     </p>
+                   </div>
+                 </div>
+               )}
+               {pageInfo?.hasNextPage && (
+                 <div className="flex justify-center my-10">
+                   <button onClick={loadMore}>View More</button>
+                 </div>
+              )}
         </div>
-      )}
-      {pageInfo?.hasNextPage && (
-        <div className="flex justify-center my-10">
-          <button onClick={loadMore}>View More</button>
-        </div>
-      )}
+      </div>
     </>
   );
 }

+ 16 - 5
src/app/(public)/category/[slug]/[id]/page.tsx

@@ -4,6 +4,7 @@ import { notFound } from "next/navigation";
 // import FilterList from "@components/theme/filters/FilterList";
 // import Pagination from "@components/catalog/Pagination";
 import ProductListing from "./_components/ProductListing";
+import Image from "next/image";
 import type{
   // ProductsResponse,
   GetCategoryAttrFiltersResult,
@@ -33,6 +34,7 @@ import {
   // extractNumericId,
   // findCategoryBySlug,
   newBuildProductFilters,
+  transformFiltersForApi,
 } from "@utils/helper";
 import { serverGraphqlFetch } from "@/utils/bagisto/index";
 import type {
@@ -158,7 +160,7 @@ export default async function CategoryPage({
   const cursor = getSearchParam(resolvedParams?.cursor);
 
   const before = getSearchParam(resolvedParams?.before);
-  const itemsPerPage = 15;
+  const itemsPerPage = 8;
   // const currentPage = page ? parseInt(page) - 1 : 0;
   const sortValue = getSearchParam(resolvedParams?.sort) ?? "name-asc";
   const selectedSort =
@@ -175,8 +177,8 @@ export default async function CategoryPage({
   if (numericId) {
     filterObject.category_id = numericId;
   }
-
-  const filterInput = JSON.stringify(filterObject);
+  const finalFilterObj =transformFiltersForApi(filterObject);
+  const filterInput = JSON.stringify(finalFilterObj);
   // 默认获取产品数据接口:
   const { data: categoryProductDatas } = await serverGraphqlFetch<
     CategoryProductsResult,
@@ -298,7 +300,7 @@ export default async function CategoryPage({
   return (
     <>
       {/* <MobileSearchBar /> */}
-      <section>
+      <section className="px-12">
         <Suspense fallback={<FilterListSkeleton />}>
           {/* <CategoryDetail
             categoryItem={{
@@ -306,10 +308,19 @@ export default async function CategoryPage({
               name: translation?.name ?? "",
             }}
           /> */}
+          <div className="felx justify-center">
+              <Image
+               src={categoryProductDatas? categoryProductDatas.categoryProducts.banner: ''}
+               alt={'categoryBanner'}
+               width={1824}
+               height={608}
+               
+              />
+          </div>
           <CategoryDesc
             title={translation?.name ?? ""}
             description={
-              "Alipearl Hair provides many style best quality but cheap lace wigs,including lace front wigs, lace closure wigs, full lace human hair wigs, HD transparent lace wigs, 13x4 frontal wigs, 13x6 lace front wigs, 360 lace wigs, all textures available straight, body wave, deep wave, water wave, curly hair wigs, all lengths from 8 inch to 40 inch, different hair colors natural black, brown, blonde, highlight, ginger, burgundy etc."
+              categoryProductDatas? categoryProductDatas.categoryProducts.description: ''
             } //translation?.description ?? ""}
           />
         </Suspense>

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

@@ -176,6 +176,7 @@ export interface CategoryAttrFilterItem  {
         adminName: string;
         sortOrder: number;
         swatchValue: string | null;
+        productCount:number;
       };
     }>;
   };
@@ -210,6 +211,8 @@ export type CategoryProductNode = {
 export type CategoryProductsResult = {
   categoryProducts: {
     totalCount: number;
+    description: string;
+    banner: string;
     pageInfo:{
       endCursor:string,
       hasNextPage: boolean

+ 90 - 165
src/components/theme/filters/NewMobileFilter.tsx

@@ -1,27 +1,11 @@
 "use client";
-
-import {
-  Drawer,
-  DrawerContent,
-  DrawerHeader,
-  DrawerBody,
-} from "@heroui/drawer";
-
 import { Button } from "@heroui/button";
-
-import { useDisclosure } from "@heroui/use-disclosure";
-import FilterPrice from "./FilterPrice";
-import {
-  AdjustmentsHorizontalIcon,
-  XMarkIcon,
-} from "@heroicons/react/24/outline";
 import { useEffect, useState, useRef } from "react";
+import FilterPrice from "./FilterPrice";
 
 interface MobileFilterProps {
   filterAttributes: any[];
-
   filters: Record<string, string>;
-
   onChange: (filters: Record<string, string>) => void;
 }
 export default function MobileFilter({
@@ -29,67 +13,38 @@ export default function MobileFilter({
   filters,
   onChange,
 }: MobileFilterProps) {
-  const { isOpen, onOpen, onOpenChange } = useDisclosure();
-  // 记录上一次抽屉状态,用来捕获【关闭→打开】瞬间
-  const prevIsOpenRef = useRef(isOpen);
-
   /**
-   * 弹窗里面临时选择
+   * 临时筛选状态
    */
   const [tempFilters, setTempFilters] = useState<Record<string, string>>({});
-
   const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>(
     {},
   );
 
-  /**
-   * 打开弹窗同步父组件状态
-   */
-  useEffect(() => {
-    // 仅从关闭切换到打开时执行
-    if (!prevIsOpenRef.current && isOpen) {
-      console.log("dadadadadadadadisopen");
-      // 使用 queueMicrotask 把状态更新延后,避开同步setState警告
-      queueMicrotask(() => {
-        // 浅拷贝,断开引用
-        setTempFilters({ ...filters });
-        console.log("tempFilters---------------------", tempFilters);
-
-        const expand: Record<string, boolean> = {};
-        filterAttributes.forEach((attr) => {
-          expand[attr.code] = true;
-        });
-        setExpandedGroups(expand);
-      });
-    }
-    prevIsOpenRef.current = isOpen;
-
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [isOpen]);
+  // 父组件filters更新时同步到本地临时筛选
   useEffect(() => {
-    console.log("【父组件传入筛选filters】", filters);
-    console.log("【抽屉是否打开isOpen】", isOpen);
-  }, [isOpen, filters]);
+    setTempFilters({ ...filters });
+    const expand: Record<string, boolean> = {};
+    filterAttributes.forEach((attr) => {
+      expand[attr.code] = true;
+    });
+    setExpandedGroups(expand);
+  }, [filters, filterAttributes]);
 
   /**
-   * 单选
+   * 单选切换
    */
   const toggleOption = (code: string, id: string) => {
     setTempFilters((prev) => {
       const current = prev[code];
-
       if (current === id) {
         const next = { ...prev };
-
         delete next[code];
-        // 打印点击后最新的临时筛选
         console.log("【点击选项后临时筛选tempFilters】", next);
         return next;
       }
-
       return {
         ...prev,
-
         [code]: id,
       };
     });
@@ -98,7 +53,6 @@ export default function MobileFilter({
   const toggleGroupExpand = (code: string) => {
     setExpandedGroups((prev) => ({
       ...prev,
-
       [code]: !prev[code],
     }));
   };
@@ -108,130 +62,101 @@ export default function MobileFilter({
    */
   const applyFilters = () => {
     onChange(tempFilters);
-
-    onOpenChange();
   };
 
   /**
-   * 清空
+   * 清空筛选
    */
   const clearAll = () => {
     setTempFilters({});
-
     onChange({});
   };
 
   const formatLabel = (str?: string) => {
     return str ? str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() : "";
   };
-  return (
-    <>
-      <div className="flex flex-wrap gap-3">
-        <Button size="md" className="flex bg-neutral-100" onPress={onOpen}>
-          <AdjustmentsHorizontalIcon className="h-6 w-8" />
 
-          <span>Filter</span>
-        </Button>
+  return (
+    <div className=" rounded-lg  w-full max-w-[300px]">
+      {/* Header */}
+      <div className="flex justify-between items-center pb-4 border-b">
+        <h2 className="text-xl font-semibold">Filters:</h2>
+        <button onClick={clearAll} className="text-sm text-primary">Clear All</button>
       </div>
 
-      <Drawer
-        isOpen={isOpen}
-        placement="right"
-        onOpenChange={onOpenChange}
-        hideCloseButton
-      >
-        <DrawerContent className="h-full w-full max-w-full !rounded-none">
-          <>
-            <DrawerHeader className="flex justify-between px-6 py-4 border-b">
-              <h2 className="text-xl font-semibold">Filters:</h2>
-
-              <div className="flex gap-6">
-                <button onClick={clearAll}>Clear All</button>
-
-                <button onClick={() => onOpenChange()}>
-                  <XMarkIcon className="w-6 h-6" />
-                </button>
-              </div>
-            </DrawerHeader>
-
-            <DrawerBody className="px-6 overflow-y-auto">
-              {filterAttributes.map((attr) => {
-                const expand = expandedGroups[attr.node.code] ?? true;
-
-                return (
-                  <div key={attr.node._id} className="py-4 border-b">
-                    <button
-                      className="flex justify-between w-full text-xl"
-                      onClick={() => toggleGroupExpand(attr.node.code)}
-                    >
-                      <span>{formatLabel(attr.node.adminName)}</span>
-
-                      <span>⌄</span>
-                    </button>
-
-                    {expand && (
-                      <div className="flex flex-col gap-3 mt-4">
-                        {attr.node.code === "price" ? (
-                          <FilterPrice
-                            priceAttr={attr.node}
-                            initialRange={{
-                              minPrice: tempFilters.minPrice
-                                ? Number(tempFilters.minPrice)
-                                : undefined,
-                              maxPrice: tempFilters.maxPrice
-                                ? Number(tempFilters.maxPrice)
-                                : undefined,
-                            }}
-                            onChangeComplete={(range) => {
-                              setTempFilters((prev) => ({
-                                ...prev,
-                                minPrice: String(range.minPrice),
-                                maxPrice: String(range.maxPrice),
-                              }));
-                            }}
-                          />
-                        ) : (
-                          (attr.node.options.edges || []).map((opt: any) => {
-                            const checked =
-                              tempFilters[attr.node.code] == opt.node._id;
-                            return (
-                              <label
-                                key={opt.node._id}
-                                className="flex gap-3 text-lg"
-                              >
-                                <input
-                                  type="checkbox"
-                                  checked={checked}
-                                  onChange={() =>
-                                    toggleOption(attr.node.code, opt.node._id)
-                                  }
-                                />
-
-                                <span>{opt.node.adminName}</span>
-                              </label>
-                            );
-                          })
-                        )}
-                      </div>
-                    )}
-                  </div>
-                );
-              })}
-            </DrawerBody>
-
-            <div className="px-6 py-4 border-t">
-              <Button
-                color="primary"
-                size="lg"
-                className="w-full"
-                onPress={applyFilters}
+      {/* 筛选内容区域 */}
+      <div className="py-4 max-h-[70vh] overflow-y-auto overflow-x-hidden">
+        {filterAttributes.map((attr) => {
+          const expand = expandedGroups[attr.node.code] ?? true;
+          return (
+            <div key={attr.node._id} className="py-4 border-b">
+              <button
+                className="flex justify-between w-full text-xl"
+                onClick={() => toggleGroupExpand(attr.node.code)}
               >
-                Apply Filter
-              </Button>
+                <span>{formatLabel(attr.node.adminName)}</span>
+                <span>⌄</span>
+              </button>
+              {expand && (
+                <div className="flex flex-col gap-3 mt-4">
+                  {attr.node.code === "price" ? (
+                    <FilterPrice
+                      priceAttr={attr.node}
+                      initialRange={{
+                        minPrice: tempFilters.price_from
+                          ? Number(tempFilters.price_from)
+                          : undefined,
+                        maxPrice: tempFilters.price_to
+                          ? Number(tempFilters.price_to)
+                          : undefined,
+                      }}
+                      onChangeComplete={(range) => {
+                        setTempFilters((prev) => ({
+                          ...prev,
+                          price_from: String(range.minPrice),
+                          price_to: String(range.maxPrice),
+                        }));
+                      }}
+                    />
+                  ) : (
+                    (attr.node.options.edges || []).map((opt: any) => {
+                      const checked =
+                        tempFilters[attr.node.code] == opt.node._id;
+                      return opt.node.productCount > 0 &&  (
+                        <label
+                          key={opt.node._id}
+                          className="flex gap-3 text-lg items-center"
+                        >
+                          <input
+                            type="checkbox"
+                            checked={checked}
+                            onChange={() =>
+                              toggleOption(attr.node.code, opt.node._id)
+                            }
+                          />
+                          <span>{opt.node.adminName}</span>
+                        </label>
+                      );
+                    })
+                  )}
+                </div>
+              )}
             </div>
-          </>
-        </DrawerContent>
-      </Drawer>
-    </>
+          );
+        })}
+      </div>
+
+      {/* 底部应用按钮 */}
+      <div className="">
+        <Button
+          color="primary"
+          size="lg"
+          className="w-full"
+          onPress={applyFilters}
+        >
+          Apply Filter
+        </Button>
+      </div>
+    </div>
   );
 }

+ 1 - 0
src/graphql/catalog/queries/CategoryAttributeFilters.ts

@@ -25,6 +25,7 @@ export const GET_CATEGORY_ATTR_FILTERS = gql`
                 adminName
                 sortOrder
                 swatchValue
+                productCount
               }
             }
           }

+ 2 - 0
src/graphql/catalog/queries/GetCategoryProducts.ts

@@ -4,6 +4,8 @@ export const CATEGORY_PRODUCTS = gql`
 query CategoryProducts($slug: String!, $filter: String, $sortKey: String, $reverse: Boolean, $first: Int, $after: String) {
   categoryProducts(slug: $slug, filter: $filter, sortKey: $sortKey, reverse: $reverse, first: $first, after: $after) {
     totalCount
+    description
+    banner
     pageInfo {
      endCursor     
      hasNextPage   

+ 33 - 6
src/utils/helper.ts

@@ -300,7 +300,6 @@ export function buildProductFilters(params: {
 export function newBuildProductFilters(params: {
   [key: string]: string | string[] | undefined;
 }) {
-  // ==========黑名单:这些字段不会筛选==========
   const EXCLUDE_KEYS = new Set([
     "q",
     "sort",
@@ -309,13 +308,13 @@ export function newBuildProductFilters(params: {
     "after",
     "page",
   ]);
+  const PRICE_KEYS = new Set(["price_from", "price_to"]);
 
   const extractId = (value: string) => {
     if (/^\d+$/.test(value)) return value;
     const match = value.match(/\/(\d+)$/);
     return match ? match[1] : null;
   };
-
   const parseParamIds = (raw: string | string[] | undefined) => {
     let list: string[] = [];
     if (typeof raw === "string") {
@@ -328,13 +327,20 @@ export function newBuildProductFilters(params: {
       .filter((id): id is string => Boolean(id));
   };
 
-  const filterObject: Record<string, string> = {};
+  const filterObject: Record<string, string > = {};
 
-  // 遍历全部传入参数
   for (const [key, value] of Object.entries(params)) {
-    // 黑名单字段直接跳过
     if (EXCLUDE_KEYS.has(key)) continue;
 
+    // ✅ 价格字段单独处理,不执行ID提取
+    if (PRICE_KEYS.has(key)) {
+      if (typeof value === "string" && value) {
+        filterObject[key] = value;
+      }
+      continue;
+    }
+
+    // 普通属性筛选,沿用原来的ID提取逻辑
     const ids = parseParamIds(value);
     if (ids.length > 0) {
       filterObject[key] = ids.join(",");
@@ -362,4 +368,25 @@ export function getUuId() {
         localStorage.setItem(key, id);
     }
     return id;
-};
+};
+// 筛选字段要多个match {\"wig_color\":{\"match\":\"33\"},\"price_from\":10,\"price_to\":212}
+ export function transformFiltersForApi(raw: Record<string, any>) {
+   // filter传参需要调整
+  const result: Record<string, any> = {};
+
+  for (const key in raw) {
+    const val = raw[key];
+    // 价格字段特殊处理:转数字,直接赋值,不包match
+    if (key === "price_from" || key === "price_to") {
+      // 空/undefined 跳过
+      if (val === undefined || val === null || val === "") continue;
+      result[key] = Number(val);
+    } else {
+      // 其他所有筛选属性:套上 {match:xxx}
+      result[key] = {
+        match:String(val),
+      };
+    }
+   }
+   return result;
+  }