Преглед на файлове

产品列表页和产品详情页链接重写

fogwind преди 19 часа
родител
ревизия
f784b2d766

+ 1 - 1
next.config.ts

@@ -40,7 +40,7 @@ const nextConfig: NextConfig = {
     return [
       {
         source: '/:slug.html',
-        destination: '/product/:slug',
+        destination: '/category/:slug',
       },
     ];
   },

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

@@ -1,362 +0,0 @@
-import { type Metadata } from "next";
-import { notFound } from "next/navigation";
-// import { isArray } from "@/utils/type-guards";
-// import FilterList from "@components/theme/filters/FilterList";
-// import Pagination from "@components/catalog/Pagination";
-import Image from "next/image";
-import ProductListing from "./_components/ProductListing";
-import type{
-  // ProductsResponse,
-  GetCategoryAttrFiltersResult,
-  CategoryProductsResult,
-} from "@components/catalog/type";
-import {
-  // GET_FILTER_PRODUCTS,
-  // GET_TREE_CATEGORIES,
-  GET_CATEHORY_BY_ID,
-  GET_CATEGORY_ATTR_FILTERS,
-  CATEGORY_PRODUCTS,
-} from "@/graphql";
-
-import { newSortByFields } from "@utils/constants";
-// import { CategoryDetail } from "@components/theme/search/CategoryDetail";
-import CategoryDesc from "./_components/CategoryDesc";
-import FaqList, { type FaqItem } from "./_components/FaqList";
-// import { TreeCategoriesResponse } from "@/types/theme/category-tree";
-// import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
-import {
-  // extractNumericId,
-  // findCategoryBySlug,
-  newBuildProductFilters,
-  transformFiltersForApi,
-} from "@utils/helper";
-import { serverGraphqlFetch } from "@/utils/bagisto/index";
-import type {
-  // CategoryNode,
-  CategorySingleResponse,
-} from "@/types/theme/category-tree";
-
-/**列表页 */
-export async function generateMetadata({
-  params,
-}: {
-  params: Promise<{ slug: string; id: string }>;
-}): Promise<Metadata> {
-  const { slug: _categorySlug, id: categoryId } = await params;
-  const { data: categoryData } = await serverGraphqlFetch<
-    CategorySingleResponse,
-    { id: number }
-  >({
-    query: GET_CATEHORY_BY_ID,
-    variables: {
-      id: Number(categoryId),
-    },
-  });
-
-  //   const {data:treeData} = await cachedGraphQLRequest<TreeCategoriesResponse>(
-  //     "category",
-  //     GET_TREE_CATEGORIES,
-  //     { parentId: 1 }
-  //   );
-
-  //   const categories = treeData?.treeCategories || [];
-  //   const categoryItem = findCategoryBySlug(categories, categorySlug);
-  const categoryItem = categoryData?.category;
-  if (!categoryItem) return notFound();
-
-  const translation = categoryItem.translation;
-
-  return {
-    title: translation?.metaTitle || translation?.name,
-    description: translation?.description || `${translation?.name} products`,
-  };
-}
-
-export default async function CategoryPage({
-  searchParams,
-  params,
-}: {
-  params: Promise<{ slug: string; id: string }>;
-  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
-}) {
-  //   const { collection: categorySlug } = await params;
-  const { slug: _categorySlug, id: categoryId } = await params;
-  const resolvedParams = await searchParams;
-  function getSearchParam(
-    value: string | string[] | undefined,
-  ): string | undefined {
-    return Array.isArray(value) ? value[0] : value;
-  }
-  /*
-  const [{data:treeData}, filterAttributes] = await Promise.all([
-    cachedGraphQLRequest<TreeCategoriesResponse>(
-      "category",
-      GET_TREE_CATEGORIES,
-      { parentId: 1 }
-    ),
-    getFilterAttributes(), // 这个不能用
-  ]);
-
-  const categories = treeData?.treeCategories || [];
-  const categoryItem = findCategoryBySlug(categories, categorySlug);
-  */
-  //const filterAttributes = await getFilterAttributes(); // 这个不能用
-  const filterAttributesRes = await serverGraphqlFetch<
-    GetCategoryAttrFiltersResult,
-    { slug: string; first: number }
-  >({
-    query: GET_CATEGORY_ATTR_FILTERS,
-    variables: {
-      slug: _categorySlug || "Ready To Go Wig",
-      first: 50,
-    },
-  });
-
-  // 错误判断
-  if (filterAttributesRes.error) {
-    console.error("获取分类筛选属性失败", filterAttributesRes.error);
-    throw new Error(filterAttributesRes.error.message);
-  }
-
-  console.log(
-    "filterAttributesRes---------------AAA",
-    filterAttributesRes.data?.categoryAttributeFilters,
-  );
-  console.log("filterAttributes---------------:", resolvedParams);
-  const { data: categoryData } = await serverGraphqlFetch<
-    CategorySingleResponse,
-    { id: number }
-  >({
-    query: GET_CATEHORY_BY_ID,
-    variables: {
-      id: Number(categoryId),
-    },
-  });
-  const categoryItem = categoryData?.category;
-  if (!categoryItem) return notFound();
-
-  console.log("categoryItem +++++++++++", categoryItem);
-  //   const numericId = extractNumericId(categoryItem.id);
-  const numericId = String(categoryItem._id);
-
-  // const {
-  //   q: searchValue,
-  //   page,
-  //   cursor,
-  //   before,
-  // } = (resolvedParams || {}) as {
-  //   [key: string]: string;
-  // };
-  const searchValue = resolvedParams?.q ?? "";
-
-  // const page = getSearchParam(resolvedParams?.page);
-
-  const cursor = getSearchParam(resolvedParams?.cursor);
-
-  const before = getSearchParam(resolvedParams?.before);
-  const itemsPerPage = 15;
-  // const currentPage = page ? parseInt(page) - 1 : 0;
-  const sortValue = getSearchParam(resolvedParams?.sort) ?? "name-asc";
-  const selectedSort =
-    newSortByFields.find((s) => s.key === sortValue) || newSortByFields[0];
-
-  const { filterObject: baseFilterObject } = newBuildProductFilters(
-    resolvedParams || {},
-  );
-
-  const filterObject: Record<string, string> = {
-    ...baseFilterObject,
-  };
-
-  if (numericId) {
-    filterObject.category_id = numericId;
-  }
-  const finalFilterObj =transformFiltersForApi(filterObject);
-  const filterInput = JSON.stringify(finalFilterObj);
-  // 默认获取产品数据接口:
-  const { data: categoryProductDatas } = await serverGraphqlFetch<
-    CategoryProductsResult,
-    any
-  >({
-    query: CATEGORY_PRODUCTS,
-    variables: {
-      slug: _categorySlug || "Ready To Go Wig",
-      filter: filterInput,
-      id: Number(categoryId),
-      ...(before
-        ? {
-            last: itemsPerPage,
-            before,
-          }
-        : {
-            first: itemsPerPage,
-            after: cursor || null,
-          }),
-      sortKey: selectedSort.sortKey,
-      reverse: selectedSort.reverse,
-    },
-  });
-  console.log("categoryProductDatas-----------:",categoryProductDatas,selectedSort);
-  
-  // 这个也改成 serverGraphqlFetch
-  // const productVariables = {
-  //   query: searchValue || "",
-  //   filter: filterInput,
-  //   ...(before
-  //     ? {
-  //         last: itemsPerPage,
-  //         before,
-  //       }
-  //     : {
-  //         first: itemsPerPage,
-  //         after: cursor || null,
-  //       }),
-  //   sortKey: selectedSort.sortKey,
-  //   reverse: selectedSort.reverse,
-  // };
-  // const { data } = await serverGraphqlFetch<
-  //   ProductsResponse,
-  //   typeof productVariables
-  // >({
-  //   query: GET_FILTER_PRODUCTS,
-  //   variables: productVariables,
-  // });
-  // const [{ data }] = await Promise.all([
-  //   cachedCategoryRequest<ProductsResponse>(categorySlug, GET_FILTER_PRODUCTS, {
-  //     query: searchValue || "",
-  //     filter: filterInput,
-  //     ...(before
-  //       ? { last: itemsPerPage, before: before }
-  //       : { first: itemsPerPage, after: cursor }),
-  //     sortKey: selectedSort.sortKey,
-  //     reverse: selectedSort.reverse,
-  //   }),
-  // ]);
-  console.log(
-    "GET_FILTER_PRODUCTS ----- ",
-    searchValue,
-    filterInput,
-    itemsPerPage,
-    before,
-    cursor,
-    selectedSort,
-  );
-
-  const products = categoryProductDatas?.categoryProducts?.products?.map((e) => e.node) || [];
-  const pageInfo = categoryProductDatas?.categoryProducts?.pageInfo||{endCursor:'',hasNextPage:false};
-  const totalCount = categoryProductDatas?.categoryProducts?.totalCount || 0;
-  const translation = categoryItem.translation;
-  console.log("initialQuery------------", {
-    search: searchValue,
-    sort: sortValue,
-    sortKey: selectedSort.sortKey,
-    reverse: selectedSort.reverse,
-    filters: baseFilterObject,
-  });
-  // 模拟FAQ假数据
-  const mockFaqData: FaqItem[] = [
-    {
-      id: "1",
-      question: "How long is the delivery time for human hair wigs?",
-      answer:
-        "Standard shipping takes 7-12 working days, expedited shipping supports 3-5 days delivery worldwide.",
-      likeCount: 248,
-    },
-    {
-      id: "2",
-      question: "Is the HD lace wig invisible on all skin tones?",
-      answer:
-        "Our HD transparent lace matches all skin tones, melts perfectly without foundation.",
-      likeCount: 186,
-    },
-    {
-      id: "3",
-      question: "Can I dye and bleach the hair extensions?",
-      answer:
-        "100% human hair can be dyed darker or bleached lighter, we recommend professional hairdresser operation.",
-      likeCount: 312,
-    },
-    {
-      id: "4",
-      question: "What payment methods do you support?",
-      answer:
-        "Credit card, PayPal, Apple Pay, Google Pay and bank transfer are all available.",
-      likeCount: 97,
-    },
-    {
-      id: "5",
-      question: "How to return or exchange products?",
-      answer:
-        "Contact our customer service within 30 days after receiving the package for free exchange.",
-      likeCount: 154,
-    },
-  ];
-  return (
-    <>
-      {/* <MobileSearchBar /> */}
-      <section>
-    
-          {/* <CategoryDetail
-            categoryItem={{
-              description: translation?.description ?? "",
-              name: translation?.name ?? "",
-            }}
-          /> */}
-           <div className="flex justify-center">
-              <Image
-               src={categoryProductDatas? categoryProductDatas.categoryProducts.banner: ''}
-               alt={'categoryBanner'}
-               width={358}
-               height={176}
-              />
-          </div>
-           <CategoryDesc
-            title={translation?.name ?? ""}
-            description={
-              categoryProductDatas? categoryProductDatas.categoryProducts.description: ''
-            } //translation?.description ?? ""}
-          />
-   
-        {/* <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full max-w-screen-2xl mx-auto px-4">
-          <FilterList filterAttributes={filterAttributes} />
-          <SortOrder sortOrders={SortByFields} title="Sort by" />
-        </div> */}
-        <ProductListing
-          categoryId={numericId}
-          slug={_categorySlug} 
-          filterAttributes={
-          filterAttributesRes.data?  filterAttributesRes.data.categoryAttributeFilters.edges:[]
-          }
-          initialProducts={products}
-          initialPageInfo={pageInfo}
-          initialTotal={totalCount}
-          initialQuery={{
-            // search: searchValue,
-            sortValue: sortValue,
-            sortKey: selectedSort.sortKey,
-            reverse: selectedSort.reverse,
-            filters: baseFilterObject,
-            cursor,
-          }}
-        />
-
-        {/* {isArray(products) &&
-          (totalCount > itemsPerPage || pageInfo?.hasNextPage) && (
-            <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>
-          )} */}
-        <FaqList faqList={mockFaqData} />
-      </section>
-    </>
-  );
-}

src/app/(public)/category/[slug]/[id]/_components/CategoryDesc.tsx → src/app/(public)/category/[slug]/_components/CategoryDesc.tsx


src/app/(public)/category/[slug]/[id]/_components/FaqList.tsx → src/app/(public)/category/[slug]/_components/FaqList.tsx


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

@@ -1,18 +1,17 @@
 "use client";
 
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useRef, Suspense } from "react";
 
 import Grid from "@components/theme/ui/grid/Grid";
 // import ProductGridItems from "@components/catalog/product/ProductGridItems";
 import NewProductGridItems from "@/components/catalog/product/NewProductGridItems";
 import NewMobileFilter from "@components/theme/filters/NewMobileFilter";
 import NewSortOrder from "@components/theme/filters/NewSortOrder";
-
 import { newSortByFields } from "@utils/constants";
 import { isArray } from "@/utils/type-guards";
 import { useRouter, usePathname } from "next/navigation";
 import {  useApolloClient } from "@apollo/client/react";
-import type{ CategoryProductsResult,PageInfo,CategoryProductNode,CategoryAttrFilterItem } from "@components/catalog/type";
+import type{ CategoryProductsResult,PageInfo,CategoryProductNode,GetCategoryAttrFiltersResult,} from "@components/catalog/type";
 import {CATEGORY_PRODUCTS} from "@/graphql";
 import {transformFiltersForApi} from "@utils/helper";
 interface QueryState {
@@ -29,7 +28,7 @@ interface ProductListingProps {
   slug:string;
   categoryId: string;
 
-  filterAttributes:Array<{ node: CategoryAttrFilterItem }>;
+  filterAttributesPromise: Promise<{error: string; data: GetCategoryAttrFiltersResult | null;}>;
 
   initialProducts: CategoryProductNode[];
 
@@ -43,7 +42,7 @@ interface ProductListingProps {
 export default function ProductListing({
   slug,
   categoryId,
-  filterAttributes,
+  filterAttributesPromise,
   initialProducts,
   initialPageInfo,
   initialTotal,
@@ -223,19 +222,21 @@ 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">
-        <NewMobileFilter
-          filterAttributes={filterAttributes}
-          filters={query.filters}
-          onChange={(filters) => {
-            updateQuery({
-              ...query,
-
-              filters,
-
-              cursor: undefined,
-            });
-          }}
-        />
+        <Suspense fallback={<><p className="text-ly-12">Loading Filter...</p></>}>
+          <NewMobileFilter
+            filterAttributesPromise={filterAttributesPromise}
+            filters={query.filters}
+            onChange={(filters) => {
+              updateQuery({
+                ...query,
+
+                filters,
+
+                cursor: undefined,
+              });
+            }}
+          />
+        </Suspense>
         <NewSortOrder
           sortOrders={newSortByFields}
           title="Sort by"

src/app/(public)/category/[slug]/[id]/loading.tsx → src/app/(public)/category/[slug]/loading.tsx


+ 242 - 0
src/app/(public)/category/[slug]/page.tsx

@@ -0,0 +1,242 @@
+import { type Metadata } from "next";
+import { cache } from "react";
+import { notFound } from "next/navigation";
+import Image from "next/image";
+import ProductListing from "./_components/ProductListing";
+import type{
+    GetCategoryAttrFiltersResult,
+    CategoryProductsResult,
+} from "@components/catalog/type";
+import {
+    GET_CATEHORY_BY_SLUG,
+    GET_CATEGORY_ATTR_FILTERS,
+    CATEGORY_PRODUCTS,
+} from "@/graphql";
+
+import { newSortByFields } from "@utils/constants";
+import CategoryDesc from "./_components/CategoryDesc";
+import FaqList, { type FaqItem } from "./_components/FaqList";
+import {
+    newBuildProductFilters,
+    transformFiltersForApi,
+} from "@utils/helper";
+import { serverGraphqlFetch } from "@/utils/bagisto/index";
+import type { CategorySingleResponse } from "@/types/theme/category-tree";
+
+
+const getCategoryDetails = cache(async function(slug: string) {
+    return await serverGraphqlFetch<
+        CategorySingleResponse,
+        { slug: string }
+    >({
+        query: GET_CATEHORY_BY_SLUG,
+        variables: {
+            slug: slug
+        },
+    });
+});
+
+
+/**列表页 */
+export async function generateMetadata({
+  params,
+}: {
+  params: Promise<{ slug: string; }>;
+}): Promise<Metadata> {
+    const { slug } = await params;
+    const { data: categoryData } = await getCategoryDetails(slug);
+
+    const categoryItem = categoryData?.category;
+
+    const translation = categoryItem?.translation;
+    const metadataRes = {
+        title: '',
+        description: ''
+    };
+    if(translation) {
+        metadataRes.title = translation.metaTitle || translation.name;
+        metadataRes.description = translation.metaDescription || translation.description;
+    } else {
+        metadataRes.title = 'Category Products';
+        metadataRes.description = '';
+    }
+
+    return metadataRes;
+}
+
+export default async function CategoryPage({
+    searchParams,
+    params,
+}: {
+    params: Promise<{ slug: string; }>;
+    searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+
+    const { slug: categorySlug } = await params;
+    const resolvedParams = await searchParams;
+    function getSearchParam(
+        value: string | string[] | undefined,
+    ): string | undefined {
+        return Array.isArray(value) ? value[0] : value;
+    }
+
+    const { data: categoryData } = await getCategoryDetails(categorySlug);
+    const categoryItem = categoryData?.category;
+
+    if (!categoryItem) return notFound();
+
+    
+    const categoryId = String(categoryItem._id);
+
+    const filterAttributesPromise = serverGraphqlFetch<
+        GetCategoryAttrFiltersResult,
+        { slug: string; first: number }
+    >({
+        query: GET_CATEGORY_ATTR_FILTERS,
+        variables: {
+        slug: categorySlug,
+        first: 50,
+        },
+    }).then((res) => {
+        return {
+            error: res.error ? res.error.message : '',
+            data: res.data
+        };
+    }).catch((err) => {
+        return {
+            error: err.message || 'Get filter attributes failed, please try again later.',
+            data: null
+        };
+    });
+
+
+    const cursor = getSearchParam(resolvedParams?.cursor);
+
+    const before = getSearchParam(resolvedParams?.before);
+    const itemsPerPage = 15;
+
+    const sortValue = getSearchParam(resolvedParams?.sort) ?? "name-asc";
+    const selectedSort =
+        newSortByFields.find((s) => s.key === sortValue) || newSortByFields[0];
+
+    const { filterObject: baseFilterObject } = newBuildProductFilters(
+        resolvedParams || {},
+    );
+
+    const filterObject: Record<string, string> = {
+        ...baseFilterObject,
+    };
+
+    if (categoryId) {
+        filterObject.category_id = categoryId;
+    }
+    const finalFilterObj =transformFiltersForApi(filterObject);
+    const filterInput = JSON.stringify(finalFilterObj);
+    // 默认获取产品数据接口:
+    const { data: categoryProductDatas } = await serverGraphqlFetch<
+        CategoryProductsResult,
+        any
+    >({
+        query: CATEGORY_PRODUCTS,
+        variables: {
+        slug: categorySlug,
+        filter: filterInput,
+        id: Number(categoryId),
+        ...(before
+            ? {
+                last: itemsPerPage,
+                before,
+            }
+            : {
+                first: itemsPerPage,
+                after: cursor || null,
+            }),
+        sortKey: selectedSort.sortKey,
+        reverse: selectedSort.reverse,
+        },
+    });
+    
+
+    const products = categoryProductDatas?.categoryProducts?.products?.map((e) => e.node) || [];
+    const pageInfo = categoryProductDatas?.categoryProducts?.pageInfo||{endCursor:'',hasNextPage:false};
+    const totalCount = categoryProductDatas?.categoryProducts?.totalCount || 0;
+    const translation = categoryItem.translation;
+
+    // 模拟FAQ假数据
+    const mockFaqData: FaqItem[] = [
+        {
+            id: "1",
+            question: "How long is the delivery time for human hair wigs?",
+            answer:
+                "Standard shipping takes 7-12 working days, expedited shipping supports 3-5 days delivery worldwide.",
+            likeCount: 248,
+        },
+        {
+            id: "2",
+            question: "Is the HD lace wig invisible on all skin tones?",
+            answer:
+                "Our HD transparent lace matches all skin tones, melts perfectly without foundation.",
+            likeCount: 186,
+        },
+        {
+            id: "3",
+            question: "Can I dye and bleach the hair extensions?",
+            answer:
+                "100% human hair can be dyed darker or bleached lighter, we recommend professional hairdresser operation.",
+            likeCount: 312,
+        },
+        {
+            id: "4",
+            question: "What payment methods do you support?",
+            answer:
+                "Credit card, PayPal, Apple Pay, Google Pay and bank transfer are all available.",
+            likeCount: 97,
+        },
+        {
+            id: "5",
+            question: "How to return or exchange products?",
+            answer:
+                "Contact our customer service within 30 days after receiving the package for free exchange.",
+            likeCount: 154,
+        },
+    ];
+    return (
+        <section>
+        
+            <div className="flex justify-center">
+                <Image
+                    src={categoryProductDatas? categoryProductDatas.categoryProducts.banner: ''}
+                    alt={'categoryBanner'}
+                    width={358}
+                    height={176}
+                />
+            </div>
+            <CategoryDesc
+                title={translation?.name ?? ""}
+                description={
+                    categoryProductDatas? categoryProductDatas.categoryProducts.description: ''
+                } //translation?.description ?? ""}
+            />
+    
+
+            <ProductListing
+                categoryId={categoryId}
+                slug={categorySlug} 
+                filterAttributesPromise={filterAttributesPromise}
+                initialProducts={products}
+                initialPageInfo={pageInfo}
+                initialTotal={totalCount}
+                initialQuery={{
+                    // search: searchValue,
+                    sortValue: sortValue,
+                    sortKey: selectedSort.sortKey,
+                    reverse: selectedSort.reverse,
+                    filters: baseFilterObject,
+                    cursor,
+                }}
+            />
+            <FaqList faqList={mockFaqData} />
+        </section>
+
+    );
+}

src/app/(public)/product/[...urlProduct]/loading.tsx → src/app/(public)/product/[urlkey]/[id]/loading.tsx


+ 11 - 11
src/app/(public)/product/[...urlProduct]/page.tsx

@@ -15,10 +15,10 @@ import { RelatedProductsSection } from "@components/catalog/product/RelatedProdu
 import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
 import { ProductInformation } from "@/app/(public)/product/_components/ProductInformation";
 import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
-import ProductDescription from "../_components/ProductDescription";
-import SwitchButton from "../_components/SwitchButton";
-import ShopServicePanel from "../_components/ShopServicePanel";
-import Recommend from "../_components/youmaylike/Recommend"
+import ProductDescription from "../../_components/ProductDescription";
+import SwitchButton from "../../_components/SwitchButton";
+import ShopServicePanel from "../../_components/ShopServicePanel";
+import Recommend from "../../_components/youmaylike/Recommend"
 import { formatFlexibleVariants } from "@/utils/variantTools";
 
 // export const dynamic = 'auto'
@@ -29,23 +29,23 @@ export const dynamic = "force-dynamic";
 export default async function ProductPage({
   params,
 }: {
-  params: Promise<{ urlProduct: string[] }>;
+  params: Promise<{ urlkey: string;  id: string; }>;
   searchParams: Promise<{ type: string }>;
 }) {
-  const { urlProduct } = await params;
-  const fullPath = urlProduct.join("/");
-  
+  const { urlkey } = await params;
+
+
   const resProductData = await serverGraphqlFetch<SingleProductResponse,{urlKey: string;}>({
       // urlKey, // 产品名称
       query: GET_PRODUCT_BY_URL_KEY, // gql查询语句
       variables:{ 
-        urlKey: fullPath 
+        urlKey: urlkey 
       },
       takeAuthorization: false,
       cache: 'no-cache'
       
   });
-  console.log("productCustomerFeatures----------------------",resProductData);
+
   const product = resProductData.data?.product;
   const fetchProductError = resProductData.error;
   if (fetchProductError && fetchProductError.extensions.status === 404) return notFound();
@@ -115,7 +115,7 @@ export default async function ProductPage({
       </div>
         
       <Suspense fallback={<RelatedProductSkeleton />}>
-        <RelatedProductsSection fullPath={fullPath} />
+        <RelatedProductsSection fullPath={urlkey} />
       </Suspense>
     </>
   );

+ 5 - 5
src/components/catalog/type.ts

@@ -161,14 +161,14 @@ export interface ProductsResponse {
   };
 }
 export interface CategoryAttrFilterItem  {
-  _id: string;
+  _id: number;
   code: string;
   adminName: string;
   type: string;
-  swatchType: string;
-  position: number;
-  minPrice: number | null;
-  maxPrice: number | null;
+  swatchType: string | null;
+  position: number | null;
+  minPrice: string;
+  maxPrice: string;
   options: {
     edges: Array<{
       node: {

+ 1 - 1
src/components/theme/filters/FilterPrice.tsx

@@ -6,7 +6,7 @@ import {currencyCountryMap} from "@/utils/constants";
 
 type PriceFilterNode = {
   _id: number;
-  code: "price";
+  code: string;
   adminName: string;
   minPrice: string;
   maxPrice: string;

+ 10 - 7
src/components/theme/filters/NewMobileFilter.tsx

@@ -6,33 +6,35 @@ import {
   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 { useEffect, useState, useRef, use } from "react";
+import type{ GetCategoryAttrFiltersResult } from "@components/catalog/type";
+
 
 interface MobileFilterProps {
-  filterAttributes: any[];
+
+  filterAttributesPromise: Promise<{error: string; data: GetCategoryAttrFiltersResult | null;}>;
 
   filters: Record<string, string>;
 
   onChange: (filters: Record<string, string>) => void;
 }
 export default function MobileFilter({
-  filterAttributes,
+  filterAttributesPromise,
   filters,
   onChange,
 }: MobileFilterProps) {
   const { isOpen, onOpen, onOpenChange } = useDisclosure();
   // 记录上一次抽屉状态,用来捕获【关闭→打开】瞬间
   const prevIsOpenRef = useRef(isOpen);
-
+  const {data: filterAttributesData, error: filterAttributesError} = use(filterAttributesPromise);
+  const filterAttributes = filterAttributesData ? filterAttributesData.categoryAttributeFilters.edges : [];
   /**
    * 弹窗里面临时选择
    */
@@ -57,7 +59,7 @@ export default function MobileFilter({
 
         const expand: Record<string, boolean> = {};
         filterAttributes.forEach((attr) => {
-          expand[attr.code] = true;
+          expand[attr.node.code] = true;
         });
         setExpandedGroups(expand);
       });
@@ -155,6 +157,7 @@ export default function MobileFilter({
             </DrawerHeader>
 
             <DrawerBody className="px-6 overflow-y-auto">
+              {filterAttributesError !== '' && <p className="text-ly-errorcolor">{filterAttributesError}</p>}
               {filterAttributes.map((attr) => {
                 const expand = expandedGroups[attr.node.code] ?? true;
 

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

@@ -38,50 +38,3 @@ export const GET_CATEGORY_ATTR_FILTERS = gql`
     }
   }
 `;
-
-// ---------------------- TS类型定义 ----------------------
-export type FilterOptionNode = {
-  _id: string;
-  adminName: string;
-  sortOrder: number;
-  swatchValue: string | null;
-};
-
-export type FilterOptionEdge = {
-  node: FilterOptionNode;
-};
-
-export type CategoryAttrFilterNode = {
-  _id: string;
-  code: string;
-  adminName: string;
-  type: string;
-  swatchType: string;
-  position: number;
-  minPrice: number | null;
-  maxPrice: number | null;
-  options: {
-    edges: FilterOptionEdge[];
-  };
-};
-
-export type CategoryAttrFilterEdge = {
-  node: CategoryAttrFilterNode;
-};
-
-export type FilterPageInfo = {
-  hasNextPage: boolean;
-  endCursor: string | null;
-};
-
-export type GetCategoryAttrFiltersData = {
-  categoryAttributeFilters: {
-    edges: CategoryAttrFilterEdge[];
-    pageInfo: FilterPageInfo;
-  };
-};
-
-export type GetCategoryAttrFiltersVars = {
-  categorySlug: string;
-  first?: number;
-};

+ 24 - 0
src/graphql/catalog/queries/GetCategoryBySlug.ts

@@ -0,0 +1,24 @@
+import { gql,type TypedDocumentNode } from "@apollo/client";
+import type {CategorySingleResponse} from "@/types/theme/category-tree";
+
+
+export const GET_CATEHORY_BY_SLUG: TypedDocumentNode<CategorySingleResponse,{slug: string}> = gql`
+query getCategoryBySlug($slug: String) {
+  category(slug: $slug) {
+    id
+    _id
+    position
+    status
+    logoPath
+    translation {
+      name
+      slug
+      urlPath
+      description
+
+      metaTitle
+      metaKeywords
+      metaDescription
+    }
+  }
+}`;

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

@@ -10,6 +10,7 @@ export { GET_FILTER_PRODUCTS } from "./GetFilterProducts";
 export { GET_TREE_CATEGORIES } from "./GetTreeCategories";
 export { SEARCH_PRODUCTS } from "./SearchProducts";
 export { GET_CATEHORY_BY_ID } from "./GetCategoryById";
+export { GET_CATEHORY_BY_SLUG } from "./GetCategoryBySlug";
 export { GET_PRODUCT_BY_ID } from "./GetProductById";
 export { GET_CATEGORY_ATTR_FILTERS } from "./CategoryAttributeFilters";
 export { CATEGORY_PRODUCTS } from "./GetCategoryProducts";

+ 31 - 27
src/proxy.ts

@@ -2,37 +2,41 @@ import { NextResponse } from 'next/server'
 import type { NextRequest } from 'next/server'
 import {handleCurrency} from "./proxy/currency";
 import {handleAuth} from "./proxy/auth";
+import { pathRewrite } from "./proxy/pathRewrite";
 import { currencyCookieOptions } from "@/utils/constants/server";
 
 export async function proxy(request: NextRequest) {
   // 找出缺失的默认 Cookie
-  const missingCookies = handleCurrency(request);
-
-  // 补齐当前请求缺失的 Cookie
-  for (const cookie of missingCookies) {
-    request.cookies.set(cookie.name, cookie.value);
-  }
-
-  // 将修改后的请求头转发给 Server Components
-  let response = NextResponse.next({
-    request: {
-      headers: request.headers,
-    },
-  });
-
-  // 将默认 Cookie 写入浏览器
-  for (const cookie of missingCookies) {
-    response.cookies.set(
-      cookie.name,
-      cookie.value,
-      currencyCookieOptions,
-    );
-  }
-
-  // 处理认证与重定向
-  response = await handleAuth(request, response);
-
-  return response;
+    const missingCookies = handleCurrency(request);
+
+    // 补齐当前请求缺失的 Cookie
+    for (const cookie of missingCookies) {
+        request.cookies.set(cookie.name, cookie.value);
+    }
+
+    // 将修改后的请求头转发给 Server Components
+    let response = NextResponse.next({
+        request: {
+            headers: request.headers,
+        },
+    });
+
+    // 将默认 Cookie 写入浏览器
+    for (const cookie of missingCookies) {
+        response.cookies.set(
+            cookie.name,
+            cookie.value,
+            currencyCookieOptions,
+        );
+    }
+
+    // 处理 URL 重写
+    response = pathRewrite(request, response);
+    
+    // 处理认证与重定向
+    response = await handleAuth(request, response);
+
+    return response;
 }
 
 

+ 2 - 0
src/proxy/auth.ts

@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
 import type { NextRequest } from "next/server";
 import { getToken } from "next-auth/jwt";
 import { env } from "@/env";
+
+// 拦截已经登录的用户访问登录页和注册页,并将他们重定向到首页,同时尽可能保留前面逻辑已经设置的 Cookie。
 export async function handleAuth(
     request: NextRequest,
     response: NextResponse

+ 29 - 0
src/proxy/pathRewrite.ts

@@ -0,0 +1,29 @@
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+
+export function pathRewrite(
+    request: NextRequest,
+    response: NextResponse
+) {
+    const { pathname } = request.nextUrl;
+
+    const match = pathname.match(/^\/products\/([^/]+)_([^/]+)\.html$/);
+
+    if (!match) {
+        return response;
+    }
+
+    const [, slug, id] = match;
+
+    const url = request.nextUrl.clone();
+    url.pathname = `/product/${slug}/${id}`;
+
+    const rewriteResponse = NextResponse.rewrite(url);
+
+    // 保留前面处理逻辑已经设置的 Cookie
+    response.cookies.getAll().forEach((cookie) => {
+        rewriteResponse.cookies.set(cookie);
+    });
+
+    return rewriteResponse;
+}

+ 4 - 2
src/types/theme/category-tree.ts

@@ -21,8 +21,10 @@ export interface CategoryTranslationNode {
   name: string;
   slug: string;
   urlPath: string;
-  description?: string | null;
-  metaTitle?: string | null;
+  description: string;
+  metaTitle: string;
+  metaKeywords: string;
+  metaDescription: string;
 }
 export interface CategoryTranslationEdge {
   node: CategoryTranslationNode;

+ 6 - 1
src/utils/bagisto/index.ts

@@ -172,7 +172,12 @@ export async function restApiFetch<T>({
   }
 }
 
-// graphql fetch use for server side
+/**
+ * graphql fetch use for server side
+ * 使用serverGraphqlFetch时,如果直接 await serverGraphqlFetch(...params), throw e 抛出的错误会被 ErrorBoundary组件或者error.tsx捕获
+ * 如果使用serverGraphqlFetch(...params).catch(fn),throw e 抛出的错误会被catch捕获
+ *   */ 
+
 export async function serverGraphqlFetch<
   TData,
   TVariables = Record<string, never>