瀏覽代碼

产品详情页优化:接口优化;代码优化;删除无用代码;

fogwind 1 天之前
父節點
當前提交
884c636b2e
共有 70 個文件被更改,包括 1274 次插入 和 3115 次删除
  1. 二進制
      public/image/payment/afterpay.png
  2. 二進制
      public/image/payment/applepay.png
  3. 二進制
      public/image/payment/credit-card.png
  4. 二進制
      public/image/payment/credit.png
  5. 二進制
      public/image/payment/klarna.png
  6. 二進制
      public/image/payment/paypal.png
  7. 二進制
      public/image/products_details/afterpay.png
  8. 二進制
      public/image/products_details/afterpay.webp
  9. 二進制
      public/image/products_details/klarna.png
  10. 二進制
      public/image/products_details/klarna.webp
  11. 二進制
      public/image/products_details/paypal-expressm.png
  12. 二進制
      public/image/products_details/paypal.webp
  13. 0 59
      src/app/(public)/_components/HomeBestSellers.tsx
  14. 0 59
      src/app/(public)/_components/HomeNewArrival.tsx
  15. 0 59
      src/app/(public)/_components/MiddleImageSwiper.tsx
  16. 0 59
      src/app/(public)/_components/RecommendedProducts.tsx
  17. 0 59
      src/app/(public)/_components/Trending.tsx
  18. 1 1
      src/app/(public)/category/[slug]/_components/ProductListing.tsx
  19. 1 1
      src/app/(public)/category/[slug]/page.tsx
  20. 123 67
      src/app/(public)/product/[urlkey]/[id]/page.tsx
  21. 66 0
      src/app/(public)/product/_components/InstallmentTips.tsx
  22. 4 4
      src/app/(public)/product/_components/NewUserBanner.tsx
  23. 8 140
      src/app/(public)/product/_components/ProductAddToCart.tsx
  24. 2 6
      src/app/(public)/product/_components/ProductDetail.tsx
  25. 71 0
      src/app/(public)/product/_components/ProductInforWrapper.tsx
  26. 0 442
      src/app/(public)/product/_components/ProductInformation.tsx
  27. 1 1
      src/app/(public)/product/_components/ProductMedia.tsx
  28. 361 0
      src/app/(public)/product/_components/ProductOptionsVariants.tsx
  29. 41 0
      src/app/(public)/product/_components/ProductPrice.tsx
  30. 4 17
      src/app/(public)/product/_components/ProductReviewSection.tsx
  31. 5 5
      src/app/(public)/product/_components/ProductText.tsx
  32. 0 144
      src/app/(public)/product/_components/ProductTwoColumnLayout.tsx
  33. 28 0
      src/app/(public)/product/_components/SkeletonProductOptions.tsx
  34. 10 97
      src/app/(public)/product/_components/SwitchButton.tsx
  35. 7 2
      src/app/(public)/product/_components/review/AddRatingStar.tsx
  36. 0 375
      src/app/(public)/product/_components/review/ReviewDetail.tsx
  37. 6 44
      src/app/(public)/product/_components/review/ReviewModal.tsx
  38. 0 59
      src/app/(public)/product/_components/youmaylike/Recommend.tsx
  39. 1 1
      src/app/(public)/search/page.tsx
  40. 0 59
      src/components/catalog/product/NewProductGridItems.tsx
  41. 7 1
      src/components/catalog/product/ProductsSection.tsx
  42. 2 2
      src/components/catalog/product/RelatedProductsSection.tsx
  43. 0 420
      src/components/catalog/type.ts
  44. 1 1
      src/components/common/AddToCartModal/AddToCartModal.tsx
  45. 98 46
      src/components/common/AddToCartModal/OpenAddToCartModalButton.tsx
  46. 1 1
      src/components/common/AddToCartModal/ProductOptionsInAddToCartModal.tsx
  47. 9 2
      src/components/common/NextImage.tsx
  48. 9 2
      src/components/common/Rating.tsx
  49. 0 34
      src/components/common/icons/ShoppingCartIcon.tsx
  50. 0 61
      src/components/common/skeleton/ProductSkeleton.tsx
  51. 0 20
      src/components/common/type.ts
  52. 0 59
      src/components/customer/accountSwiper.tsx
  53. 0 53
      src/components/home/ProductCarouselTheme.tsx
  54. 1 1
      src/components/theme/filters/NewMobileFilter.tsx
  55. 0 63
      src/graphql/catalog/fragments/ProductDetailed.ts
  56. 0 1
      src/graphql/catalog/fragments/index.ts
  57. 39 0
      src/graphql/catalog/queries/GetProductBaseInfo.ts
  58. 42 9
      src/graphql/catalog/queries/GetProductById.ts
  59. 0 16
      src/graphql/catalog/queries/GetProductByUrlKey.ts
  60. 28 0
      src/graphql/catalog/queries/GetProductOptions.ts
  61. 0 2
      src/graphql/catalog/queries/GetRelatedProducts.ts
  62. 1 1
      src/graphql/catalog/queries/SearchProducts.ts
  63. 2 1
      src/graphql/catalog/queries/index.ts
  64. 3 3
      src/store/slices/addToCartDialogSlice.ts
  65. 90 179
      src/types/category/type.ts
  66. 139 20
      src/types/products/productDetail.ts
  67. 19 9
      src/types/products/review.ts
  68. 0 345
      src/types/types.ts
  69. 1 1
      src/utils/hooks/useSearchProduct.ts
  70. 42 2
      src/utils/variantTools.ts

二進制
public/image/payment/afterpay.png


二進制
public/image/payment/applepay.png


二進制
public/image/payment/credit-card.png


二進制
public/image/payment/credit.png


二進制
public/image/payment/klarna.png


二進制
public/image/payment/paypal.png


二進制
public/image/products_details/afterpay.png


二進制
public/image/products_details/afterpay.webp


二進制
public/image/products_details/klarna.png


二進制
public/image/products_details/klarna.webp


二進制
public/image/products_details/paypal-expressm.png


二進制
public/image/products_details/paypal.webp


+ 0 - 59
src/app/(public)/_components/HomeBestSellers.tsx

@@ -75,65 +75,6 @@ const HomeBestSellers = ({ bestSellerSection }: jinGangSectionProps) => {
 
               <OpenAddToCartModalButton
                 productId={product.id}
-                icon={
-                  <svg
-                          xmlns="http://www.w3.org/2000/svg"
-                          width="32"
-                          height="32"
-                          viewBox="0 0 32 32"
-                          fill="none"
-                        >
-                          <rect
-                            x="0"
-                            y="0"
-                            width="32"
-                            height="32"
-                            rx="8"
-                            fill="#036141"
-                          ></rect>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
-                          ></path>
-                          <ellipse
-                            cx="11.066040515899658"
-                            cy="23.199928283691406"
-                            rx="1.4000248908996582"
-                            ry="1.4000015258789062"
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                          ></ellipse>
-                          <ellipse
-                            cx="23.1998291015625"
-                            cy="23.199928283691406"
-                            rx="1.4000244140625"
-                            ry="1.4000015258789062"
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                          ></ellipse>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M15.2656 15.2665L18.999 15.2665"
-                          ></path>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M17.1348 17.1332L17.1348 13.3999"
-                          ></path>
-                    </svg>
-                }
               />
             </div>
           </div>

+ 0 - 59
src/app/(public)/_components/HomeNewArrival.tsx

@@ -71,65 +71,6 @@ const HomeNewArrival = ({ newArrivalSection }: newArrivalSectionProps) => {
 
               <OpenAddToCartModalButton
                 productId={product.id}
-                icon={
-                  <svg
-                          xmlns="http://www.w3.org/2000/svg"
-                          width="32"
-                          height="32"
-                          viewBox="0 0 32 32"
-                          fill="none"
-                        >
-                          <rect
-                            x="0"
-                            y="0"
-                            width="32"
-                            height="32"
-                            rx="8"
-                            fill="#036141"
-                          ></rect>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
-                          ></path>
-                          <ellipse
-                            cx="11.066040515899658"
-                            cy="23.199928283691406"
-                            rx="1.4000248908996582"
-                            ry="1.4000015258789062"
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                          ></ellipse>
-                          <ellipse
-                            cx="23.1998291015625"
-                            cy="23.199928283691406"
-                            rx="1.4000244140625"
-                            ry="1.4000015258789062"
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                          ></ellipse>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M15.2656 15.2665L18.999 15.2665"
-                          ></path>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M17.1348 17.1332L17.1348 13.3999"
-                          ></path>
-                    </svg>
-                }
               />
             </div>
           </div>

+ 0 - 59
src/app/(public)/_components/MiddleImageSwiper.tsx

@@ -205,65 +205,6 @@ const MiddleImageSwiper = ({ zeroWear,zeroWearImage }: zeroWearProps) => {
                       {/* 购物车图标 */}
                       <OpenAddToCartModalButton
                          productId={item.id}
-                        icon={
-                          <svg
-                            xmlns="http://www.w3.org/2000/svg"
-                            width="32"
-                            height="32"
-                            viewBox="0 0 32 32"
-                            fill="none"
-                          >
-                            <rect
-                              x="0"
-                              y="0"
-                              width="32"
-                              height="32"
-                              rx="8"
-                              fill="#036141"
-                            ></rect>
-                            <path
-                              stroke="rgba(255, 255, 255, 1)"
-                              stroke-width="1.3333333333333333"
-                              stroke-linejoin="round"
-                              stroke-linecap="round"
-                              d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
-                            ></path>
-                            <ellipse
-                              cx="11.066040515899658"
-                              cy="23.199928283691406"
-                              rx="1.4000248908996582"
-                              ry="1.4000015258789062"
-                              stroke="rgba(255, 255, 255, 1)"
-                              stroke-width="1.3333333333333333"
-                              stroke-linejoin="round"
-                              stroke-linecap="round"
-                            ></ellipse>
-                            <ellipse
-                              cx="23.1998291015625"
-                              cy="23.199928283691406"
-                              rx="1.4000244140625"
-                              ry="1.4000015258789062"
-                              stroke="rgba(255, 255, 255, 1)"
-                              stroke-width="1.3333333333333333"
-                              stroke-linejoin="round"
-                              stroke-linecap="round"
-                            ></ellipse>
-                            <path
-                              stroke="rgba(255, 255, 255, 1)"
-                              stroke-width="1.3333333333333333"
-                              stroke-linejoin="round"
-                              stroke-linecap="round"
-                              d="M15.2656 15.2665L18.999 15.2665"
-                            ></path>
-                            <path
-                              stroke="rgba(255, 255, 255, 1)"
-                              stroke-width="1.3333333333333333"
-                              stroke-linejoin="round"
-                              stroke-linecap="round"
-                              d="M17.1348 17.1332L17.1348 13.3999"
-                            ></path>
-                          </svg>
-                        }
                       />
                     </div>
                 

+ 0 - 59
src/app/(public)/_components/RecommendedProducts.tsx

@@ -192,65 +192,6 @@ export default function RecommendedProducts({ recommendedSection }: newArrivalSe
                 productId={
                   item.id
                 }
-                icon={
-                  <svg
-                    xmlns="http://www.w3.org/2000/svg"
-                    width="32"
-                    height="32"
-                    viewBox="0 0 32 32"
-                    fill="none"
-                  >
-                    <rect
-                      x="0"
-                      y="0"
-                      width="32"
-                      height="32"
-                      rx="8"
-                      fill="#036141"
-                    ></rect>
-                    <path
-                      stroke="rgba(255, 255, 255, 1)"
-                      stroke-width="1.3333333333333333"
-                      stroke-linejoin="round"
-                      stroke-linecap="round"
-                      d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
-                    ></path>
-                    <ellipse
-                      cx="11.066040515899658"
-                      cy="23.199928283691406"
-                      rx="1.4000248908996582"
-                      ry="1.4000015258789062"
-                      stroke="rgba(255, 255, 255, 1)"
-                      stroke-width="1.3333333333333333"
-                      stroke-linejoin="round"
-                      stroke-linecap="round"
-                    ></ellipse>
-                    <ellipse
-                      cx="23.1998291015625"
-                      cy="23.199928283691406"
-                      rx="1.4000244140625"
-                      ry="1.4000015258789062"
-                      stroke="rgba(255, 255, 255, 1)"
-                      stroke-width="1.3333333333333333"
-                      stroke-linejoin="round"
-                      stroke-linecap="round"
-                    ></ellipse>
-                    <path
-                      stroke="rgba(255, 255, 255, 1)"
-                      stroke-width="1.3333333333333333"
-                      stroke-linejoin="round"
-                      stroke-linecap="round"
-                      d="M15.2656 15.2665L18.999 15.2665"
-                    ></path>
-                    <path
-                      stroke="rgba(255, 255, 255, 1)"
-                      stroke-width="1.3333333333333333"
-                      stroke-linejoin="round"
-                      stroke-linecap="round"
-                      d="M17.1348 17.1332L17.1348 13.3999"
-                    ></path>
-                  </svg>
-                }
               />
             </div>
           </div>

+ 0 - 59
src/app/(public)/_components/Trending.tsx

@@ -179,65 +179,6 @@ const Trending = ({ trendingSection }: newArrivalSectionProps) => {
                     {/* 购物车图标 */}
                     <OpenAddToCartModalButton
                       productId={item.id}
-                      icon={
-                        <svg
-                          xmlns="http://www.w3.org/2000/svg"
-                          width="32"
-                          height="32"
-                          viewBox="0 0 32 32"
-                          fill="none"
-                        >
-                          <rect
-                            x="0"
-                            y="0"
-                            width="32"
-                            height="32"
-                            rx="8"
-                            fill="#036141"
-                          ></rect>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
-                          ></path>
-                          <ellipse
-                            cx="11.066040515899658"
-                            cy="23.199928283691406"
-                            rx="1.4000248908996582"
-                            ry="1.4000015258789062"
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                          ></ellipse>
-                          <ellipse
-                            cx="23.1998291015625"
-                            cy="23.199928283691406"
-                            rx="1.4000244140625"
-                            ry="1.4000015258789062"
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                          ></ellipse>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M15.2656 15.2665L18.999 15.2665"
-                          ></path>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M17.1348 17.1332L17.1348 13.3999"
-                          ></path>
-                        </svg>
-                      }
                     />
                   </div>
                 </div>

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

@@ -11,7 +11,7 @@ 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,GetCategoryAttrFiltersResult } from "@components/catalog/type";
+import type{ CategoryProductsResult,PageInfo,CategoryProductNode,GetCategoryAttrFiltersResult } from "@/types/category/type";
 import {CATEGORY_PRODUCTS} from "@/graphql";
 import {transformFiltersForApi} from "@utils/helper";
 interface QueryState {

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

@@ -6,7 +6,7 @@ import Image from "next/image";
 import type{
   GetCategoryAttrFiltersResult,
   CategoryProductsResult,
-} from "@components/catalog/type";
+} from "@/types/category/type";
 import {
   GET_CATEHORY_BY_SLUG,
   GET_CATEGORY_ATTR_FILTERS,

+ 123 - 67
src/app/(public)/product/[urlkey]/[id]/page.tsx

@@ -1,26 +1,78 @@
 import { notFound } from "next/navigation";
-import { Suspense } from "react";
+import { type Metadata } from "next";
+import { Suspense, cache } from "react";
 import {
   RelatedProductSkeleton,
 } from "@/components/common/skeleton/ProductSkeleton";
 import { BASE_SCHEMA_URL, PRODUCT_TYPE } from "@/utils/constants";
-import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
+import { 
+  GET_PRODUCT_BASE_INFO,
+  GET_PRODUCT_OPTIONS
+} from "@/graphql";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import type {
   ProductOption,
-  SingleProductResponse,
+  ResolvedVariant,
+  GetProductBaseInfoResponse,
   ProductMediaType,
-} from "@components/catalog/type";
+  GetProductOptionsResponse
+} from "@/types/products/productDetail";
 import { RelatedProductsSection } from "@components/catalog/product/RelatedProductsSection";
 import Recommend from "../../_components/youmaylike/Recommend"
 import { formatFlexibleVariants } from "@/utils/variantTools";
-import ProductTwoColumnLayout from "../../_components/ProductTwoColumnLayout";
 import ProductdetailAndQuestion from "../../_components/ProductdetailAndQuestion";
+import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
+import ShopServicePanel from "../../_components/ShopServicePanel";
+import ProductDescription from "../../_components/ProductDescription";
+import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
+import ProductText from "../../_components/ProductText";
+import { ProductInforWrapper } from "../../_components/ProductInforWrapper";
+
 
 // export const dynamic = 'auto'
 // // 'auto' | 'force-dynamic' | 'error' | 'force-static'
 export const dynamic = "force-dynamic";
 
+
+const getProductBaseInfo = cache(async function(urlkey: string) {
+    return await serverGraphqlFetch<GetProductBaseInfoResponse,{urlKey: string;}>({
+
+      query: GET_PRODUCT_BASE_INFO,
+      variables:{ 
+        urlKey: urlkey 
+      },
+      takeAuthorization: false,
+      cache: 'no-cache'
+    });
+});
+
+
+export async function generateMetadata({
+  params,
+}: {
+  params: Promise<{ urlkey: string;  id: string; }>;
+}): Promise<Metadata> {
+    const { urlkey } = await params;
+    const { data: productData } = await getProductBaseInfo(urlkey);
+
+    const product = productData?.product;
+
+    const metadataRes = {
+        title: '',
+        description: ''
+    };
+    if(product) {
+        metadataRes.title = product.metaTitle || product.name;
+        metadataRes.description = product.metaDescription || product.description;
+    } else {
+        metadataRes.title = 'Product';
+        metadataRes.description = '';
+    }
+
+    return metadataRes;
+}
+
+
 //动态路由中的参数 -- params
 export default async function ProductPage({
   params,
@@ -30,22 +82,18 @@ export default async function ProductPage({
 }) {
   const { urlkey } = await params;
   
-  const resProductData = await serverGraphqlFetch<SingleProductResponse,{urlKey: string;}>({
-      // urlKey, // 产品名称
-      query: GET_PRODUCT_BY_URL_KEY, // gql查询语句
-      variables:{ 
-        urlKey: urlkey 
-      },
-      takeAuthorization: false,
-      cache: 'no-cache'
-      
-  });
-  console.log("productCustomerFeatures----------------------",resProductData);
+  const resProductData = await getProductBaseInfo(urlkey);
+
   const product =  resProductData.data?.product;
   const fetchProductError = resProductData.error;
   if (fetchProductError && fetchProductError.extensions.status === 404) return notFound();
   if(!product) throw new Error(fetchProductError?.message ?? 'Get product data failed');
  
+  const mediaImgs = (product?.images?.edges ?? []).map(
+    (edge: { node: ProductMediaType }) => {
+      return edge.node;
+    },
+  );
 
   const questionTotal = 73;
   
@@ -59,16 +107,46 @@ export default async function ProductPage({
     sku: product?.sku,
   };
 
-  const mediaImgs = (product?.images?.edges ?? []).map(
-    (edge: { node: ProductMediaType }) => {
-      return edge.node;
-    },
-  );
-  const productOptions: ProductOption[] = product?.productOptions
-    ? JSON.parse(product?.productOptions)
-    : [];
 
-  const flexibleVariants = formatFlexibleVariants(product?.flexibleVariants);
+  // 获取产品选项和变体
+  const productOptionsRes = serverGraphqlFetch<GetProductOptionsResponse,{id: number;}>({
+      query: GET_PRODUCT_OPTIONS,
+      variables:{ 
+        id: product._id 
+      },
+      takeAuthorization: false,
+      cache: 'no-cache'
+  }).then((res) => {
+      const resData = res.data?.product;
+      let errorMsg = null;
+      let productOptions: ProductOption[] = [];
+      let flexibleVariants: ResolvedVariant[] = []; 
+      if(!res.error) {
+        productOptions = resData?.productOptions
+          ? JSON.parse(resData.productOptions)
+          : [];
+
+        flexibleVariants = formatFlexibleVariants(resData?.flexibleVariants);
+          
+      } else {
+          errorMsg = res.error.message || 'Failed to get options for this product.';
+      }
+
+      return {
+        err: errorMsg,
+        productOptions,
+        flexibleVariants
+        
+      };
+  }).catch((err:unknown) => {
+      const productOptions: ProductOption[] = [];
+      const flexibleVariants: ResolvedVariant[] = [];
+      return {
+        err: err instanceof Error ?  err.message : 'Failed to get options for this product.',
+        productOptions,
+        flexibleVariants
+      };
+  });
 
 
 
@@ -80,50 +158,28 @@ export default async function ProductPage({
         }}
         type="application/ld+json"
       />
-      {/* <div className="w-full">
-        <Suspense fallback={<HeroCarouselShimmer />}>
-          <ProductMedia mediaData={mediaImgs} />
-        </Suspense>
+      <div className="flex items-start justify-between gap-22 max-w-300 mx-auto">
+          <div className="sticky top-40 z-1">
+              <ProductMedia mediaData={mediaImgs} />
+          </div>
+          <div className="max-w-124">
+                <h3 className="text-ly-14 text-black  leading-ly-22">
+                    {product.name}
+                </h3>
+                <ProductText productText={product.shortDescription || ""} />
+
+                <ProductInforWrapper 
+                    productBaseInfo={product}
+                    optionsVariantsPromise={productOptionsRes}
+                />
+
+                <ShopServicePanel />
+                <ProductDescription htmlString={product.feature || ""} />
+
+                <ProductDetail detail={product.description || ""} />
+          </div>
       </div>
 
-      <div className="">
-        <Suspense fallback={<ProductDetailSkeleton />}>
-          <ProductInformation
-            key={product._id}
-            productId={product._id}
-            name={product?.name || ""}
-            productOptions={productOptions}
-            flexibleVariants={flexibleVariants}
-            isSaleable={product.isSaleable}
-            shortDescription={product.shortDescription || ""}
-          />
-          <ShopServicePanel />
-
-         <ProductDescription   htmlString={product?.feature || ""} />
-          <SwitchButton
-            productId={"3792"} //|| String(product._id)
-
-            questionTotal={questionTotal}
-            questionList={["aaa"]}
-          />
-          <ProductDetail detail={product.description || ""} />
-          
-        </Suspense>
-        <Recommend/>
-      </div> */}
-        {/* 左右两栏滚动跟随布局(客户端组件) */}
-      <ProductTwoColumnLayout
-        mediaImgs={mediaImgs}
-        productId={product._id}
-        productName={product.name || ""}
-        productOptions={productOptions}
-        flexibleVariants={flexibleVariants}
-        isSaleable={product.isSaleable}
-        shortDescription={product.shortDescription || ""}
-        featureHtml={product.feature || ""}
-        descriptionHtml={product.description || ""}
-       
-      />
       <ProductdetailAndQuestion  
         productId={product._id}
         questionTotal={questionTotal}

File diff suppressed because it is too large
+ 66 - 0
src/app/(public)/product/_components/InstallmentTips.tsx


+ 4 - 4
src/app/(public)/product/_components/NewUserBanner.tsx

@@ -20,7 +20,7 @@ export default function NewUserBanner() {
             width="16"
             height="16"
             fill="#CCCCCC"
-            fill-opacity="0"
+            fillOpacity="0"
           ></rect>
           <ellipse
             cx="7.998697280883789"
@@ -51,9 +51,9 @@ export default function NewUserBanner() {
         >
           <path
             stroke="rgba(0, 0, 0, 1)"
-            stroke-width="1.5"
-            stroke-linejoin="round"
-            stroke-linecap="square"
+            strokeWidth="1.5"
+            strokeLinejoin="round"
+            strokeLinecap="square"
             d="M9 18L15 12L9 6"
           ></path>
         </svg>

+ 8 - 140
src/app/(public)/product/_components/ProductAddToCart.tsx

@@ -1,162 +1,35 @@
 "use client";
 
-import clsx from "clsx";
-import { useState, useEffect, useCallback } from "react";
-import PriceModal from "./popup/PriceModal";
-
-// 价格组件复用你项目自带的Price
-// import { Price } from "@components/theme/ui/Price";
-
-// SVG图标:左侧对话图标、右侧下拉箭头
-// const ChatSvg = () => (
-//   <svg
-//     xmlns="http://www.w3.org/2000/svg"
-//     width="24"
-//     height="24"
-//     viewBox="0 0 24 24"
-//     fill="none"
-//   >
-//     <path
-//       d="M22 12C22 17.5229 17.5229 22 12 22C9.01325 22 2 22 2 22C2 22 2 14.5361 2 12C2 6.47715 6.47717 2 12 2C17.5229 2 22 6.47715 22 12Z"
-//       stroke="rgba(0, 0, 0, 1)"
-//       stroke-width="1.5"
-//       stroke-linejoin="round"
-//       stroke-linecap="round"
-//     ></path>
-//     <path
-//       stroke="rgba(0, 0, 0, 1)"
-//       stroke-width="1.5"
-//       stroke-linejoin="round"
-//       stroke-linecap="round"
-//       d="M7 9L16 9"
-//     ></path>
-//     <path
-//       stroke="rgba(0, 0, 0, 1)"
-//       stroke-width="1.5"
-//       stroke-linejoin="round"
-//       stroke-linecap="round"
-//       d="M7 13L16 13"
-//     ></path>
-//     <path
-//       stroke="rgba(0, 0, 0, 1)"
-//       stroke-width="1.5"
-//       stroke-linejoin="round"
-//       stroke-linecap="round"
-//       d="M7 17L12 17"
-//     ></path>
-//   </svg>
-// );
-
-// const ArrowDownSvg = () => (
-//   <svg
-//     xmlns="http://www.w3.org/2000/svg"
-//     width="24"
-//     height="24"
-//     viewBox="0 0 24 24"
-//     fill="none"
-//   >
-//     <path
-//       stroke="rgba(0, 0, 0, 1)"
-//       stroke-width="1.5"
-//       stroke-linejoin="round"
-//       stroke-linecap="square"
-//       d="M18 9L12 15L6 9"
-//     ></path>
-//   </svg>
-// );
-
-// 价格数据类型定义,从父组件传入
-type VariantPriceInfo = {
-  totalNowPrice: number; // 现价 $119
-  totalLinePrice: number; // 划线原价 $170
-  save: number; // 优惠金额 $51
- 
-};
 
 export function ProductAddToCart({
   isAvailable,
   onAddToCart,
   onBuyNow,
-  priceInfo, // 父组件传入价格数据
-  currencyCode
+
 }: {
   isAvailable: boolean;
   onAddToCart: () => void;
   onBuyNow: () => void;
-  priceInfo: VariantPriceInfo; // 价格数据源
-  currencyCode:string;
-}) {
-  const btnClass =
-    "flex items-center justify-center rounded-full text-ly-16 font-bold text-white h-[46px] w-full";
-  // 滚动控制显示
-  const [isShowBar, setIsShowBar] = useState(false);
-  const SCROLL_THRESHOLD = 200;
-  const [couponModalOpen, setCouponModalOpen] = useState(false);
-  const handleScroll = useCallback(() => {
-    const scrollY = window.scrollY;
-    setIsShowBar(scrollY >= SCROLL_THRESHOLD);
-  }, []);
 
-  useEffect(() => {
-    window.addEventListener("scroll", handleScroll);
-    return () => window.removeEventListener("scroll", handleScroll);
-  }, [handleScroll]);
+}) {
 
-  // 解构价格数据
-  // const { totalNowPrice, totalLinePrice, save } = priceInfo;
-  // 是否有折扣(原价≠现价)
-  // const hasDiscount = totalLinePrice !== totalNowPrice;
 
   return (
     <div
-      className={clsx(
-        " w-full  bg-white box-border mt-8   transition-all duration-300 ease-in-out",  //fixed z-[1000] left-0 border-t border-solid border-ly-middlegray pt-3 px-4 pb-6
-        isShowBar ? "bottom-0" : "-bottom-[160px]",
-      )}
+      className= "w-full bg-white box-border mt-8"
+      
     >
-      {/* 顶部价格区域 */}
-      {/* <div className="flex items-center justify-between mb-4 w-full">
-        划线原价(左侧)
-        {hasDiscount && (
-          <Price
-            className="text-ly-gray  text-base line-through "
-            amount={String(totalLinePrice)}
-            currencyCode={currencyCode}
-          />
-        )}
 
-        <div className="flex items-center justify-end ">
-          优惠标签黄色块
-          {hasDiscount && (
-            <Price
-              className="bg-ly-gold px-2  leading-ly-22  text-sm mr-2  font-bold"
-              amount={`-${save}`}
-              currencyCode={currencyCode}
-            />
-          )}
-          现价
-          <Price
-            className="  text-base font-bold mr-1"
-            amount={String(totalNowPrice)}
-            currencyCode={currencyCode}
-          />
-          右侧下拉箭头SVG
-          <span onClick={() => setCouponModalOpen(true)}>
-            <ArrowDownSvg />
-          </span>
-        </div>
-      </div> */}
 
       {/* 底部按钮行:左侧对话图标 + 两个圆角按钮 */}
       {isAvailable ? (
         <div className="">
-          {/* 左侧对话图标 */}
-          {/* <ChatSvg /> */}
+  
           {/* Buy Now 深色绿按钮 */}
           <button
             onClick={onBuyNow}
             type="button"
-            className={clsx(btnClass, "mb-4 flex-1 bg-ly-deepgreen")}
+            className="flex items-center justify-center rounded-full text-ly-16 font-bold text-white h-[46px] w-full mb-4 flex-1 bg-ly-deepgreen"
           >
             Buy Now
           </button>
@@ -164,7 +37,7 @@ export function ProductAddToCart({
           <button
             onClick={onAddToCart}
             type="button"
-            className={clsx(btnClass, "flex-1 bg-ly-middlegreen")}
+            className="flex items-center justify-center rounded-full text-ly-16 font-bold text-white h-[46px] w-full flex-1 bg-ly-middlegreen"
           >
             Add to Cart
           </button>
@@ -174,12 +47,7 @@ export function ProductAddToCart({
           The selected options are not available!
         </p>
       )}
-      <PriceModal
-        visible={couponModalOpen}
-        onClose={() => setCouponModalOpen(false)}
-        priceInfo={priceInfo}
-        currencyCode={currencyCode}
-      />
+
     </div>
   );
 }

+ 2 - 6
src/app/(public)/product/_components/ProductDetail.tsx

@@ -1,15 +1,11 @@
 "use client";
-// import { useState } from 'react';
+import { useState } from 'react';
 export function ProductDetail({
     detail, 
-    isExpanded,
-    setIsExpanded,
 } : {
     detail:string;
-    isExpanded: boolean;
-    setIsExpanded: (v: boolean) => void;
 }) { 
-    // const [isExpanded, setIsExpanded] = useState(false);
+    const [isExpanded, setIsExpanded] = useState(false);
     return (
         <>
             <div className="box-border w-full ">

+ 71 - 0
src/app/(public)/product/_components/ProductInforWrapper.tsx

@@ -0,0 +1,71 @@
+"use client";
+
+import { useState, Suspense } from "react";
+import Big from 'big.js';
+import type { ProductOption, ResolvedVariant, ProductBaseInfo } from "@/types/products/productDetail";
+import NewUserBanner from "./NewUserBanner";
+import PromotionDiscountCardProps from "./PromotionDiscountCardProps ";
+import { ProductPrice } from "./ProductPrice";
+import { InstallmentTips } from "./InstallmentTips";
+import { ProductOptionsVariants } from "./ProductOptionsVariants";
+import { SkeletonProductOptions } from "./SkeletonProductOptions";
+
+interface PriceInfo {
+    totalNowPrice: number;
+    totalLinePrice: number;
+    save: number;
+}
+
+export function ProductInforWrapper({
+    productBaseInfo,
+    optionsVariantsPromise
+}: {
+    productBaseInfo: ProductBaseInfo; 
+    optionsVariantsPromise: Promise<{
+        err: string | null;
+        productOptions: ProductOption[];
+        flexibleVariants: ResolvedVariant[];
+    }>
+}){
+
+    const productId = productBaseInfo._id;
+    const isSaleable = productBaseInfo.isSaleable;
+    const productPrice = productBaseInfo.price ? Number(productBaseInfo.price) : 0;
+    const minimumPrice = productBaseInfo.minimumPrice ? Number(productBaseInfo.minimumPrice) : 0;
+    const [priceInfo, setPriceInfo] = useState<PriceInfo>({
+        totalNowPrice: minimumPrice,
+        totalLinePrice: productPrice,
+        save: Big(minimumPrice).minus(productPrice).toNumber()
+    });
+    const onPriceChange = (data: PriceInfo) => {
+        setPriceInfo(data);
+    };
+ 
+
+    return (<>
+        <div className="box-border w-full ">
+
+            <div className="mt-2">
+                <ProductPrice 
+                    totalNowPrice={priceInfo.totalNowPrice}
+                    totalLinePrice={priceInfo.totalLinePrice}
+                    save={priceInfo.save}
+                />
+            </div>
+
+            <InstallmentTips totalPrice={priceInfo.totalNowPrice} />
+            <NewUserBanner />
+            {/* 产品Code */}
+            <PromotionDiscountCardProps productId="product-1001" />
+        </div>
+
+        <Suspense fallback={<SkeletonProductOptions />}>
+            <ProductOptionsVariants 
+                productId={productId}
+                isSaleable={isSaleable}
+                optionsVariantsPromise={optionsVariantsPromise}
+                onPriceChange={onPriceChange}
+            />
+        </Suspense>
+    </>);
+}

+ 0 - 442
src/app/(public)/product/_components/ProductInformation.tsx

@@ -1,442 +0,0 @@
-"use client";
-
-import { useState, useMemo } from "react";
-import clsx from "clsx";
-import Big from 'big.js';
-import type { ProductOption, ResolvedVariant } from "@/components/catalog/type";
-import { ProductAddToCart } from "@/app/(public)/product/_components/ProductAddToCart";
-import { useCustomToast } from "@utils/hooks/useToast";
-import { useAddProduct } from "@utils/hooks/useAddToCart";
-import { useRouter } from "next/navigation";
-import InstallmentPopup from "./popup/InstallmentPopup";
-import Guide from "./popup/Guide";
-import ProductText from "./ProductText";
-import NewUserBanner from "./NewUserBanner";
-import PromotionDiscountCardProps from "./PromotionDiscountCardProps ";
-import Image from "next/image";
-import {useStore} from "@/utils/hooks/useConfig";
-import {currencyCountryMap} from "@/utils/constants";
-import {
-  isValueAvailable,
-  isOptionValueAvailable,
-  getFirstAvailable,
-  getAvailableVariants,
-  isCombinationAvailable,
-  findNearestAvailableVariant,
-} from "@/utils/variantTools";
-import { Price } from "@components/theme/ui/Price";
-import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-
-export function ProductInformation({
-  name,
-  productId,
-  productOptions,
-  flexibleVariants,
-  isSaleable,
-  shortDescription,
-}: {
-  name: string;
-  productId: number;
-  productOptions: ProductOption[];
-  flexibleVariants: ResolvedVariant[];
-  isSaleable: string | undefined;
-  shortDescription:string;
-}) {
-  const { appendProductToCart } = useAddProduct();
-  const { showToast } = useCustomToast();
-  const router = useRouter();
-
-  // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
-  const [lastClickedOptionId, setLastClickedOptionId] = useState<number>(
-    productOptions[0]?.id,
-  );
-
-  const [productQty, setProductQty] = useState(1);
-  const [modalOpen, setModalOpen] = useState(false);
-  const [guideModalOpen, setGuideModalOpen] = useState(false);
-  // 获取所有可用变体(quantity > 0)
-  const availableVariants = useMemo(() => {
-    return getAvailableVariants(flexibleVariants);
-  }, [flexibleVariants]);
-
-  // 当前选中的选项值映射:option_id -> value_id
-  const [selected, setSelected] = useState<Record<number, number>>(() => {
-    let res: Record<number, number> = {};
-
-    const defaultSelected = getFirstAvailable(productOptions, flexibleVariants);
-    if (defaultSelected) {
-      res = defaultSelected;
-    } else {
-      // 极端情况:没有任何可用组合,默认全选每组第一个(但仍需标记为不可用,由禁用逻辑处理)
-      productOptions.forEach((opt) => {
-        res[opt.id] = opt.values[0]?.id;
-      });
-    }
-    console.log("set default selected");
-    return res;
-  });
-
-  // 计算每个选项值的禁用状态
-  const disabledMap = useMemo(() => {
-    const map: Record<number, boolean> = {};
-    if (Object.keys(selected).length === 0) return map; // 没有选中的项
-
-    productOptions.forEach((option) => {
-      option.values.forEach((value) => {
-        // 规则:如果该选项组是最近点击的组,校验组中的值是否存在可用变体
-        if (option.id === lastClickedOptionId) {
-          //   map[value.id] = false;
-          map[value.id] = !isValueAvailable(value.id, availableVariants);
-        } else {
-          // 校验其他组的选项值是否可用
-          map[value.id] = !isOptionValueAvailable(
-            option.id,
-            value.id,
-            selected,
-            productOptions,
-            availableVariants,
-          );
-        }
-      });
-    });
-    return map;
-  }, [selected, lastClickedOptionId, productOptions, availableVariants]);
-
-  // 处理选项点击
-  const handleOptionClick = (optionId: number, valueId: number) => {
-    // 更新前需要判断当前选中的组合是否可以购买,如果可以购买正常更新;如果不能购买查找可以购买的组合然后更新
-    let newSelected: Record<number, number> = {
-      ...selected,
-      [optionId]: valueId,
-    };
-    const selectedValueIds = Object.values(newSelected);
-    if (!isCombinationAvailable(selectedValueIds, availableVariants)) {
-      newSelected = findNearestAvailableVariant(
-        optionId,
-        valueId,
-        { ...newSelected },
-        productOptions,
-        availableVariants,
-      );
-    }
-    setSelected({ ...newSelected });
-    // 记录当前点击的选项组
-    setLastClickedOptionId(optionId);
-  };
-
-  // 判断当前选中的组合是否可购买(用于控制购买按钮等)
-  const isCurrentSelectionAvailable = useMemo(() => {
-    let res = true;
-    if (isSaleable !== "1") {
-      res = false;
-    } else {
-      const selectedIds = Object.values(selected);
-      if (selectedIds.length !== productOptions.length) {
-        res = false;
-      } else {
-        res = isCombinationAvailable(selectedIds, availableVariants);
-      }
-    }
-    return res;
-  }, [selected, productOptions, flexibleVariants, isSaleable]);
-
-  // 获取当前选中变体和价格等信息
-  // useEffect和useMemo的执行时机不同,useMemo早于useEffect执行,如果使用useEffect初始化selected,则需要对selected做判断,当为空对象时返回占位对象
-  const currentVariantInfo: {
-    variant: ResolvedVariant | null;
-    totalLinePrice: number;
-    totalNowPrice: number;
-    save: number;
-    displayDayPrice: number;
-  } = useMemo(() => {
-    const selectedIds = Object.values(selected);
-    let totalLinePrice = 0;
-    let totalNowPrice = 0;
-    let save = 0;
-    let displayDayPrice = 0;
-    const variant = flexibleVariants.find((v) => {
-      const vIds = v.optionValues.map((ov) => ov.id);
-      return (
-        selectedIds.length === vIds.length &&
-        selectedIds.every((id) => vIds.includes(id))
-      );
-    });
-    if (selectedIds.length === 0 || variant === undefined) {
-      // 尚未初始化,返回一个安全占位对象
-      return {
-        variant: null,
-        totalLinePrice: 0,
-        totalNowPrice: 0,
-        save: 0,
-        displayDayPrice: 0,
-      };
-    }
-
-    if (variant.priceIndices && variant.priceIndices.length > 0) {
-      totalLinePrice = Big(variant.priceIndices[0].regular_min_price).times(productQty).toNumber();
-      totalNowPrice = Big(variant.priceIndices[0].min_price).times(productQty).toNumber();
-      const dayPrice = Big(totalNowPrice).div(30).toFixed(2); // 30天一期,日均成本
-      displayDayPrice = Number(dayPrice);
-    }
-    if (totalLinePrice !== totalNowPrice) {
-      save = Big(totalNowPrice).minus(totalLinePrice).toNumber();
-    }
-
-    return {
-      variant,
-      totalLinePrice:   totalLinePrice,
-      totalNowPrice:   totalNowPrice,
-      save:  save,
-      displayDayPrice: displayDayPrice,
-    };
-  }, [selected, flexibleVariants, productQty]);
-
-  const classNameOptionValueBtn =
-    "text-ly-16 leading-ly-24 border border-solid rounded-lg px-6 py-3 bg-white";
-
-  const handlerProductQty = (action: string) => {
-    if (action === "increase") {
-      setProductQty(productQty + 1);
-    } else if (action === "decrease" && productQty > 1) {
-      setProductQty(productQty - 1);
-    }
-  };
-
-  async function addProductToCart(action:string = 'addtocart') {
-        overlayLoading.start();
-        const res = await appendProductToCart({
-            productId: productId, 
-            quantity: productQty,
-            variantId: currentVariantInfo.variant?._id,
-        });
-        console.log('onAddToCart result --- ', res);
-        overlayLoading.stop();
-        if(action === 'buynow') {
-            if(res.data) {
-                const responseData = res.data;
-                if(responseData.success) {
-                    router.push('/checkout');
-                }
-            }
-        }
-  } 
-
-  const addToCartHandler = async () => {
-   
-        if(!isCurrentSelectionAvailable) {
-            showToast("The selected options are not available!", "warning");
-            return;
-        }
-
-        if(currentVariantInfo.variant) {
-            addProductToCart();
-        }
-  };
-
-
-  const buyNowHandler = () => {
-    if (!isCurrentSelectionAvailable) {
-      showToast("The selected options are not available!", "warning");
-      return;
-    }
-    if (currentVariantInfo.variant) {
-      addProductToCart("buynow");
-    }
-  };
-  const {store} = useStore();
-  const currencyCode = store.currentCurrency;
-  const currencySymbol = currencyCountryMap[currencyCode].symbol;
-  
-  
-
-  return (
-    <>
-      <div className="box-border w-full ">
-        <h3 className="text-ly-14 text-black  leading-ly-22">
-          {name}
-        </h3>
-        <ProductText ProductText={shortDescription} />
-        <div className="flex items-center mt-2">
-          <Price
-            className="text-ly-16 leading-ly-24 font-bold"
-            amount={String(currentVariantInfo.totalNowPrice)}
-            currencyCode={currencyCode}
-          />
-          {currentVariantInfo.totalLinePrice !==
-            currentVariantInfo.totalNowPrice && (
-            <>
-              <Price
-                className="text-ly-12 leading-ly-20 text-ly-gray line-through ml-1"
-                amount={String(currentVariantInfo.totalLinePrice)}
-                currencyCode={currencyCode}
-              />
-              <Price
-                className="text-ly-12 leading-ly-16 bg-ly-gold pr-1.5 pl-1.5 ml-3"
-                amount={String(currentVariantInfo.save)}
-                currencyCode={currencyCode}
-              />
-            </>
-          )}
-        </div>
-
-        <div className="w-full flex items-center gap-2 py-2">
-          <span className="text-black text-ly-12 leading-ly-20 font-normal ">
-            As Low as{" "}
-            <b>
-              {currencySymbol}
-              {currentVariantInfo.displayDayPrice}
-            </b>
-            /day with
-          </span>
-          <div className="">
-            <Image
-              src={"/image/products_details/klarna.png"}
-              width={44}
-              height={18}
-              alt="klarna"
-            />
-          </div>
-          <div className="">
-            <Image
-              src={"/image/products_details/afterpay.png"}
-              width={56}
-              height={18}
-              alt="afterpay"
-            />
-          </div>
-          <div className="">
-            <Image
-              src={"/image/products_details/paypal-expressm.png"}
-              width={56}
-              height={18}
-              alt="paypal-expressm"
-            />
-          </div>
-          <button
-            onClick={() => setModalOpen(true)}
-            className="w-4 h-4 rounded-full border border-black flex items-center justify-center text-black font-medium text-ly-12  "
-          >
-            ?
-          </button>
-          <InstallmentPopup
-            open={modalOpen}
-            onClose={() => setModalOpen(false)}
-          />
-        </div>
-        <NewUserBanner />
-        {/* 产品Code */}
-        <PromotionDiscountCardProps productId="product-1001" />
-      </div>
-      <Guide open={guideModalOpen} onClose={() => setGuideModalOpen(false)} />
-      <div className="box-border w-full  mt-4 relative">
-        <span
-          onClick={() => setGuideModalOpen(true)}
-          className="absolute text-sm font-medium leading-ly-22 underline text-black top-0 right-4"
-        >
-          Guide*
-        </span>
-        {productOptions.map((option) => (
-          <div key={option.id}>
-            <div className="text-ly-16 leading-6 font-medium ">{option.label}</div>
-
-            <div className="flex flex-wrap gap-3 mt-3">
-              {option.values.map((value) => {
-                const isSelected = selected[option.id] === value.id;
-                const isDisabled = disabledMap[value.id] || false;
-                return (
-                  <button
-                    key={value.id}
-                    type="button"
-                    onClick={() =>
-                      !isDisabled && handleOptionClick(option.id, value.id)
-                    }
-                    className={clsx(classNameOptionValueBtn, {
-                      "border-ly-green text-ly-green": isSelected,
-                      "border-black text-black": !isSelected,
-                      "opacity-40 cursor-not-allowed strokeline-bg-disabled":
-                        isDisabled,
-                    })}
-                    disabled={isDisabled}
-                  >
-                    {value.label}
-                  </button>
-                );
-              })}
-            </div>
-          </div>
-        ))}
-
-        <div className="w-full flex justify-between items-center mt-3">
-          <div className="text-ly-16 leading-6 text-black  font-medium ">Quantity*</div>
-          <div className="flex border border-solid rounded-lg">
-            <button
-              onClick={() => handlerProductQty("decrease")}
-              type="button"
-              className="flex-none w-7.5 h-11 flex items-center justify-center"
-            >
-              <svg
-                className="w-4 h-4"
-                xmlns="http://www.w3.org/2000/svg"
-                width="16"
-                height="16"
-                viewBox="0 0 16 16"
-                fill="none"
-              >
-                <path
-                  stroke="rgba(0, 0, 0, 1)"
-                  strokeWidth="1.5"
-                  strokeLinejoin="round"
-                  d="M3.33325 8L12.6666 8"
-                />
-              </svg>
-            </button>
-            <input
-              type="text"
-              className="w-15 h-11 leading-ly-36 text-center"
-              value={productQty}
-              readOnly
-            />
-            <button
-              onClick={() => handlerProductQty("increase")}
-              type="button"
-              className="flex-none w-7.5 h-11 flex items-center justify-center"
-            >
-              <svg
-                className="w-4 h-4"
-                xmlns="http://www.w3.org/2000/svg"
-                width="16"
-                height="16"
-                viewBox="0 0 16 16"
-                fill="none"
-              >
-                <path
-                  stroke="rgba(0, 0, 0, 1)"
-                  strokeWidth="1.5"
-                  strokeLinejoin="round"
-                  d="M8.02026 3.3335L8.00806 12.6668"
-                />
-                <path
-                  stroke="rgba(0, 0, 0, 1)"
-                  strokeWidth="1.5"
-                  strokeLinejoin="round"
-                  d="M3.33325 8L12.6666 8"
-                />
-              </svg>
-            </button>
-          </div>
-        </div>
-      </div>
-      <ProductAddToCart
-        isAvailable={isCurrentSelectionAvailable}
-        onAddToCart={addToCartHandler}
-        onBuyNow={buyNowHandler}
-        currencyCode={currencyCode}
-        priceInfo={{
-          totalNowPrice: currentVariantInfo.totalNowPrice,
-          totalLinePrice: currentVariantInfo.totalLinePrice,
-          save: currentVariantInfo.save,
-        }}
-      />
-    </>
-  );
-}

+ 1 - 1
src/app/(public)/product/_components/ProductMedia.tsx

@@ -5,7 +5,7 @@ import { Swiper, SwiperSlide } from "swiper/react";
 import { Pagination } from "swiper/modules";
 import type { Swiper as SwiperType } from "swiper";
 import type { PaginationOptions } from "swiper/types";
-import type { ProductMediaType } from "@/components/catalog/type";
+import type { ProductMediaType } from "@/types/products/productDetail";
 import { getImageUrl } from "@/utils/constants";
 import { useState, useRef } from "react";
 

+ 361 - 0
src/app/(public)/product/_components/ProductOptionsVariants.tsx

@@ -0,0 +1,361 @@
+"use client";
+
+import { useState, useMemo, use, useRef, useEffect } from "react";
+import clsx from "clsx";
+import type { ProductOption, ResolvedVariant } from "@/types/products/productDetail";
+import { ProductAddToCart } from "@/app/(public)/product/_components/ProductAddToCart";
+import { useCustomToast } from "@utils/hooks/useToast";
+import { useAddProduct } from "@utils/hooks/useAddToCart";
+import { useRouter } from "next/navigation";
+import Guide from "./popup/Guide";
+import {
+    isValueAvailable,
+    isOptionValueAvailable,
+    getFirstAvailable,
+    getAvailableVariants,
+    isCombinationAvailable,
+    findNearestAvailableVariant,
+    calculateSelectedVariantInfo
+} from "@/utils/variantTools";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+
+
+export function ProductOptionsVariants({
+    productId,
+    isSaleable,
+    optionsVariantsPromise,
+    onPriceChange
+}: {
+    productId: number;
+    isSaleable: string | undefined;
+    optionsVariantsPromise: Promise<{
+        err: string | null;
+        productOptions: ProductOption[];
+        flexibleVariants: ResolvedVariant[];
+    }>;
+    onPriceChange: (data: {
+        totalNowPrice: number;
+        totalLinePrice: number;
+        save: number;
+    }) => void;
+}) {
+
+    const {productOptions, flexibleVariants, err: optionsVariantsError} = use(optionsVariantsPromise);
+
+    const { appendProductToCart } = useAddProduct();
+    const { showToast } = useCustomToast();
+    const router = useRouter();
+
+
+    const hasInit = useRef(false);
+
+    // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
+    const [lastClickedOptionId, setLastClickedOptionId] = useState<number>(productOptions[0]?.id);
+
+    const [productQty, setProductQty] = useState(1);
+
+    const [guideModalOpen, setGuideModalOpen] = useState(false);
+    // 获取所有可用变体(quantity > 0)
+    const availableVariants = useMemo(() => {
+        return getAvailableVariants(flexibleVariants);
+    }, [flexibleVariants]);
+
+    // 当前选中的选项值映射:option_id -> value_id
+    const [selected, setSelected] = useState<Record<number, number>>(() => {
+        let res: Record<number, number> = {};
+
+        const defaultSelected = getFirstAvailable(productOptions, flexibleVariants);
+        if (defaultSelected) {
+            res = defaultSelected;
+        } else {
+            // 极端情况:没有任何可用组合,默认全选每组第一个(但仍需标记为不可用,由禁用逻辑处理)
+            productOptions.forEach((opt) => {
+                res[opt.id] = opt.values[0]?.id;
+            });
+        }
+        console.log("set default selected");
+        return res;
+    });
+
+    // 计算每个选项值的禁用状态
+    const disabledMap = useMemo(() => {
+        const map: Record<number, boolean> = {};
+        if (Object.keys(selected).length === 0) return map; // 没有选中的项
+
+        productOptions.forEach((option) => {
+            option.values.forEach((value) => {
+                // 规则:如果该选项组是最近点击的组,校验组中的值是否存在可用变体
+                if (option.id === lastClickedOptionId) {
+                    //   map[value.id] = false;
+                    map[value.id] = !isValueAvailable(value.id, availableVariants);
+                } else {
+                    // 校验其他组的选项值是否可用
+                    map[value.id] = !isOptionValueAvailable(
+                        option.id,
+                        value.id,
+                        selected,
+                        productOptions,
+                        availableVariants,
+                    );
+                }
+            });
+        });
+        return map;
+    }, [selected, lastClickedOptionId, productOptions, availableVariants]);
+
+    // 处理选项点击
+    const handleOptionClick = (optionId: number, valueId: number) => {
+        // 更新前需要判断当前选中的组合是否可以购买,如果可以购买正常更新;如果不能购买查找可以购买的组合然后更新
+        let newSelected: Record<number, number> = {
+        ...selected,
+        [optionId]: valueId,
+        };
+        const selectedValueIds = Object.values(newSelected);
+        if (!isCombinationAvailable(selectedValueIds, availableVariants)) {
+            newSelected = findNearestAvailableVariant(
+                optionId,
+                valueId,
+                { ...newSelected },
+                productOptions,
+                availableVariants,
+            );
+        }
+        setSelected({ ...newSelected });
+        // 记录当前点击的选项组
+        setLastClickedOptionId(optionId);
+
+        const res = calculateSelectedVariantInfo(newSelected, flexibleVariants, productQty);
+        onPriceChange({
+            totalLinePrice: res.totalLinePrice,
+            totalNowPrice: res.totalNowPrice,
+            save: res.save
+        });
+    };
+
+    // 判断当前选中的组合是否可购买(用于控制购买按钮等)
+    const isCurrentSelectionAvailable = useMemo(() => {
+        let res = true;
+        if (isSaleable !== "1") {
+            res = false;
+        } else {
+            const selectedIds = Object.values(selected);
+            if (selectedIds.length !== productOptions.length) {
+                res = false;
+            } else {
+                res = isCombinationAvailable(selectedIds, availableVariants);
+            }
+        }
+        return res;
+    }, [selected, productOptions, flexibleVariants, isSaleable]);
+
+    // 获取当前选中变体和价格等信息
+    // useEffect和useMemo的执行时机不同,useMemo早于useEffect执行,如果使用useEffect初始化selected,则需要对selected做判断,当为空对象时返回占位对象
+    const currentVariantInfo = useMemo(() => {
+
+    
+        return calculateSelectedVariantInfo(selected, flexibleVariants, productQty);
+
+        /*
+        const selectedIds = Object.values(selected);
+        let totalLinePrice = 0;
+        let totalNowPrice = 0;
+        let save = 0;
+
+        const variant = flexibleVariants.find((v) => {
+        const vIds = v.optionValues.map((ov) => ov.id);
+            return (
+                selectedIds.length === vIds.length &&
+                selectedIds.every((id) => vIds.includes(id))
+            );
+        });
+        if (selectedIds.length === 0 || variant === undefined) {
+            // 尚未初始化,返回一个安全占位对象
+            return {
+                variant: null,
+                totalLinePrice: 0,
+                totalNowPrice: 0,
+                save: 0,
+            };
+        }
+
+        if (variant.priceIndices && variant.priceIndices.length > 0) {
+            totalLinePrice = Big(variant.priceIndices[0].regular_min_price).times(productQty).toNumber();
+            totalNowPrice = Big(variant.priceIndices[0].min_price).times(productQty).toNumber();
+
+        }
+        if (totalLinePrice !== totalNowPrice) {
+            save = Big(totalNowPrice).minus(totalLinePrice).toNumber();
+        }
+
+        return {
+            variant,
+            totalLinePrice:   totalLinePrice,
+            totalNowPrice:   totalNowPrice,
+            save:  save,
+        };
+        */
+    }, [selected, flexibleVariants, productQty]);
+
+
+    const handlerProductQty = (action: string) => {
+        let newQty = 0;
+        if (action === "increase") {
+            newQty = productQty + 1;
+        } else if (action === "decrease" && productQty > 1) {
+            newQty = productQty - 1;
+        }
+        if(newQty) {
+            setProductQty(newQty);
+            const res = calculateSelectedVariantInfo(selected, flexibleVariants, newQty);
+            onPriceChange({
+                totalLinePrice: res.totalLinePrice,
+                totalNowPrice: res.totalNowPrice,
+                save: res.save
+            });
+        }
+    };
+
+    async function addProductToCart(action:string = 'addtocart') {
+        overlayLoading.start();
+        const res = await appendProductToCart({
+            productId: productId, 
+            quantity: productQty,
+            variantId: currentVariantInfo.variant?._id,
+        });
+        console.log('onAddToCart result --- ', res);
+        overlayLoading.stop();
+        if(action === 'buynow') {
+            if(res.data) {
+                const responseData = res.data;
+                if(responseData.success) {
+                    router.push('/checkout');
+                }
+            }
+        }
+    } 
+
+    const addToCartHandler = async () => {
+   
+        if(!isCurrentSelectionAvailable) {
+            showToast("The selected options are not available!", "warning");
+            return;
+        }
+
+        if(currentVariantInfo.variant) {
+            addProductToCart();
+        }
+    };
+
+
+    const buyNowHandler = () => {
+        if (!isCurrentSelectionAvailable) {
+            showToast("The selected options are not available!", "warning");
+            return;
+        }
+        if (currentVariantInfo.variant) {
+            addProductToCart("buynow");
+        }
+    };
+  
+
+    useEffect(() => {
+        if(hasInit.current) return;
+        const initialPriceInfo = calculateSelectedVariantInfo(
+            selected,
+            flexibleVariants,
+            productQty,
+        );
+        hasInit.current = true;
+        onPriceChange({
+            totalLinePrice: initialPriceInfo.totalLinePrice,
+            totalNowPrice: initialPriceInfo.totalNowPrice,
+            save: initialPriceInfo.save
+        });
+        
+    }, [
+        selected,
+        flexibleVariants,
+        productQty,
+        onPriceChange,
+    ]);
+
+    if(optionsVariantsError) {
+        return (<>
+            <div className="box-border w-full pr-4 pl-4 mt-4 relative">
+                <p className="text-ly-14 text-center text-ly-errorcolor">
+                    {optionsVariantsError}
+                </p>
+            </div>
+        </>);
+    }
+
+    return (<>
+        <div className="box-border w-full  mt-4 relative">
+            <span onClick={() => setGuideModalOpen(true)}
+                className="absolute text-sm font-medium leading-ly-22 underline text-black top-0 right-4"
+            >
+                Guide*
+            </span>
+            {productOptions.map((option) => (
+                <div key={option.id}>
+                    <div className="text-ly-16 leading-6 font-medium ">{option.label}</div>
+                    <div className="flex flex-wrap gap-3 mt-3">
+                        {option.values.map((value) => {
+                            const isSelected = selected[option.id] === value.id;
+                            const isDisabled = disabledMap[value.id] || false;
+                            return (
+                                <button key={value.id}
+                                    type="button"
+                                    onClick={() => !isDisabled && handleOptionClick(option.id, value.id)}
+                                    className={clsx("text-ly-16 leading-ly-24 border border-solid rounded-lg px-6 py-3 bg-white", {
+                                        "border-ly-green text-ly-green": isSelected,
+                                        "border-black text-black": !isSelected,
+                                        "opacity-40 cursor-not-allowed strokeline-bg-disabled": isDisabled,
+                                    })}
+                                    disabled={isDisabled}
+                                >
+                                    {value.label}
+                                </button>
+                            );
+                        })}
+                    </div>
+                </div>
+            ))}
+
+            <div className="w-full flex justify-between items-center mt-3">
+                <div className="text-ly-16 leading-6 text-black  font-medium ">Quantity*</div>
+                <div className="flex border border-solid rounded-lg">
+                    <button onClick={() => handlerProductQty("decrease")}
+                        type="button"
+                        className="flex-none w-7.5 h-11 flex items-center justify-center"
+                    >
+                        <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
+                            <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" d="M3.33325 8L12.6666 8"/>
+                        </svg>
+                    </button>
+                    <input
+                        type="text"
+                        className="w-15 h-11 leading-ly-36 text-center"
+                        value={productQty}
+                        readOnly
+                    />
+                    <button onClick={() => handlerProductQty("increase")}
+                        type="button"
+                        className="flex-none w-7.5 h-11 flex items-center justify-center"
+                    >
+                        <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
+                            <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" d="M8.02026 3.3335L8.00806 12.6668"/>
+                            <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" d="M3.33325 8L12.6666 8"/>
+                        </svg>
+                    </button>
+                </div>
+            </div>
+        </div>
+        <Guide open={guideModalOpen} onClose={() => setGuideModalOpen(false)} />
+        <ProductAddToCart
+            isAvailable={isCurrentSelectionAvailable}
+            onAddToCart={addToCartHandler}
+            onBuyNow={buyNowHandler}
+        />
+    </>);
+}

+ 41 - 0
src/app/(public)/product/_components/ProductPrice.tsx

@@ -0,0 +1,41 @@
+"use client";
+
+import { useStore } from "@utils/hooks/useConfig";
+import { Price } from "@components/theme/ui/Price";
+
+export function ProductPrice({
+    totalNowPrice,
+    totalLinePrice,
+    save
+}: {
+    totalNowPrice: number;
+    totalLinePrice: number;
+    save: number;
+}) {
+    const {store} = useStore();
+    const currencyCode = store.currentCurrency;
+
+    return (
+        <div className="flex items-center">
+            <Price
+                className="text-ly-16 leading-ly-24 font-bold"
+                amount={totalNowPrice}
+                currencyCode={currencyCode}
+            />
+            {totalLinePrice !== totalNowPrice && (
+                <>
+                <Price
+                    className="text-ly-12 leading-ly-20 text-ly-gray line-through ml-1"
+                    amount={totalLinePrice}
+                    currencyCode={currencyCode}
+                />
+                <Price
+                    className="text-ly-12 leading-ly-16 bg-ly-gold pr-1.5 pl-1.5 ml-3"
+                    amount={save}
+                    currencyCode={currencyCode}
+                />
+                </>
+            )}
+        </div>
+    );
+}

+ 4 - 17
src/app/(public)/product/_components/ProductReviewSection.tsx

@@ -1,17 +1,16 @@
-// import ReviewDetail from "@/app/(public)/product/_components/review/ReviewDetail";
+
 import ReviewModal from "@/app/(public)/product/_components/review/ReviewModal";
 import { NoReview } from "@/app/(public)/product/_components/review/NoReview";
-// import { getProductReviews } from "@utils/hooks/getProductReviews";
-import type { ProductReviewList,ProductReviewNode } from "@/types/products/review";
 import type {
   GalleryItem,
+  ProductReviewNode
 } from "@/types/products/review";
 import React from "react";
 // 组件入参TS规范
 interface ProductReviewSectionProps {
   productId: string;
   reviewTotal:number
-  reviews: ProductReviewList;
+  reviews: ProductReviewNode[];
   // modalOpen: boolean;
   closeModal: () => void;
   onOpenBaseModal: () => void;
@@ -45,24 +44,12 @@ function ProductReviewSection({
   galleryClose,
   onOpenGallery
 }: ProductReviewSectionProps) {
-  // const getAllreviews = await getProductReviews(productId)
-  console.log("productId-------------:", productId, reviews);
+
   return (
     <>
       <div className="box-border w-full">
         {reviews.length > 0 ? (
           <>
-            {/* <ReviewDetail
-              reviewTotal ={reviewTotal}
-              reviewDetails={reviews}
-              totalReview={reviews.length}
-              productId={productId}
-              // modalOpen={modalOpen}
-              onOpenBaseModal={onOpenBaseModal}
-              onClose={closeModal}
-              activeTab={activeTab}
-              onChangeTab={onChangeTab} // 关键:把切换tab的函数传给弹窗
-            /> */}
             <ReviewModal
               // open={modalOpen}
               onClose={closeModal}

+ 5 - 5
src/app/(public)/product/_components/ProductText.tsx

@@ -1,14 +1,14 @@
 "use client";
 import { useState } from "react";
 
-export default function ProductText({ ProductText }: { ProductText: string }) {
+export default function ProductText({ productText }: { productText: string }) {
   const stripHtml = (html: string) => {
   if(!html) return ""
   return html.replace(/<[^>]*>/g, "").trim()
 }
   // 模拟商品长标题
   const titleText =
-     (ProductText ? stripHtml(ProductText) : null) ||
+     (productText ? stripHtml(productText) : null) ||
     "Asteria Hair HD Lace Frontal Wigs straight A Grade Human Hair 13x4 Transparent Lace Front Wig For Women Natural Black 18-30 Inch";
   const [isExpand, setIsExpand] = useState(false);
 
@@ -40,9 +40,9 @@ export default function ProductText({ ProductText }: { ProductText: string }) {
         >
           <path
             stroke="rgba(0, 0, 0, 1)"
-            stroke-width="1.5"
-            stroke-linejoin="round"
-            stroke-linecap="square"
+            strokeWidth="1.5"
+            strokeLinejoin="round"
+            strokeLinecap="square"
             d="M18 9L12 15L6 9"
           ></path>
         </svg>

+ 0 - 144
src/app/(public)/product/_components/ProductTwoColumnLayout.tsx

@@ -1,144 +0,0 @@
-"use client";
-import { Suspense } from "react";
-import { HeroCarouselShimmer } from "@components/common/slider";
-import { useRef, useState, useEffect, useCallback } from "react";
-import { ProductDetailSkeleton } from "@/components/common/skeleton/ProductSkeleton";
-import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
-import { ProductInformation } from "./ProductInformation";
-import ShopServicePanel from "./ShopServicePanel";
-import ProductDescription from "./ProductDescription";
-// import SwitchButton from "./SwitchButton";
-import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
-import type {
-  ProductMediaType,
-  ProductOption,
-  ResolvedVariant,
-} from "@components/catalog/type";
-
-// props 把服务组件拿到的数据全部透传进来
-type Props = {
-  mediaImgs: ProductMediaType[];
-  productId: number;
-  productName: string;
-  productOptions: ProductOption[];
-  flexibleVariants: ResolvedVariant[];
-  isSaleable: string | undefined;
-  shortDescription: string;
-  featureHtml: string;
-  descriptionHtml: string;
-//   questionTotal: number;
-//   questionList: string[];
-};
-
-// 节流
-function throttle<T extends (...args: any[]) => void>(fn: T, delay: number) {
-  let last = 0;
-  return (...args: Parameters<T>) => {
-    const now = Date.now();
-    if (now - last >= delay) {
-      last = now;
-      fn(...args);
-    }
-  };
-}
-
-export default function ProductTwoColumnLayout({
-  mediaImgs,
-  productId,
-  productName,
-  productOptions,
-  flexibleVariants,
-  isSaleable,
-  shortDescription,
-  featureHtml,
-  descriptionHtml,
-//   questionTotal,
-//   questionList,
-}: Props) {
-  const wrapRef = useRef<HTMLDivElement>(null);
-  const leftRef = useRef<HTMLDivElement>(null);
-  const rightRef = useRef<HTMLDivElement>(null);
-  const [offsetY, setOffsetY] = useState(0);
-  // ========== 新增:展开收起状态,提升到这里 ==========
-  const [isExpanded, setIsExpanded] = useState(false);
-
-  const update = useCallback(() => {
-    const wrap = wrapRef.current;
-    const leftDom = leftRef.current;
-    const rightDom = rightRef.current;
-    // console.log("aaaaaaa-----------------------", wrap, leftDom, rightDom);
-
-    if (!wrap || !leftDom || !rightDom) return;
-
-    const wrapTop = wrap.getBoundingClientRect().top + window.scrollY;
-    const scrollY = window.scrollY;
-
-    const leftHeight = leftDom.offsetHeight;
-    const rightHeight = rightDom.offsetHeight;
-    const maxTranslateY = Math.max(0, rightHeight - leftHeight);
-
-    const relativeScroll = scrollY - wrapTop;
-    const targetY = Math.max(0, Math.min(maxTranslateY, relativeScroll));
-
-    setOffsetY(targetY);
-  }, []);
-
-  useEffect(() => {
-    const throttled = throttle(update, 16);
-    window.addEventListener("scroll", throttled);
-    window.addEventListener("resize", throttled);
-    update();
-    return () => {
-      window.removeEventListener("scroll", throttled);
-      window.removeEventListener("resize", throttled);
-    };
-  }, [update]);
-
-   // 监听isExpanded展开收起,重新计算
-useEffect(() => {
-    const rafId = requestAnimationFrame(() => {
-        update();
-    });
-    return () => {
-        // ✅ 清理:组件销毁前取消raf,防止在已卸载组件上setState
-        cancelAnimationFrame(rafId);
-    };
-}, [isExpanded, update]);
-  return (
-    <div ref={wrapRef} className="flex gap-22 max-w-[1200px] mx-auto ">
-      {/* 左侧:外层 self‑start,禁止flex拉伸,ref在这里拿真实高度 */}
-      <div ref={leftRef} className="flex-1 self-start">
-        {/* 仅内部盒子做位移 */}
-        <div style={{ transform: `translateY(${offsetY}px)` }}>
-          <Suspense fallback={<HeroCarouselShimmer />}>
-            <ProductMedia mediaData={mediaImgs} />
-          </Suspense>
-        </div>
-      </div>
-
-      {/* 右侧,正常高内容 */}
-      <div ref={rightRef} className="flex-1 max-w-124">
-        <Suspense fallback={<ProductDetailSkeleton />}>
-          <ProductInformation
-            key={productId}
-            productId={productId}
-            name={productName}
-            productOptions={productOptions}
-            flexibleVariants={flexibleVariants}
-            isSaleable={isSaleable}
-            shortDescription={shortDescription}
-          />
-          <ShopServicePanel />
-          <ProductDescription htmlString={featureHtml} />
-          {/* <SwitchButton
-            productId={String(productId)}
-            questionTotal={questionTotal}
-            questionList={questionList}
-          /> */}
-          <ProductDetail isExpanded={isExpanded}
-                        setIsExpanded={setIsExpanded} detail={descriptionHtml} />
-        </Suspense>
-      </div>
-    </div>
-  );
-}

+ 28 - 0
src/app/(public)/product/_components/SkeletonProductOptions.tsx

@@ -0,0 +1,28 @@
+
+
+export function SkeletonProductOptions() {
+
+
+    return (<>
+        <div className="box-border w-full pr-4 pl-4 mt-4 relative animate-pulse">
+            <div>
+
+                <div className="w-30 h-6 bg-skeletonbg"></div>
+                <div className="flex flex-wrap gap-3 mt-3">
+                    {[1,2,3,4,5,6,7,8,9,10].map((value) => {
+
+                        return (
+                            <button key={value} type="button" className="h-12 w-26 rounded-lg bg-skeletonbg"></button>
+                        );
+                    })}
+                </div>
+            </div>
+        
+
+            <div className="w-full flex justify-between items-center mt-3">
+                <div className="h-6 w-20 bg-skeletonbg"></div>
+                <div className="w-30 h-11.5 rounded-lg bg-skeletonbg"></div>
+            </div>
+        </div>
+    </>);
+}

+ 10 - 97
src/app/(public)/product/_components/SwitchButton.tsx

@@ -13,6 +13,7 @@ import type {
   // ProductReviewList,
   ProductReviewNode,
   GalleryItem,
+  ProductReviewsRestfulResponse
 } from "@/types/products/review";
 interface ReviewQuestionTabProps {
   productId: string;
@@ -59,7 +60,7 @@ export default function SwitchButton({
 }: ReviewQuestionTabProps) {
   // const [uuid, setUuidId] = useState<string>('');
   // 自己通过接口拿到,不再由父组件传入
-  const [reviewList, setReviewList] = useState<any[]>([]);
+  const [reviewList, setReviewList] = useState<ProductReviewNode[]>([]);
   const [reviewTotal, setReviewTotal] = useState<number>(0);
 
   useEffect(() => {
@@ -73,11 +74,11 @@ export default function SwitchButton({
     const fetchReviews = async () => {
       try {
         const clientUuid = getUuId();
-        const res = await clientFetch(
+        const res = await clientFetch<ProductReviewsRestfulResponse>(
           `/api/shop/products/${productId}/reviews?page=1&per_page=5&has_images=1&sort=photos&client_id=${clientUuid}`,
           { signal }, // 绑定中断信号
         );
-      console.log("res------------------------------ccc:", res);
+
 
         if (res.success) {
           setReviewList(res?.data ?? []);
@@ -116,22 +117,14 @@ export default function SwitchButton({
     console.log("【根】reviewModalOpen变更:", reviewModalOpen);
   }, [reviewModalOpen]);
 
-  // const openPhotoTabModal = useCallback(() => {
-  //   setActiveModalTab("photos"); // 打开弹窗默认图片tab
-  //   setReviewModalOpen(true);
-  // }, []);
+
   const openAllReviewModal = useCallback((e?: React.MouseEvent) => {
     e?.stopPropagation();
     console.log("【根】调用打开弹窗");
     setActiveModalTab("all");
     setReviewModalOpen(true);
   }, []);
-  // const hairImages = [
-  //   "https://cdn.alipearlhair.com/media/reviewimages/cache/6/image/150x/040ec09b1e35df139433887a97daa66f/reviewimages/7O_A6W92AW3D_87N5PR8CO.png",
-  //   "https://cdn.alipearlhair.com/media/reviewimages/cache/6/image/150x/040ec09b1e35df139433887a97daa66f/reviewimages/7O_A6W92AW3D_87N5PR8CO.png",
-  //   "https://cdn.alipearlhair.com/media/reviewimages/cache/6/image/150x/040ec09b1e35df139433887a97daa66f/reviewimages/7O_A6W92AW3D_87N5PR8CO.png",
-  //   "https://cdn.alipearlhair.com/media/reviewimages/cache/6/image/150x/040ec09b1e35df139433887a97daa66f/reviewimages/7O_A6W92AW3D_87N5PR8CO.png",
-  // ];
+
   const closeModal = useCallback(() => {
     console.log("【根】调用关闭弹窗");
     setReviewModalOpen(false);
@@ -158,7 +151,7 @@ export default function SwitchButton({
       edge.attachments.forEach((imgUrl) => {
         allGallery.push({
           reviewIndex: reviewIdx,
-          imageUrl: imgUrl,
+          imageUrl: imgUrl.url,
           review: edge,
         });
       });
@@ -178,88 +171,15 @@ const openGalleryModal = useCallback((
   const targetImg = targetEdge.attachments[clickImgIndex];
 
   const targetSlide = galleryAll.findIndex(
-    (g) => g.reviewIndex === clickReviewIndex && g.imageUrl === targetImg
+    (g) => g.reviewIndex === clickReviewIndex && g.imageUrl === targetImg.url
   );
   setGalleryData({ galleryList: galleryAll, initialSlide: targetSlide });
   setGalleryOpen(true);
 }, [buildGalleryList]);
   return (
     <div className="w-full mt-6 px-4">
-      {/* 两个Tab按钮,还原截图圆角样式 */}
-      {/* <div className="flex gap-3"> */}
-        {/* Reviews 标签 */}
-        {/* <div
-          onClick={() => setActiveTab("review")}
-          className={`px-10.5 py-3 rounded-full text-ly-14 leading-ly-22 font-normal transition-all ${
-            activeTab === "review"
-              ? "bg-black text-white"
-              : "bg-white border border-black text-black"
-          }`}
-        >
-          Reviews ({reviewTotal})
-        </div> */}
-
-        {/* Questions 标签 */}
-        {/* <div
-          onClick={() => setActiveTab("question")}
-          className={`px-[42px] py-3 rounded-full text-ly-14 leading-ly-22 font-normal transition-all ${
-            activeTab === "question"
-              ? "bg-black text-white"
-              : "bg-white border border-black text-black"
-          }`}
-        >
-          Questions ({questionTotal})
-        </div> */}
-      {/* </div> */}
 
-      {/* 分割线 */}
-      {/* <div className="w-full h-[1px] bg-black mt-4 mb-4"></div> */}
 
-     
-     
-      {/* 根据tab切换渲染对应内容 */}
-      {/* <div>
-        多层判断
-        {activeTab === "review" ? (
-          isEmptyReview ? (
-            <div className="flex flex-wrap items-center justify-center gap-4 px-10 text-ly-14 leading-ly-24 mt-6 font-normal">
-              <Image
-                src={"/image/products_details/reviews.png"}
-                width={80}
-                height={80}
-                alt="reviews"
-              />
-              <p className="text-center">
-                No reviews yet—be the first to write one.
-              </p>
-            </div>
-          ) : (
-            <ProductReviewSection
-              modalOpen={reviewModalOpen}
-              closeModal={closeModal}
-              reviews={reviewList}
-              productId={productId}
-              onOpenBaseModal={openAllReviewModal}
-              activeTab={activeModalTab}
-              onChangeTab={onTabChange}
-            />
-          )
-        ) : isEmptyQuestion ? (
-          <div className="flex flex-wrap items-center justify-center gap-4 px-10 text-ly-14 leading-ly-24 mt-6 font-normal">
-            <Image
-              src={"/image/products_details/question.png"}
-              width={80}
-              height={80}
-              alt="question"
-            />
-            <p className="text-center">
-              No relevant questions. <br /> Feel free to ask! Get quick answers.
-            </p>
-          </div>
-        ) : (
-          <FaqList faqList={mockFaqData} />
-        )}
-      </div> */}
       <div className="flex items-start gap-6 mt-6 max-w-[1200px] mx-auto">
         {/* 评论模块:固定宽度816px */}
         <div className="w-[816px] shrink-0 ">
@@ -276,19 +196,13 @@ const openGalleryModal = useCallback((
             <ReviewButton setShowForm={setShowForm} />
             </div>
           <div className="w-full flex gap-2 overflow-hidden my-4">
-              {/* {reviewList.map((edge) => ( */}
-                {/* <div key={edge.id}> */}
-                  {/*评论文字、评分等*/}
-                  {/* {edge.attachments.length > 0 && ( */}
+
                     <HairPhotoGallery
                       // review={edge}
                       fullReviewList={reviewList}
                       onOpenModal={openGalleryModal}
                     />
-                  {/* )} */}
-                {/* </div> */}
-              {/* ))} */}
-             {/* <ReviewAdd productId={productId} /> */}
+     
           </div>
            {open && (
                   <AddProductReviewForm
@@ -312,7 +226,6 @@ const openGalleryModal = useCallback((
             </div>
           ) : (
             <ProductReviewSection
-              // modalOpen={reviewModalOpen}
               reviewTotal={reviewTotal}
               closeModal={closeModal}
               reviews={reviewList}

+ 7 - 2
src/app/(public)/product/_components/review/AddRatingStar.tsx

@@ -3,7 +3,6 @@
 import { useState, useEffect } from "react";
 import clsx from "clsx";
 import { StarIcon } from "@heroicons/react/24/solid";
-import type { RatingTypes } from "@/components/catalog/type";
 
 
 export const AddRatingStar = ({
@@ -12,7 +11,13 @@ export const AddRatingStar = ({
     size = "size-6",
     className,
     onChange
-}: RatingTypes) => {
+}: {
+    length?: number;
+    value?: number;
+    size?: string;
+    className?: string;
+    onChange?: (value: number) => void;
+}) => {
 
     const [internalValue, setInternalValue] = useState(value ?? 0);
     const [hovered, setHovered] = useState<number | null>(null);

File diff suppressed because it is too large
+ 0 - 375
src/app/(public)/product/_components/review/ReviewDetail.tsx


+ 6 - 44
src/app/(public)/product/_components/review/ReviewModal.tsx

@@ -2,9 +2,9 @@
 import React, { useState, useRef, useEffect, useCallback } from "react";
 import type {
   ReviewTabType,
-  // ProductReviewList,
-  ProductReviewNode,
   GalleryItem,
+  ProductReviewNode,
+  ProductReviewsRestfulResponse
 } from "@/types/products/review";
 import ReviewImageGalleryModal from "./ReviewImageGalleryModal";
 import Image from "next/image";
@@ -48,7 +48,7 @@ async function fetchReviewApi(
   if (tab == "photos") {
     has_images = 1;
   }
-  const res = await clientFetch(
+  const res = await clientFetch<ProductReviewsRestfulResponse>(
     `/api/shop/products/${pid}/reviews?page=${pageNum}&per_page=${PAGE_SIZE}&has_images=${has_images}&sort=${tab}&client_id=${guestId}`,
     {
       signal
@@ -324,45 +324,7 @@ const tabLabelMap: Record<ReviewTabType, string> = {
         <div className="w-full flex items-center justify-center ">
           {/* //fixed inset-0 bg-black/60 z-[90]    */}
           <div className="w-full  h-full bg-white flex flex-col overflow-hidden">
-            {/* 顶部固定头部:返回 + Reviews标题 */}
-            {/* <div className="flex items-center justify-between p-4 border-b border-gray-200 shrink-0">
-              <button onClick={onClose} className="shrink-0">
-                <svg
-                  width="24"
-                  height="24"
-                  viewBox="0 0 24 24"
-                  fill="none"
-                  stroke="#000"
-                  strokeWidth="2"
-                >
-                  <polyline points="19 12 5 12"></polyline>
-                  <polyline points="12 19 5 12 12 5"></polyline>
-                </svg>
-              </button>
-              <h2 className="text-2xl font-semibold">Reviews</h2>
-              <div className="w-6 shrink-0"></div>
-            </div> */}
 
-            {/* Tab切换栏 固定 */}
-            {/* <div className="flex gap-3 px-4 py-3 border-b border-gray-200 shrink-0">
-              {(["all", "newest", "photos"] as ReviewTabType[]).map((tab) => (
-                <button
-                  key={tab}
-                  onClick={() => handleTabClick(tab)}
-                  className={`px-6 py-2 rounded-full text-lg transition-all ${
-                    activeTab === tab
-                      ? "bg-[#0F6636] text-white"
-                      : "border border-[#0F6636] text-[#0F6636]"
-                  }`}
-                >
-                  {tab === "all"
-                    ? "ALL"
-                    : tab === "newest"
-                      ? "Newest"
-                      : "Photos"}
-                </button>
-              ))}
-            </div> */}
             <div className="flex items-center justify-between  py-3 border-b border-gray-200 shrink-0">
             {/* 左侧 Reviews (数量) */}
             <div className="text-xl font-medium">Reviews ({reviewTotal})</div>
@@ -474,8 +436,8 @@ const tabLabelMap: Record<ReviewTabType, string> = {
                                 ))}
                               </span>
                             </div>
-                            {/* VIP / influencer / verified 标签 */}
-                            <div className="flex gap-2 mt-1">
+                            {/* VIP / influencer / verified 标签 接口返回没有tag字段*/}
+                            {/* <div className="flex gap-2 mt-1">
                               {item.tag && (
                                 <span className="bg-[#D9EBE4] text-[#0F6636] px-2 py-0.5 rounded text-sm flex items-center gap-1">
                                   <svg
@@ -493,7 +455,7 @@ const tabLabelMap: Record<ReviewTabType, string> = {
                                   {item.tag}
                                 </span>
                               )}
-                            </div>
+                            </div> */}
                           </div>
                         </div>
                         <div

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

@@ -211,65 +211,6 @@ const Recommend = ({
                     {/* 购物车图标 */}
                     <OpenAddToCartModalButton
                         productId={product.id}
-                      icon={
-                        <svg
-                          xmlns="http://www.w3.org/2000/svg"
-                          width="32"
-                          height="32"
-                          viewBox="0 0 32 32"
-                          fill="none"
-                        >
-                          <rect
-                            x="0"
-                            y="0"
-                            width="32"
-                            height="32"
-                            rx="8"
-                            fill="#036141"
-                          ></rect>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
-                          ></path>
-                          <ellipse
-                            cx="11.066040515899658"
-                            cy="23.199928283691406"
-                            rx="1.4000248908996582"
-                            ry="1.4000015258789062"
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                          ></ellipse>
-                          <ellipse
-                            cx="23.1998291015625"
-                            cy="23.199928283691406"
-                            rx="1.4000244140625"
-                            ry="1.4000015258789062"
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                          ></ellipse>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M15.2656 15.2665L18.999 15.2665"
-                          ></path>
-                          <path
-                            stroke="rgba(255, 255, 255, 1)"
-                            stroke-width="1.3333333333333333"
-                            stroke-linejoin="round"
-                            stroke-linecap="round"
-                            d="M17.1348 17.1332L17.1348 13.3999"
-                          ></path>
-                        </svg>
-                      }
                     />
                   </div>
                 </div>

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

@@ -12,7 +12,7 @@ import { SortByFields } from "@/utils/constants";
 import type {
   SearchProductsResponse,
   SearchProductsVariables,
-} from "@/components/catalog/type";
+} from "@/types/category/type";
 import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
 import HotSearch from "./_components/HotSearch";
 import NewProductGridItems from "@components/catalog/product/NewProductGridItems";

+ 0 - 59
src/components/catalog/product/NewProductGridItems.tsx

@@ -64,65 +64,6 @@ export default function NewProductGridItems({
                           {/* 购物车图标 */}
                           <OpenAddToCartModalButton
                               productId={product.id}
-                            icon={
-                              <svg
-                                xmlns="http://www.w3.org/2000/svg"
-                                width="32"
-                                height="32"
-                                viewBox="0 0 32 32"
-                                fill="none"
-                              >
-                                <rect
-                                  x="0"
-                                  y="0"
-                                  width="32"
-                                  height="32"
-                                  rx="8"
-                                  fill="#036141"
-                                ></rect>
-                                <path
-                                  stroke="rgba(255, 255, 255, 1)"
-                                  stroke-width="1.3333333333333333"
-                                  stroke-linejoin="round"
-                                  stroke-linecap="round"
-                                  d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
-                                ></path>
-                                <ellipse
-                                  cx="11.066040515899658"
-                                  cy="23.199928283691406"
-                                  rx="1.4000248908996582"
-                                  ry="1.4000015258789062"
-                                  stroke="rgba(255, 255, 255, 1)"
-                                  stroke-width="1.3333333333333333"
-                                  stroke-linejoin="round"
-                                  stroke-linecap="round"
-                                ></ellipse>
-                                <ellipse
-                                  cx="23.1998291015625"
-                                  cy="23.199928283691406"
-                                  rx="1.4000244140625"
-                                  ry="1.4000015258789062"
-                                  stroke="rgba(255, 255, 255, 1)"
-                                  stroke-width="1.3333333333333333"
-                                  stroke-linejoin="round"
-                                  stroke-linecap="round"
-                                ></ellipse>
-                                <path
-                                  stroke="rgba(255, 255, 255, 1)"
-                                  stroke-width="1.3333333333333333"
-                                  stroke-linejoin="round"
-                                  stroke-linecap="round"
-                                  d="M15.2656 15.2665L18.999 15.2665"
-                                ></path>
-                                <path
-                                  stroke="rgba(255, 255, 255, 1)"
-                                  stroke-width="1.3333333333333333"
-                                  stroke-linejoin="round"
-                                  stroke-linecap="round"
-                                  d="M17.1348 17.1332L17.1348 13.3999"
-                                ></path>
-                              </svg>
-                            }
                           />
                         </div>
             </Grid.Item>

+ 7 - 1
src/components/catalog/product/ProductsSection.tsx

@@ -3,7 +3,13 @@
 import Grid from "../../theme/ui/grid/Grid";
 import { getImageUrl } from "@/utils/constants";
 import { ProductCard } from "./ProductCard";
-import type { ProductsSectionProps } from "../type";
+import type { ProductCoreNode } from "@/types/products/productDetail";
+
+type ProductsSectionProps = {
+  title: string;
+  description: string;
+  products: ProductCoreNode[];
+};
 
 
 export function ProductsSection({ title, description, products }: ProductsSectionProps) {

+ 2 - 2
src/components/catalog/product/RelatedProductsSection.tsx

@@ -1,6 +1,6 @@
 import { GET_RELATED_PRODUCTS } from "@/graphql";
 import { ProductsSection } from "./ProductsSection";
-import type { SingleProductResponse } from "@components/catalog/type";
+import type { GetRelatedProductsResponse } from "@/types/products/productDetail";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 
 export async function RelatedProductsSection({
@@ -10,7 +10,7 @@ export async function RelatedProductsSection({
 }) {
     async function getRelatedProduct(urlKey: string) {
       try {
-        const {data:dataById} = await serverGraphqlFetch<SingleProductResponse,{urlKey:string;first:number;}>({
+        const {data:dataById} = await serverGraphqlFetch<GetRelatedProductsResponse,{urlKey:string;first:number;}>({
           // urlKey,
           query:GET_RELATED_PRODUCTS,
           variables:{

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

@@ -1,420 +0,0 @@
-export interface SingleProductResponse {
-  product: ProductNode | null;
-}
-export interface ProductOptionValue {
-  code: string;
-  id: number;
-  label: string;
-  meta: [] | Record<string, unknown>;  // Record<string, unknown>也可以写成 { [key: string]: unknown },表示任意对象,unknow比any更安全
-  position: number;
-}
-
-export interface ProductOption {
-  code: string;
-  id: number;
-  is_required: boolean;
-  label: string;
-  meta: [] | Record<string, unknown>;
-  position: number;
-  type: string;
-  values: Array<ProductOptionValue>;
-}
-
-export interface ProductFlexibleVariantValue {
-    code: string;
-    id: number;
-    label: string;
-    option_code: string;
-    option_id: number;
-}
-
-export interface ProductFlexibleVariantPriceIndices{
-  id: number;
-  min_price: string;
-  regular_min_price:string;
-}
-
-export interface VatiantImages {
-  id: number;
-  position: number;
-  url: string;
-}
-
-export interface ProductFlexibleVariant {
-  id: string;
-  _id: number;
-  sku: string;
-  variantImages: string | null | VatiantImages[]; 
-  optionValues: string | Array<ProductFlexibleVariantValue>
-  quantity:number;
-  price: string;
-  priceIndices: string | null | Array<ProductFlexibleVariantPriceIndices>;
-}
-
-export interface ProductMediaType{
-  id: string;
-  path: string;
-  publicPath: string;
-  position: string;
-}
-
-// 定义解析后的类型(根据最初类型裁剪)
-export type ResolvedVariant = Omit<ProductFlexibleVariant, 'optionValues' | 'variantImages | priceIndices'> & {
-  optionValues: ProductFlexibleVariantValue[];
-  variantImages: VatiantImages[]; // 去掉 string 可能性
-  priceIndices: ProductFlexibleVariantPriceIndices[];
-};
-
-export interface ProductSectionNode {
-  isSaleable: string | undefined;
-  id: string;
-  sku: string;
-  type: string;
-  urlKey?: string;
-  name?: string;
-  baseImageUrl?: string;
-  price?: string | number;
-  specialPrice?: string;
-  images?: {
-    edges: Array<{
-      node: {
-        publicPath: string;
-      };
-    }>;
-  };
-}
-
-export interface ProductVariantNode {
-  id: string;
-  sku: string;
-  name?: string;
-  price?: number;
-  attributeValues?: {
-    edges: Array<{
-      node: {
-        attribute?: {
-          code?: string;
-        };
-        value?: string;
-      };
-    }>;
-  };
-}
-
-export interface ProductNode {
-  variants: {
-    edges: Array<{ node: ProductVariantNode }>;
-  };
-  id: string;
-  _id: number;
-  sku: string;
-  type: string;
-  name?: string;
-  urlKey?: string;
-  isSaleable?: string | undefined;
-  feature?:string;
-  description?: string;
-  shortDescription?: string;
-  specialPrice?: string;
-  metaTitle?: string;
-  baseImageUrl?: string;
-  images?: {
-    edges: Array<{ node: ProductMediaType }>
-  };
-  price?: string | number | { value?: number; currencyCode?: string } | null;
-  minimumPrice?: string | number;
-  formattedPrice?: string;
-  formattedSpecialPrice?: string | null;
-  formattedMinimumPrice?: string;
-  productOptions?: string | null;
-  flexibleVariants?: Array<ProductFlexibleVariant>;
-  priceHtml?: {
-    currencyCode?: string;
-  };
-  superAttributes?: {
-    edges: Array<{ node: ProductReviewNode }>;
-  };
-  reviews?: {
-    edges: Array<{ node: ProductReviewNode }>;
-  };
-  relatedProducts?: {
-    edges: Array<{ node: ProductSectionNode }>;
-  };
-  crossSells?: {
-    edges: Array<{ node: ProductSectionNode }>;
-  };
-  upSells?: {
-    edges: Array<{ node: ProductSectionNode }>;
-  };
-}
-
-export interface ProductsResponse {
-  products: {
-    edges: Array<{ node: ProductNode }>;
-    pageInfo: {
-      endCursor: string;
-      startCursor: string;
-      hasNextPage: boolean;
-      hasPreviousPage: boolean;
-    };
-    totalCount: number;
-  };
-}
-export interface CategoryAttrFilterItem  {
-  _id: number;
-  code: string;
-  adminName: string;
-  type: string;
-  swatchType: string | null;
-  position: number | null;
-  minPrice: string;
-  maxPrice: string;
-  options: {
-    edges: Array<{
-      node: {
-        _id: string;
-        adminName: string;
-        sortOrder: number;
-        swatchValue: string | null;
-        productCount:number;
-      };
-    }>;
-  };
-};
-export interface GetCategoryAttrFiltersResult  {
-  categoryAttributeFilters: {
-    edges: Array<{ node: CategoryAttrFilterItem }>;
-    pageInfo: {
-      hasNextPage: boolean;
-      endCursor: string | null;
-    };
-  };
-};
-export type CategoryProductNode = {
-  _id: number;
-  id: string;
-  sku: string;
-  name: string;
-  type: string;
-  price: string;
-  minimumPrice: string;
-  maximumPrice: string;
-  formattedPrice: string;
-  baseImageUrl: string;
-  status: string;
-  new: string;
-  featured: string;
-  urlKey: string;
-  isInWishlist: string;
-};
-
-export type CategoryProductsResult = {
-  categoryProducts: {
-    totalCount: number;
-    description: string;
-    banner: string;
-    pageInfo:{
-      endCursor:string,
-      hasNextPage: boolean
-    }
-    products: Array<{
-      cursor: string;
-      node: CategoryProductNode;
-    }>;
-  };
-};
-export type PageInfo = {
-  endCursor: string;
-  hasNextPage: boolean;
-};
-export interface SearchProductsVariables {
-    query: string;
-    suggest?: boolean;
-    filter?: string;
-    sortKey?: string;
-    reverse?: boolean;
-    page?: number;
-    first?: number;
-    last?: number;
-    after?: string;
-    before?: string;
-    channel?: string;
-    locale?: string;
-}
-export interface SearchProductsResponse {
-  search: {
-    edges: Array<{ node: ProductNode }>;
-    pageInfo: {
-      endCursor: string;
-      startCursor: string;
-      hasNextPage: boolean;
-      hasPreviousPage: boolean;
-      currentPage: number;
-      totalPages: number;
-    };
-    totalCount: number;
-
-    searchMetadata: {
-      originalQuery: string;
-      effectiveQuery: string;
-      resultMessage: string;
-      searchInsteadMessage: string;
-    }
-  };
-}
-
-export interface ProductSectionNode {
-  id: string;
-  _id: number;
-  sku: string;
-  type: string;
-  urlKey?: string;
-  name?: string;
-  baseImageUrl?: string;
-  minimumPrice?: string | number;
-  price?: string | number;
-  specialPrice?: string;
-  images?: {
-    edges: Array<{
-      node: {
-        publicPath: string;
-      };
-    }>;
-  };
-}
-
-export type ProductsSectionProps = {
-  title: string;
-  description: string;
-  products: ProductSectionNode[];
-};
-
-export interface ProductFilterAttributeResponse {
-  attribute: {
-    id: string;
-    code: string;
-    options: {
-      edges: Array<{
-        node: {
-          id: string;
-          adminName: string;
-        };
-      }>;
-    };
-  };
-}
-
-export type ProductReview = {
-  rating: number;
-};
-
-export interface ProductReviewNode {
-  __typename: "ProductReview";
-  name: string;
-  title: string;
-  rating: number;
-  comment: string;
-  createdAt: string;
-  images?: {
-    url: string;
-    reviewId: string;
-  }[];
-  customer?: {
-    name: string;
-    imageUrl: string;
-  };
-}
-
-export interface ProductData {
-  urlKey: string;
-  variants?: {
-    edges?: {
-      node?: {
-        attributeValues?: {
-          edges?: {
-            node?: {
-              attribute?: {
-                code?: string;
-              };
-              value?: string;
-            };
-          }[];
-        };
-        id?: string;
-        priceBaseImageUrl?: string;
-        name?: string;
-        name_id?: string;
-        sku?: string;
-        type?: string;
-        color?: string;
-        size?: string;
-      };
-    }[];
-  } | null;
-  name?: string;
-  price?: { value?: number; currencyCode?: string } | number | null;
-  priceHtml?: { currencyCode?: string } | null;
-  averageRating?: number;
-  type?: string;
-  reviewCount?: number;
-  minimumPrice?: string;
-  specialPrice?: string;
-  shortDescription?: string;
-  status?: boolean;
-  id?: string;
-  description?: string;
-  configutableData?: {
-    attributes?: unknown[];
-    index?: unknown[];
-  } | null;
-}
-
-export interface AttributeType {
-  isVisibleOnFront: string;
-  id: string;
-  code: string;
-  adminName: string;
-  type: string;
-  label?: string;
-}
-
-export type additionalDataTypes = {
-  attribute: AttributeType;
-  id: string;
-  code: string;
-  label: string;
-  value: string | null;
-  admin_name: string;
-  type: string;
-};
-
-// Product review
-
-export interface RatingTypes {
-  length?: number;
-  value?: number;
-  size?: string;
-  className?: string;
-  onChange?: (value: number) => void;
-}
-
-export interface ReviewDatatypes {
-  id: string;
-  name: string;
-  title: string;
-  rating: 5;
-  status: string;
-  comment: string;
-  productId: string;
-  customerId: string;
-  createdAt: string;
-  images: {
-    url: string;
-    reviewId: string;
-  }[];
-  customer: {
-    name: string;
-    imageUrl: string;
-  };
-}
-

+ 1 - 1
src/components/common/AddToCartModal/AddToCartModal.tsx

@@ -18,7 +18,7 @@ import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import type { 
     ProductOption, 
     ResolvedVariant 
-} from "@/components/catalog/type";
+} from "@/types/products/productDetail";
 import CommonModal from "@/components/theme/ui/CommonModal";
 import ProductOptionsInAddToCartModal from "./ProductOptionsInAddToCartModal";
 import { Price } from "@components/theme/ui/Price";

+ 98 - 46
src/components/common/AddToCartModal/OpenAddToCartModalButton.tsx

@@ -1,63 +1,115 @@
 "use client";
-import { type FC, type ReactNode } from "react";
+import { type FC, type ReactNode, useRef } from "react";
+import clsx from "clsx";
 import { useAppDispatch } from "@/store/hooks";
 import { openAddToCartDialog } from "@/store/slices/addToCartDialogSlice";
-import { GET_PRODUCT_BY_ID } from "@/graphql";
+import { 
+    GET_PRODUCT_OPTIONS,
+    GET_PRODUCT_BASE_INFO
+} from "@/graphql";
 import { useLazyQuery } from "@apollo/client/react";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
-import ShoppingCartIcon from "@components/common/icons/ShoppingCartIcon";
 import { useCustomToast } from "@utils/hooks/useToast";
 import { handleApolloBusinessError } from "@/lib/ApolloErrorHandler";
 interface OpenAddToCartModalButtonProps {
-  // productUrlKey?: string;
-  productId: number;
-  icon?: ReactNode;
+    className?: string;
+    productId: number;
+    children?: ReactNode;
 }
 export const OpenAddToCartModalButton: FC<OpenAddToCartModalButtonProps> = ({
-  // productUrlKey,
-  productId,
-  icon,
+    className,
+    productId,
+    children
 }) => {
-  const dispatch = useAppDispatch();
-  // const [loading, setLoading] = useState(false);
-  const { showToast } = useCustomToast();
+    const dispatch = useAppDispatch();
+    const { showToast } = useCustomToast();
 
-  const [getContent, { loading }] = useLazyQuery(GET_PRODUCT_BY_ID, {
-    fetchPolicy: "network-only",
-  });
-  const openDialog = async () => {
-    if (loading) return;
-    try {
-      const result = await getContent({
-        variables: { id: productId },
-      });
+    // const [getContent, { loading }] = useLazyQuery(GET_PRODUCT_BY_ID, {
+    //     fetchPolicy: "network-only",
+    // });
+    const [getBaseInfo, { loading: baseInfoLoading }] = useLazyQuery(GET_PRODUCT_BASE_INFO, {
+        fetchPolicy: "network-only",
+    });
+    const [getOptions, { loading: optionsLoading }] = useLazyQuery(GET_PRODUCT_OPTIONS, {
+        fetchPolicy: "network-only",
+    });
+    const clickLockRef = useRef(false);
+    const openDialog = async () => {
+        if (clickLockRef.current) return;
+        clickLockRef.current = true;
+        try {
+            const [baseRes, optRes] = await Promise.all([
+                getBaseInfo({ variables: { id: productId }}),
+                getOptions({ variables: { id: productId }})
+            ]);
 
+            if (baseRes.error || optRes.error) {
+                let msg = '';
+                if(baseRes.error) {
+                    msg = baseRes.error.message;
+                } else if(optRes.error) {
+                    msg = optRes.error.message;
+                }
+                showToast(msg, "danger");
+            } else {
+                const baseInfo = baseRes.data?.product;
+                const optData = optRes.data?.product;
+                if(!baseInfo || !optData) {
+                    showToast("Product data is null", "danger");
+                } else {
+                    dispatch(openAddToCartDialog({
+                        ...baseInfo,
+                        ...optData
+                    }));
+                }
+            }
+            /*
+            const result = await getContent({
+                variables: { id: productId },
+            });
 
-      const productData = result?.data?.product;
-      if (result.error) {
-        showToast(result.error.message, "danger");
-      } else {
-        if (productData) {
-          dispatch(openAddToCartDialog(productData));
-        } else {
-          //提示产品数据为空
-          showToast("Product data is null", "danger");
+            console.log("product ----- ", result);
+
+            const productData = result?.data?.product;
+            if (result.error) {
+                showToast(result.error.message, "danger");
+            } else {
+                if (productData) {
+                    dispatch(openAddToCartDialog(productData));
+                } else {
+                    //提示产品数据为空
+                    showToast("Product data is null", "danger");
+                }
+            }
+            */
+        } catch (error) {
+            console.error("error ----- ", error);
+            handleApolloBusinessError(error, (err) => {
+                showToast(err.message, "danger");
+            });
+        } finally {
+            clickLockRef.current = false;
         }
-      }
-    } catch (error) {
-      console.log("error ----- ", error);
-      handleApolloBusinessError(error, (err) => {
-        showToast(err.message, "danger");
-      });
-    }
-  };
-  return (
-    <button onClick={openDialog} disabled={loading}>
-      {loading ? (
-        <LoadingSpinner colorClassName={"text-ly-green"} />
-      ) : (
-        <ShoppingCartIcon icon={icon} />
-      )}
-    </button>
+    };
+    return (
+        <button onClick={openDialog} disabled={baseInfoLoading || optionsLoading}
+            className={clsx(className ? className : "flex justify-center items-center h-8 w-8 bg-ly-middlegreen rounded-lg")}
+        >
+            {baseInfoLoading || optionsLoading ? (
+                <LoadingSpinner colorClassName={"text-ly-green"} />
+            ) : (<>
+                {children ? 
+                    children 
+                : 
+                    <svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="22.39990234375" height="22.400390625" viewBox="0 0 22.39990234375 22.400390625" fill="none">
+                        <path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.3333333333333333" strokeLinejoin="round" strokeLinecap="round"  d="M1.40137 2.7998L3.0347 2.7998L3.7347 5.5998M3.7347 5.5998L6.06803 14.9331L18.2014 14.9331L20.5347 5.5998L3.7347 5.5998Z"></path>
+                        <ellipse cx="6.067017078399658" cy="18.199806213378906" rx="1.4000248908996582" ry="1.4000015258789062" stroke="rgba(255, 255, 255, 1)" strokeWidth="1.3333333333333333" strokeLinejoin="round" strokeLinecap="round" ></ellipse>
+                        <ellipse cx="18.2008056640625" cy="18.199806213378906" rx="1.4000244140625" ry="1.4000015258789062" stroke="rgba(255, 255, 255, 1)" strokeWidth="1.3333333333333333" strokeLinejoin="round" strokeLinecap="round" ></ellipse>
+                        <path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.3333333333333333" strokeLinejoin="round" strokeLinecap="round"  d="M10.2666 10.2666L13.9999 10.2666"></path>
+                        <path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.3333333333333333" strokeLinejoin="round" strokeLinecap="round"  d="M12.1357 12.1327L12.1357 8.39941"></path>
+                    </svg>
+                }
+            </>)}
+        </button>
   );
 };

+ 1 - 1
src/components/common/AddToCartModal/ProductOptionsInAddToCartModal.tsx

@@ -3,7 +3,7 @@
 import clsx from "clsx";
 import type { 
     ProductOption, 
-} from "@/components/catalog/type";
+} from "@/types/products/productDetail";
 
 export default function ProductOptionsInAddToCartModal({
     productOptions,

+ 9 - 2
src/components/common/NextImage.tsx

@@ -3,7 +3,6 @@
 import { useState } from "react";
 import Image from "next/image";
 import { NOT_IMAGE } from "@/utils/constants";
-import type { NextImageProps } from "./type";
 import { Shimmer } from "./Shimmer";
 
 
@@ -15,7 +14,15 @@ export function NextImage({
   height,
   sizes,
   priority = false,
-}: NextImageProps) {
+}: {
+  src: string;
+  alt: string;
+  className?: string;
+  width: number;
+  height: number;
+  sizes?: string;
+  priority?: boolean;
+}) {
   const [isLoaded, setIsLoaded] = useState(false);
   const [hasError, setHasError] = useState(false);
 

+ 9 - 2
src/components/common/Rating.tsx

@@ -1,5 +1,4 @@
 import clsx from "clsx";
-import type { RatingTypes } from "./type";
 import StarIcon from "./icons/StarIcon";
 
 export const Rating = ({
@@ -9,7 +8,15 @@ export const Rating = ({
   className,
   reviewCount,
   onReviewClick,
-}: RatingTypes) => {
+}: {
+  length?: number;
+  star: number;
+  reviewCount?: number;
+  size?: string;
+  className?: string;
+  totalReview?: number;
+  onReviewClick?: () => void;
+}) => {
   const rating = star ?? 0;
   const reviewCountToShow = reviewCount ?? star > 0;
 

+ 0 - 34
src/components/common/icons/ShoppingCartIcon.tsx

@@ -1,34 +0,0 @@
-import { type ComponentProps, type ReactNode } from "react";
-import clsx from "clsx";
-
-interface ShoppingCartIconProps extends ComponentProps<"svg"> {
-  icon?: ReactNode;
-}
-
-export default function ShoppingCartIcon({
-  className,
-  icon,
-}: ShoppingCartIconProps) {
-  // 如果传了 icon → 直接返回自定义图标
-  if (icon) {
-    return <>{icon}</>;
-  }
-
-  // 没传 → 返回默认购物车图标
-  return (
-    <svg
-      className={clsx("fill-primary h-5 w-5 dark:fill-transparent", className)}
-      fill="none"
-      stroke="currentColor"
-      strokeWidth="1.5"
-      viewBox="0 0 24 24"
-      xmlns="http://www.w3.org/2000/svg"
-    >
-      <path
-        d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
-        strokeLinecap="round"
-        strokeLinejoin="round"
-      />
-    </svg>
-  );
-}

+ 0 - 61
src/components/common/skeleton/ProductSkeleton.tsx

@@ -1,66 +1,5 @@
 "use client";
 
-import { useScrollToTop } from "@/utils/hooks/useScrollTo";
-
-export const ProductPageSkeleton = () => {
-  useScrollToTop();
-
-  return (
-    <div className="min-h-screen py-8">
-      <div className="container mx-auto">
-        <ProductDetailSkeleton />
-        <RelatedProductSkeleton />
-      </div>
-    </div>
-  );
-};
-
-export const ProductDetailSkeleton = () => {
-  return (
-    <div className="flex flex-row gap-x-8">
-      <div className="flex min-w-[878px] items-center justify-center">
-        <div className="h-[736px] w-full animate-pulse rounded-2xl bg-neutral-100 dark:bg-neutral-900" />
-      </div>
-
-      <div className="flex w-full flex-col space-y-6">
-        <div className="h-12 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        <div className="h-12 w-1/2 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        <div className="flex justify-between">
-          <div className="h-6 w-1/3 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-
-          <div className="h-6 w-1/3 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-        <hr className="text-neutral-100 dark:text-neutral-900" />
-
-        <div className="space-y-4">
-          <div className="h-6 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-4 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-6 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-4 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-4 w-2/3 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-        <div className="flex gap-x-6">
-          <div className="h-10 w-24 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-24 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-24 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-24 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-        <div className="flex gap-x-6">
-          <div className="h-12 w-32 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-12 w-48 animate-pulse rounded-xl bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-
-        <hr className="text-neutral-100 dark:text-neutral-900" />
-
-        <div className="space-y-4">
-          <div className="h-10 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-      </div>
-    </div>
-  );
-};
 
 export const RelatedProductSkeleton = () => {
   return (

+ 0 - 20
src/components/common/type.ts

@@ -1,20 +0,0 @@
-export interface NextImageProps {
-  src: string;
-  alt: string;
-  className?: string;
-  width: number;
-  height: number;
-  sizes?: string;
-  priority?: boolean;
-}
-
-
-export interface RatingTypes {
-  length?: number;
-  star: number;
-  reviewCount?: number;
-  size?: string;
-  className?: string;
-  totalReview?: number;
-  onReviewClick?: () => void;
-}

+ 0 - 59
src/components/customer/accountSwiper.tsx

@@ -273,65 +273,6 @@ const AccountSwiper = () => {
                 {/* 购物车图标 */}
                 <OpenAddToCartModalButton
                   productId={item.id}
-                  icon={
-                    <svg
-                      xmlns="http://www.w3.org/2000/svg"
-                      width="32"
-                      height="32"
-                      viewBox="0 0 32 32"
-                      fill="none"
-                    >
-                      <rect
-                        x="0"
-                        y="0"
-                        width="32"
-                        height="32"
-                        rx="8"
-                        fill="#036141"
-                      ></rect>
-                      <path
-                        stroke="rgba(255, 255, 255, 1)"
-                        stroke-width="1.3333333333333333"
-                        stroke-linejoin="round"
-                        stroke-linecap="round"
-                        d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
-                      ></path>
-                      <ellipse
-                        cx="11.066040515899658"
-                        cy="23.199928283691406"
-                        rx="1.4000248908996582"
-                        ry="1.4000015258789062"
-                        stroke="rgba(255, 255, 255, 1)"
-                        stroke-width="1.3333333333333333"
-                        stroke-linejoin="round"
-                        stroke-linecap="round"
-                      ></ellipse>
-                      <ellipse
-                        cx="23.1998291015625"
-                        cy="23.199928283691406"
-                        rx="1.4000244140625"
-                        ry="1.4000015258789062"
-                        stroke="rgba(255, 255, 255, 1)"
-                        stroke-width="1.3333333333333333"
-                        stroke-linejoin="round"
-                        stroke-linecap="round"
-                      ></ellipse>
-                      <path
-                        stroke="rgba(255, 255, 255, 1)"
-                        stroke-width="1.3333333333333333"
-                        stroke-linejoin="round"
-                        stroke-linecap="round"
-                        d="M15.2656 15.2665L18.999 15.2665"
-                      ></path>
-                      <path
-                        stroke="rgba(255, 255, 255, 1)"
-                        stroke-width="1.3333333333333333"
-                        stroke-linejoin="round"
-                        stroke-linecap="round"
-                        d="M17.1348 17.1332L17.1348 13.3999"
-                      ></path>
-                    </svg>
-                  }
                 />
               </div>
             </div>

+ 0 - 53
src/components/home/ProductCarouselTheme.tsx

@@ -1,53 +0,0 @@
-import { ProductCard } from "@components/catalog/product/ProductCard";
-import type { ProductsSectionProps } from "@components/catalog/type";
-import { getImageUrl } from "@utils/constants";
-
-const Theme = ({ title, description, products }: ProductsSectionProps) => {
-  return (
-    <section className="pt-8 sm:pt-12 lg:pt-20">
-      <div className="md:max-w-4.5xl mx-auto mb-6 w-full px-0 text-center xss:mb-10 md:px-36">
-        <h2 className="mb-4 font-outfit text-xl md:text-4xl font-semibold text-black dark:text-white">
-          {title}
-        </h2>
-        <p className="text-sm md:text-base font-normal text-black/60 dark:text-neutral-300">
-          {description}
-        </p>
-      </div>
-
-      <div className="w-full pb-6 pt-1">
-        <ul className="m-0 grid grid-cols-2 justify-center gap-5 lg:gap-11.5 p-0 xss:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
-          {products.map((item, index) => {
-            const imageUrl = getImageUrl(
-              item?.baseImageUrl
-            );
-            const ProductPrice =
-              item?.type === "configurable"
-                ? (item?.minimumPrice ?? "0")
-                : (item?.price ?? "0");
-
-            return (
-              <ProductCard
-                key={item.id ?? index}
-                currency="USD"
-                imageUrl={imageUrl || ""}
-                price={String(ProductPrice)}
-                product={{
-                  _id: item._id,
-                  urlKey: item.urlKey || item.sku,
-                  name: item?.name || item.sku,
-                  id: item.id,
-                  type: item.type,
-                  isSaleable: item.isSaleable,
-                }}
-                specialPrice={""}
-                priority={index < 4}
-              />
-            );
-          })}
-        </ul>
-      </div>
-    </section>
-  );
-};
-
-export default Theme;

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

@@ -1,6 +1,6 @@
 "use client";
 import { useEffect, useState, useMemo, useRef, use } from "react";
-import type{ GetCategoryAttrFiltersResult } from "@components/catalog/type";
+import type{ GetCategoryAttrFiltersResult } from "@/types/category/type";
 import FilterPrice from "./FilterPrice";
 
 interface MobileFilterProps {

+ 0 - 63
src/graphql/catalog/fragments/ProductDetailed.ts

@@ -1,63 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const PRODUCT_DETAILED_FRAGMENT = gql`
-  fragment ProductDetailed on Product {
-    id
-    _id
-    sku
-    type
-    name
-    urlKey
-    description
-    shortDescription
-    feature
-    price
-    baseImageUrl
-    minimumPrice
-    specialPrice
-    isSaleable
-    images {
-      edges{
-        node {
-          id
-          path
-          publicPath
-          position
-        }
-      }
-    }
-    productOptions
-    flexibleVariants {
-      id
-      _id
-      sku
-      variantImages
-      optionValues
-      quantity
-      price
-      priceIndices
-    }
-    variants {
-      edges {
-        node {
-          id
-          sku
-          baseImageUrl
-        }
-      }
-    }
-    reviews {
-      edges {
-        node {
-          id
-          _id
-          rating
-          title
-          name
-          comment
-          createdAt
-        }
-      }
-    }
-  }
-`;

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

@@ -1,2 +1 @@
 export { PRODUCT_CORE_FRAGMENT } from "./ProductCore";
-export { PRODUCT_DETAILED_FRAGMENT } from "./ProductDetailed";

+ 39 - 0
src/graphql/catalog/queries/GetProductBaseInfo.ts

@@ -0,0 +1,39 @@
+import { gql,type TypedDocumentNode } from "@apollo/client";
+import type { ProductBaseInfo } from "@/types/products/productDetail";
+
+export const GET_PRODUCT_BASE_INFO: TypedDocumentNode<{product: ProductBaseInfo}> = gql`
+    query GetProductBaseInfo(
+        $urlKey: String
+        $id: ID
+    ) {
+        product(
+            urlKey: $urlKey
+            id: $id
+        ) {
+            id
+            _id
+            sku
+            type
+            name
+            urlKey
+            description
+            shortDescription
+            feature
+            price
+            baseImageUrl
+            minimumPrice
+            specialPrice
+            isSaleable
+            images {
+                edges{
+                    node {
+                        id
+                        path
+                        publicPath
+                        position
+                    }
+                }
+            }
+        }
+    }
+`;

+ 42 - 9
src/graphql/catalog/queries/GetProductById.ts

@@ -1,15 +1,48 @@
 import { gql,type TypedDocumentNode } from "@apollo/client";
-import { PRODUCT_DETAILED_FRAGMENT } from "../fragments";
-import type { SingleProductResponse } from "@/components/catalog/type";
+import type { GetProductByIdResponse } from "@/types/products/productDetail";
 /**
+ * 这个接口用GET_PRODUCT_BASE_INFO和GET_PRODUCT_OPTIONS 这两个接口代替
  * Fetch a single product by id with all details
+ * used in add to cart modal
  */
-export const GET_PRODUCT_BY_ID: TypedDocumentNode<SingleProductResponse> = gql`
-  ${PRODUCT_DETAILED_FRAGMENT}
-
-  query GetProductById($id: ID!) {
-    product(id: $id) {
-      ...ProductDetailed
+export const GET_PRODUCT_BY_ID: TypedDocumentNode<GetProductByIdResponse> = gql`
+    query GetProductById($id: ID!) {
+        product(id: $id) {
+            id
+            _id
+            sku
+            type
+            name
+            urlKey
+            description
+            shortDescription
+            feature
+            price
+            baseImageUrl
+            minimumPrice
+            specialPrice
+            isSaleable
+            images {
+                edges{
+                    node {
+                        id
+                        path
+                        publicPath
+                        position
+                    }
+                }
+            }
+            productOptions
+            flexibleVariants {
+                id
+                _id
+                sku
+                variantImages
+                optionValues
+                quantity
+                price
+                priceIndices
+            }
+        }
     }
-  }
 `;

+ 0 - 16
src/graphql/catalog/queries/GetProductByUrlKey.ts

@@ -1,16 +0,0 @@
-import { gql,type TypedDocumentNode } from "@apollo/client";
-import { PRODUCT_DETAILED_FRAGMENT } from "../fragments";
-import type { SingleProductResponse } from "@/components/catalog/type";
-/**
- * Fetch a single product by URL key with all details
- * @param urlKey - Product URL key
- */
-export const GET_PRODUCT_BY_URL_KEY: TypedDocumentNode<SingleProductResponse> = gql`
-  ${PRODUCT_DETAILED_FRAGMENT}
-
-  query GetProductById($urlKey: String!) {
-    product(urlKey: $urlKey) {
-      ...ProductDetailed
-    }
-  }
-`;

+ 28 - 0
src/graphql/catalog/queries/GetProductOptions.ts

@@ -0,0 +1,28 @@
+import { gql,type TypedDocumentNode } from "@apollo/client";
+import type { GetProductOptionsResponse } from "@/types/products/productDetail";
+/**
+ * Fetch a single product's options and flexible variants by id
+ */
+export const GET_PRODUCT_OPTIONS: TypedDocumentNode<GetProductOptionsResponse> = gql`
+    query GetProductDetail(
+        $id: ID
+        $urlKey: String
+    ) {
+        product(
+            id: $id
+            urlKey: $urlKey
+        ) {
+            productOptions
+            flexibleVariants {
+                id
+                _id
+                sku
+                variantImages
+                optionValues
+                quantity
+                price
+                priceIndices
+            }
+        }
+    }
+`;

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

@@ -11,8 +11,6 @@ export const GET_RELATED_PRODUCTS = gql`
 
   query GetRelatedProducts($urlKey: String, $first: Int) {
     product(urlKey: $urlKey) {
-      id
-      sku
       relatedProducts(first: $first) {
         edges {
           node {

+ 1 - 1
src/graphql/catalog/queries/SearchProducts.ts

@@ -1,6 +1,6 @@
 import { gql,type TypedDocumentNode } from "@apollo/client";
 import { PRODUCT_CORE_FRAGMENT } from "../fragments";
-import type { SearchProductsResponse,SearchProductsVariables } from "@/components/catalog/type";
+import type { SearchProductsResponse,SearchProductsVariables } from "@/types/category/type";
 
 /**
  * SearchProducts 支持两种分页,一种是游标分页;一种是页码分页

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

@@ -1,5 +1,4 @@
 export { GET_PRODUCTS } from "./GetProducts";
-export { GET_PRODUCT_BY_URL_KEY } from "./GetProductByUrlKey";
 export { GET_PRODUCTS_PAGINATION } from "./GetProductsPagination";
 export { GET_RELATED_PRODUCTS } from "./GetRelatedProducts";
 export { GET_PRODUCT_SWATCH_REVIEW } from "./GetProductSwatchReview";
@@ -12,5 +11,7 @@ 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_PRODUCT_BASE_INFO } from "./GetProductBaseInfo";
+export { GET_PRODUCT_OPTIONS } from  "./GetProductOptions";
 export { GET_CATEGORY_ATTR_FILTERS } from "./CategoryAttributeFilters";
 export { CATEGORY_PRODUCTS } from "./GetCategoryProducts";

+ 3 - 3
src/store/slices/addToCartDialogSlice.ts

@@ -1,9 +1,9 @@
 import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
-import type { ProductNode } from "@/components/catalog/type";
+import type { ProductBaseInfo, ProductOptionsData } from "@/types/products/productDetail";
 
 interface DialogState {
   isOpen: boolean;
-  product: ProductNode | null;
+  product: ProductBaseInfo & ProductOptionsData | null;
 }
 
 const initialState: DialogState = {
@@ -15,7 +15,7 @@ const addToCartDialogSlice = createSlice({
   name: 'addToCartDialog',
   initialState,
   reducers: {
-    openAddToCartDialog(state, action: PayloadAction<ProductNode>) {
+    openAddToCartDialog(state, action: PayloadAction<ProductBaseInfo & ProductOptionsData>) {
       state.isOpen = true;
       state.product = action.payload;
     },

+ 90 - 179
src/types/category/type.ts

@@ -1,196 +1,107 @@
-// Category List Types 
-
-
-// Category Details Types 
-
-export interface ProductOptionNode {
-  id: string;
-  adminName: string;
-  isValid?: boolean; 
-}
-
-
-export interface ProductOptionEdge {
-  node: ProductOptionNode;
+import type { ProductCoreNode } from "@/types/products/productDetail";
+
+export interface SearchProductsVariables {
+    query: string;
+    suggest?: boolean;
+    filter?: string;
+    sortKey?: string;
+    reverse?: boolean;
+    page?: number;
+    first?: number;
+    last?: number;
+    after?: string;
+    before?: string;
+    channel?: string;
+    locale?: string;
+}
+export interface SearchProductsResponse {
+  search: {
+    edges: Array<{ node: ProductCoreNode }>;
+    pageInfo: {
+      endCursor: string;
+      startCursor: string;
+      hasNextPage: boolean;
+      hasPreviousPage: boolean;
+      currentPage: number;
+      totalPages: number;
+    };
+    totalCount: number;
+
+    searchMetadata: {
+      originalQuery: string;
+      effectiveQuery: string;
+      resultMessage: string;
+      searchInsteadMessage: string;
+    }
+  };
 }
 
 
-export interface AttributeOptions {
-  edges: ProductOptionEdge[];
-}
 
-
-export interface ProductAttribute {
-  id: string;
+export interface CategoryAttrFilterItem  {
+  _id: number;
   code: string;
   adminName: string;
-  options: AttributeOptions;
-}
-export type AttributeData = ProductAttribute;
-
-
-export interface ProductReview {
-  id: string;
-  rating: number;
-  name: string;
-  title: string;
-  comment: string;
-}
-
-export interface ProductReviewEdge {
-  node: ProductReview;
-}
-
-
-
-
-
-// Product Swatch Review Types
-export interface SuperAttributeOption {
-  id: string;
-  code: string;
-  label?: string;
-  adminName?: string;
-  type?: string;
-  swatchType?: string;
-  swatchValue?: string;
-  options?: Array<{
-    id: string;
-    label?: string;
-    swatchValue?: string;
-    products?: number[];
-  }>;
-}
-
-export interface AttributeValueNode {
-  id: string;
-  code: string;
-  label?: string;
-  value?: string | null;
-  attribute?: {
-    id: string;
-    code: string;
-    adminName?: string;
-    type?: string;
-  };
-}
-
-export interface ProductSwatchReview {
-  id: string;
   type: string;
-  isSaleable?: string;
-  superAttributeOptions?: string; // JSON string
-  combinations?: string; // JSON string
-  superAttributes?: {
+  swatchType: string | null;
+  position: number | null;
+  minPrice: string;
+  maxPrice: string;
+  options: {
     edges: Array<{
-      node: SuperAttributeOption;
+      node: {
+        _id: string;
+        adminName: string;
+        sortOrder: number;
+        swatchValue: string | null;
+        productCount:number;
+      };
     }>;
   };
-  attributeValues?: {
-    edges: Array<{
-      node: AttributeValueNode;
-    }>;
-  };
-}
-
-
-
-// GraphQL Response Types
-
-export interface ProductEdge {
-  node: {
-    id: string;
-    name: string;
-    urlKey: string;
-    type: string;
-    sku: string;
-    baseImageUrl?: string;
-    minimumPrice?: string;
-    specialPrice?: string;
-    priceHtml?: {
-      regularPrice: string;
-      finalPrice: string;
-      currencyCode: string;
-    };
-  };
-}
-
-export interface ProductsResponse {
-  products: {
-    edges: ProductEdge[];
-    pageInfo?: {
+};
+export interface GetCategoryAttrFiltersResult  {
+  categoryAttributeFilters: {
+    edges: Array<{ node: CategoryAttrFilterItem }>;
+    pageInfo: {
       hasNextPage: boolean;
-      hasPreviousPage: boolean;
-      startCursor?: string;
-      endCursor?: string;
-    };
-    totalCount?: number;
-  };
-}
-
-export interface CategoryEdge {
-  node: {
-    id: string;
-    position?: number;
-    logoUrl?: string;
-    translation: {
-      name: string;
-      slug: string;
-      description?: string;
+      endCursor: string | null;
     };
   };
-}
-
-export interface CategoriesResponse {
-  categories: {
-    edges: CategoryEdge[];
-  };
-}
-
-export interface RelatedProductsResponse {
-  relatedProducts: {
-    edges: ProductEdge[];
-  };
-}
-
-
-// Review Types
-
-export interface ReviewInput {
-  productId: number;
-  title: string;
-  comment: string;
-  rating: number;
-  name: string;
-  email: string;
-  attachments?: string;
-}
-
-export interface ReviewFieldInputs {
-  input: ReviewInput;
-}
-
-export interface ProductReview {
+};
+export type CategoryProductNode = {
+  _id: number;
   id: string;
+  sku: string;
   name: string;
-  title: string;
-  rating: number;
-  comment: string;
+  type: string;
+  price: string;
+  minimumPrice: string;
+  maximumPrice: string;
+  formattedPrice: string;
+  baseImageUrl: string;
   status: string;
-}
-
-export interface CreateProductReviewData {
-  createProductReview: {
-    success: boolean;
-    review: ProductReview;
+  new: string;
+  featured: string;
+  urlKey: string;
+  isInWishlist: string;
+};
+
+export type CategoryProductsResult = {
+  categoryProducts: {
+    totalCount: number;
+    description: string;
+    banner: string;
+    pageInfo:{
+      endCursor:string,
+      hasNextPage: boolean
+    }
+    products: Array<{
+      cursor: string;
+      node: CategoryProductNode;
+    }>;
   };
-}
-
-export interface CreateProductReviewVariables {
-  input: ReviewInput;
-}
-
-export interface CreateProductReviewOperation {
-  data: CreateProductReviewData;
-  variables: CreateProductReviewVariables;
-}
+};
+export type PageInfo = {
+  endCursor: string;
+  hasNextPage: boolean;
+};

+ 139 - 20
src/types/products/productDetail.ts

@@ -1,20 +1,139 @@
-/**
- * 单条评论主体 Node 结构 ProductReview
- */
-export interface ProductReviewNode {
-  name: string; // 评论人昵称
-  title: string; // 标题/发长
-  rating: number; // 星级 1-5
-  comment: string; // 评论正文
-  createdAt: string; // ISO时间字符串
-  __typename: "ProductReview";
-}
-export interface ProductReviewEdge {
-  node: ProductReviewNode;
-  __typename: "ProductReviewEdge";
-}
-
-/**
- * 整体:评论数组 = ProductReviewEdge[]
- */
-export type ProductReviewList = ProductReviewEdge[];
+export interface ProductOptionValue {
+  code: string;
+  id: number;
+  label: string;
+  meta: [] | Record<string, unknown>;  // Record<string, unknown>也可以写成 { [key: string]: unknown },表示任意对象,unknow比any更安全
+  position: number;
+}
+
+export interface ProductOption {
+  code: string;
+  id: number;
+  is_required: boolean;
+  label: string;
+  meta: [] | Record<string, unknown>;
+  position: number;
+  type: string;
+  values: Array<ProductOptionValue>;
+}
+
+export interface ProductFlexibleVariantValue {
+    code: string;
+    id: number;
+    label: string;
+    option_code: string;
+    option_id: number;
+}
+
+export interface ProductFlexibleVariantPriceIndices{
+  id: number;
+  min_price: string;
+  regular_min_price:string;
+}
+
+export interface VatiantImages {
+  id: number;
+  position: number;
+  url: string;
+}
+
+export interface ProductFlexibleVariant {
+  id: string;
+  _id: number;
+  sku: string;
+  variantImages: string | null | VatiantImages[]; 
+  optionValues: string | Array<ProductFlexibleVariantValue>
+  quantity:number;
+  price: string;
+  priceIndices: string | null | Array<ProductFlexibleVariantPriceIndices>;
+}
+
+export interface ProductMediaType{
+  id: string;
+  path: string;
+  publicPath: string;
+  position: string;
+}
+
+// 定义解析后的类型(根据最初类型裁剪)
+export type ResolvedVariant = Omit<ProductFlexibleVariant, 'optionValues' | 'variantImages | priceIndices'> & {
+  optionValues: ProductFlexibleVariantValue[];
+  variantImages: VatiantImages[]; // 去掉 string 可能性
+  priceIndices: ProductFlexibleVariantPriceIndices[];
+};
+
+export interface ProductCoreNode {
+  id: string;
+  _id: number;
+  sku: string;
+  type: string;
+  name: string;
+  price: string;
+  formattedPrice: string;
+  formattedSpecialPrice: string | null;
+  formattedMinimumPrice: string;
+  urlKey: string;
+  baseImageUrl: string;
+  minimumPrice: string;
+  specialPrice: string | null;
+  isSaleable: string | undefined;
+  // images?: {
+  //   edges: Array<{
+  //     node: {
+  //       publicPath: string;
+  //     };
+  //   }>;
+  // };
+}
+
+
+export interface ProductBaseInfo {
+    id: string;
+    _id: number;
+    sku: string;
+    type: string;
+    name: string;
+    urlKey: string;
+    description: string;
+    shortDescription: string;
+    feature: string;
+    price: string;
+    baseImageUrl: string;
+    minimumPrice: string;
+    specialPrice: string | null;
+    isSaleable: string | undefined;
+    metaTitle?: string;
+    metaKeywords?: string;
+    metaDescription?: string;
+    images: {
+        edges: Array<{ 
+            node: ProductMediaType
+        }>;
+    }
+}
+
+export interface ProductOptionsData {
+  productOptions: string | null;
+  flexibleVariants: Array<ProductFlexibleVariant>;
+}
+
+export interface GetRelatedProductsResponse {
+  product: {
+    relatedProducts: {
+        edges: Array<{ node: ProductCoreNode }>;
+      };
+  }
+}
+export interface GetProductBaseInfoResponse {
+    product: ProductBaseInfo;
+}
+
+export type GetProductByIdDetail  = ProductBaseInfo & ProductOptionsData;
+export interface GetProductByIdResponse  {
+    product: GetProductByIdDetail | null;
+}
+
+
+export interface GetProductOptionsResponse {
+    product: ProductOptionsData;
+}

+ 19 - 9
src/types/products/review.ts

@@ -4,22 +4,32 @@ export interface ProductReviewNode {
   name: string;
   title: string;
   rating: number;
+  like_count: number;
   comment: string;
+  status: string;
   created_at: string;
   updated_at: string;
-  attachments: string[]; // 评论图片数组,空=无图
   migrated_from_asteria_id:number;
-  isVip: boolean;
-  tag: "influencer" | "verified" | "";
-  like_count: number;
-  liked:boolean;
+  liked: boolean;
+  attachments: Array<{
+    type: string;
+    url: string;       
+  }>; // 评论图片数组,空=无图
 }
 
-export interface ProductReviewEdge {
-  node: ProductReviewNode;
-  __typename: "ProductReviewEdge";
+export interface ProductReviewsRestfulResponse {
+  data: ProductReviewNode[];
+  message: string;
+  pagination: {
+    current_page: number;
+    per_page: number;
+    total: number; 
+    last_page: number;
+    has_more: boolean;
+  };
+  success: boolean;
 }
-export type ProductReviewList = ProductReviewEdge[];
+
 
 // 图库弹窗一维平铺图片项
 export interface GalleryItem {

+ 0 - 345
src/types/types.ts

@@ -11,19 +11,6 @@ export interface LogoutData {
 }
 
 
-export type Maybe<T> = T | null;
-
-export type Connection<T> = {
-  edges: Array<Edge<T>>;
-};
-
-export type Edge<T> = {
-  node: T;
-};
-
-
-
-
 /**
  * Sorting & filtration constants
  */
@@ -69,290 +56,6 @@ export type getFilterAttributeTypes = {
 
 
 
-export type selectedPaymentMethodType = {
-  id: string;
-  method: string;
-  methodTitle: string;
-  cartId: string;
-};
-
-
-export type Image = {
-  url: string;
-  altText: string;
-  width: number;
-  height: number;
-};
-
-export type ImageInfo = {
-  url: string;
-  type: string;
-  path: string;
-  productId: number;
-  altText: string;
-};
-export type Product = Omit<BagistoProductInfo, "variants" | "images"> & {
-  products: ProductDetailsInfo[];
-  paginatorInfo: {
-    count: number;
-    currentPage: number;
-    lastPage: number;
-    total: number;
-  };
-};
-
-export type ProductDetailsInfo = Omit<
-  BagistoProductInfo,
-  "variants" | "images"
-> & {
-  variants: ProductVariant[];
-  images: ImageInfo[];
-};
-
-export type ProductOption = {
-  id: string;
-  name: string;
-  // values: string[];
-  displayName: string;
-  values: ProductOptionValues[];
-};
-
-export type ProductOptionValues = {
-  label: string;
-  hexColors?: string[];
-};
-
-export type ProductVariant = {
-  id: string;
-  label: string;
-  sku: string;
-  availableForSale: boolean;
-  options: {
-    id: string;
-    label: string;
-    products: [number];
-  }[];
-  selectedOptions: {
-    name: string;
-    value: string;
-  }[];
-  price: string;
-};
-
-export type SEO = {
-  title: string;
-  description: string;
-};
-
-
-
-export type BagistoCollection = {
-  handle: string;
-  title: string;
-  description: string;
-  seo: SEO;
-  urlPath?: string;
-  updatedAt: string;
-  id: string;
-  logoPath?: string;
-  logoUrl?: string;
-  name: string;
-  slug: string;
-};
-
-export type BagistoCollectionMenus = {
-  id: string;
-  logoPath?: string;
-  logoUrl?: string;
-  name: string;
-  slug: string;
-};
-
-export type BagistoProduct = {
-  name: string;
-  longDescription?: string;
-  urlKey: string;
-  slug?: string;
-  path?: string;
-  price: ProductPrice;
-  id: string;
-  handle: string;
-  availableForSale: boolean;
-  title: string;
-  description: string;
-  descriptionHtml: string;
-  options: ProductOption[];
-  variants: Connection<ProductVariant>;
-  featuredImage: Image;
-  images: Connection<Image>;
-  seo: SEO;
-  tags: string[];
-  updatedAt: string;
-};
-
-export type BagistoProductInfo = {
-  name: string;
-  longDescription?: string;
-  urlKey: string;
-  type: string;
-  status: boolean;
-  slug?: string;
-  width: string;
-  height: string;
-  path?: string;
-  metaTitle: string;
-  metaDescription: string;
-  price: ProductPrice;
-  id: string;
-  _id: number;
-  handle: string;
-  availableForSale: boolean;
-  title: string;
-  description: string;
-  shortDescription: string;
-  descriptionHtml: string;
-  options: ProductOption[];
-  cacheGalleryImages: {
-    smallImageUrl: string;
-    mediumImageUrl: string;
-    largeImageUrl: string;
-    originalImageUrl: string;
-  }[];
-  priceHtml: {
-    regularPrice: string;
-    currencyCode: string;
-    formattedRegularPrice: string;
-    finalPrice: string;
-    formattedFinalPrice: string;
-  };
-  variants: Array<ProductVariant>;
-  featuredImage: Image;
-  images: Array<ImageInfo>;
-  productFlats: {
-    id: string;
-    name: string;
-    description: string;
-    metaTitle: string;
-    metaDescription: string;
-    width: string;
-    height: string;
-  }[];
-  relatedProducts: RelatedProducts[];
-  inventories: {
-    qty: string;
-  }[];
-  configutableData: {
-    attributes: ConfigurableProductData[];
-    index: ConfigurableProductIndexData[];
-  };
-  seo: SEO;
-  tags: string[];
-  updatedAt: string;
-};
-
-export type ConfigurableProductIndexData = {
-  id: string;
-  attributeOptionIds: {
-    attributeId: string;
-    attributeCode: string;
-    attributeOptionId: string;
-  }[];
-};
-export type RealatedImageArray = {
-  url: string;
-};
-
-export type RelatedProducts = {
-  id: string;
-  name: string;
-  urlKey: string;
-  type: string;
-  priceHtml: {
-    regularPrice: string;
-    currencyCode: string;
-    finalPrice: string;
-  };
-  images?: RealatedImageArray[];
-  cacheGalleryImages: {
-    smallImageUrl: string;
-    mediumImageUrl: string;
-    largeImageUrl: string;
-    originalImageUrl: string;
-  }[];
-};
-
-export type ConfigurableProductData = {
-  id: string;
-  code: string;
-  label: string;
-  availableForSale: boolean;
-  options: {
-    id: string;
-    code: string;
-    label: string;
-    swatchType: string;
-    swatchValue: string;
-    products: [number];
-  }[];
-};
-
-export type AttributeOptionNode = {
-  id: string;
-  adminName?: string;
-  label?: string;
-  isValid: boolean;
-};
-
-export type AttributeOptionEdge = {
-  node: AttributeOptionNode;
-};
-
-export type AttributeData = {
-  id: string;
-  code: string;
-  label?: string;
-  options: AttributeOptionNode[] | {
-    edges: AttributeOptionEdge[];
-  };
-};
-
-export type ProductPrice = {
-  value: number;
-  currencyCode?: "USD" | "EUR" | "ARS" | string;
-  retailPrice?: number;
-  salePrice?: number;
-  listPrice?: number;
-  extendedSalePrice?: number;
-  extendedListPrice?: number;
-};
-
-
-
-
-export type AddressDataTypes = {
-  id: string;
-  firstName: string;
-  lastName: string;
-  companyName: string;
-  address: string;
-  city: string;
-  state: string;
-  countryName: string;
-  postcode: string;
-  email: string;
-  country: string;
-  addressType: string;
-  vatId: string;
-  phone: string;
-  stateName: string;
-  defaultAddress: boolean;
-  useForShipping: boolean;
-};
-
-
-
-
-
 export type CreateUserVariables = {
   input: {
       firstName: string;
@@ -412,19 +115,6 @@ export type BagistoUser = {
 
 
 
-export type SuperAttribute = {
-  attributeId: number;
-  attributeOptionId: number;
-};
-
-
-export type TranslationsTypes = {
-  id: string;
-  themeCustomizationId: number;
-  localeCode: string;
-  type: string;
-  options: OptionDataTypes;
-};
 
 export type FilterDataTypes = {
   key: string;
@@ -432,41 +122,6 @@ export type FilterDataTypes = {
   __typename: string;
 };
 
-export type ThemeOptions = {
-  url: string;
-  title: string;
-  sortOrder: string;
-};
-export type OptionDataTypes = {
-  title: string;
-  css: string;
-  html: string;
-  images: ImagesDataType[];
-  filters: FilterDataTypes[];
-  column_1: ThemeOptions[];
-  column_2: ThemeOptions[];
-  column_3: ThemeOptions[];
-  services: {
-    service_icon: string;
-    description: string;
-    title: string;
-  }[];
-};
-
-export type ImagesDataType = {
-  title: string;
-  link: string;
-  image: string;
-  imageUrl: string;
-};
-
-
-
-export type InputData = {
-  key: string;
-  value: string | number;
-};
-
 
 export type IconSvgProps = SVGProps<SVGSVGElement> & {
   size?: number;

+ 1 - 1
src/utils/hooks/useSearchProduct.ts

@@ -2,7 +2,7 @@
 import { useState, useRef } from "react";
 import { useApolloClient } from "@apollo/client/react";
 import { SEARCH_PRODUCTS } from "@/graphql";
-import type { SearchProductsResponse, SearchProductsVariables } from "@/components/catalog/type";
+import type { SearchProductsResponse, SearchProductsVariables } from "@/types/category/type";
 
 interface SearchProductResult {
   data: SearchProductsResponse | null;

+ 42 - 2
src/utils/variantTools.ts

@@ -1,3 +1,4 @@
+import Big from "big.js";
 import type { 
     ProductOption, 
     VatiantImages,
@@ -5,7 +6,7 @@ import type {
     ProductFlexibleVariant,
     ProductFlexibleVariantValue,
     ProductFlexibleVariantPriceIndices
-} from "@/components/catalog/type";
+} from "@/types/products/productDetail";
 
 
 /**
@@ -189,4 +190,43 @@ export function formatFlexibleVariants(originData: ProductFlexibleVariant[] | un
 // 获取可以购买的变体
 export function getAvailableVariants(flexibleVariants: ResolvedVariant[]) {
     return flexibleVariants.filter((variant) => variant.quantity > 0);
-}
+}
+
+export function calculateSelectedVariantInfo(selected: Record<number, number>, flexibleVariants: ResolvedVariant[], productQty: number) {
+    const selectedIds = Object.values(selected);
+    let totalLinePrice = 0;
+    let totalNowPrice = 0;
+    let save = 0;
+
+    const variant = flexibleVariants.find((v) => {
+        const vIds = v.optionValues.map((ov) => ov.id);
+        return (
+            selectedIds.length === vIds.length &&
+            selectedIds.every((id) => vIds.includes(id))
+        );
+    });
+    if (selectedIds.length === 0 || variant === undefined) {
+        // 尚未初始化,返回一个安全占位对象
+        return {
+            variant: null,
+            totalLinePrice: 0,
+            totalNowPrice: 0,
+            save: 0,
+        };
+    }
+
+    if (variant.priceIndices && variant.priceIndices.length > 0) {
+        totalLinePrice = Big(variant.priceIndices[0].regular_min_price).times(productQty).toNumber();
+        totalNowPrice = Big(variant.priceIndices[0].min_price).times(productQty).toNumber();
+    }
+    if (totalLinePrice !== totalNowPrice) {
+        save = Big(totalNowPrice).minus(totalLinePrice).toNumber();
+    }
+
+    return {
+        variant,
+        totalLinePrice: totalLinePrice,
+        totalNowPrice: totalNowPrice,
+        save: save,
+    };
+};