29 Комити 47eb377206 ... ce15eaa76d

Аутор SHA1 Порука Датум
  zhangzf ce15eaa76d Merge branch 'zzf-account' into dev пре 1 недеља
  zhangzf 7e4fafd6c6 根据eslint修改报错 пре 1 недеља
  zhangzf 1d154a8b3a 详情页评论 пре 1 недеља
  zhangzf 5507b8bfa8 首页接口 пре 1 недеља
  zgl b0042e5883 Merge branch 'zzf-account' of http://gogs.hnwmzp.cn/fogwind/nshop-front-m into zzf-account пре 2 недеља
  zgl f3090f3603 添加_id参数 пре 2 недеља
  zhangzf 40ed5a380a Merge branch 'zzf-account' of http://gogs.hnwmzp.cn/fogwind/nshop-front-m into zzf-account пре 2 недеља
  zhangzf e547cc7177 首页调整 пре 2 недеља
  zgl 6ee051f91d 触发加购弹窗的按钮改为使用产品id获取产品信息 пре 2 недеља
  zhangzf fde58cc929 列表页代码 пре 2 недеља
  zhangzf f6713db261 Merge branch 'master' into zzf-account пре 3 недеља
  zhangzf ae92db09ef 产品详情页调整 пре 3 недеља
  fogwind 4ed3d7f605 合并dev-apolloerror пре 3 недеља
  fogwind c83a0bf416 readme修改 пре 3 недеља
  fogwind f7a16e7340 eslint 错误修改;支付方式错误展示修改 пре 3 недеља
  fogwind 5df1cf30cf 结账页地址保存优化;支付方式和运输方式板块优化;下单按钮显示隐藏使用display;运输方式不自动给用户选,让用户自己选。 пре 3 недеља
  fogwind 4520bb050e 结账页添加giftcard; 购物车和订单详情里添加礼品卡和vipplus相关字段 пре 3 недеља
  fogwind b4532aed97 graphql api 错误处理优化 пре 3 недеља
  fogwind 2b9163fa02 restful api 代理接口返回数据优化;clientFetch函数返回数据优化; пре 3 недеља
  fogwind 4d6bca351c eruda 调试工具 пре 3 недеља
  zgl d16e157084 合并dev-testapplepay пре 3 недеља
  zhangzf e6bcb81f10 地址编辑页报错调整 пре 1 месец
  zhangzf d217858ea9 搜索页和产品详情页调整 пре 1 месец
  zhangzf 69f801882b 同步master пре 1 месец
  zhangzf 323a822af0 aa пре 1 месец
  zhangzf 88783e58c2 Merge branch 'master' into zzf-account пре 1 месец
  zhangzf 7d2d776f36 搜索+订单物流查询 пре 1 месец
  zhangzf 9e7021132f Merge branch 'master' into zzf-account пре 1 месец
  zhangzf a09ae1c967 个人中心订单相关和首页搜索 пре 1 месец
100 измењених фајлова са 7330 додато и 1791 уклоњено
  1. 52 9
      README.md
  2. BIN
      public/image/products_details/afterpay.png
  3. BIN
      public/image/products_details/guide.png
  4. BIN
      public/image/products_details/installment_description.png
  5. BIN
      public/image/products_details/klarna.png
  6. BIN
      public/image/products_details/paypal-expressm.png
  7. BIN
      public/image/products_details/question.png
  8. BIN
      public/image/products_details/reviews.png
  9. 10 4
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  10. 171 107
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  11. 4 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  12. 16 6
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  13. 176 0
      src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx
  14. 37 23
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  15. 23 4
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  16. 18 5
      src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx
  17. 20 1
      src/app/(checkout)/checkout/continuetopay/page.tsx
  18. 1 2
      src/app/(checkout)/checkout/page.tsx
  19. 24 134
      src/app/(public)/_components/HomeBestSellers.tsx
  20. 46 10
      src/app/(public)/_components/HomeContent.tsx
  21. 21 27
      src/app/(public)/_components/HomeMiddleBanner.tsx
  22. 19 132
      src/app/(public)/_components/HomeNewArrival.tsx
  23. 89 93
      src/app/(public)/_components/JinGangSwiper.tsx
  24. 42 37
      src/app/(public)/_components/MiddleImageSwiper.tsx
  25. 20 15
      src/app/(public)/_components/RecommendedProducts.tsx
  26. 22 11
      src/app/(public)/_components/TopImageSwiper.tsx
  27. 63 56
      src/app/(public)/_components/Trending.tsx
  28. 0 0
      src/app/(public)/category copy/[collection]/page.tsx
  29. 71 0
      src/app/(public)/category/[slug]/[id]/_components/CategoryDesc.tsx
  30. 129 0
      src/app/(public)/category/[slug]/[id]/_components/FaqList.tsx
  31. 212 0
      src/app/(public)/category/[slug]/[id]/_components/ProductListing.tsx
  32. 301 0
      src/app/(public)/category/[slug]/[id]/page.tsx
  33. 110 11
      src/app/(public)/customer/account/_components/AccountVipPopup.tsx
  34. 1 1
      src/app/(public)/customer/account/mycoupon/_components/GiftCardList.tsx
  35. 2 2
      src/app/(public)/customer/account/pointsreceivelist/_components/RedeemGiftCard.tsx
  36. 33 33
      src/app/(public)/customer/address/eidt/[id]/page.tsx
  37. 122 41
      src/app/(public)/customer/order/history/_components/OrderHistory.tsx
  38. 119 24
      src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx
  39. 59 0
      src/app/(public)/customer/order/track/order_id/[id]/_components/TrackModal.tsx
  40. 55 55
      src/app/(public)/customer/order/track/order_id/[id]/page.tsx
  41. 28 27
      src/app/(public)/customer/order/view/order_id/[id]/_components/OrderInfoBlock.tsx
  42. 48 16
      src/app/(public)/customer/order/view/order_id/[id]/_components/PriceSummary.tsx
  43. 14 25
      src/app/(public)/customer/order/view/order_id/[id]/_components/ProductList.tsx
  44. 22 59
      src/app/(public)/customer/order/view/order_id/[id]/page.tsx
  45. 21 6
      src/app/(public)/page.tsx
  46. 7 1
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  47. 80 58
      src/app/(public)/product/[...urlProduct]/page.tsx
  48. 63 0
      src/app/(public)/product/_components/NewUserBanner.tsx
  49. 181 24
      src/app/(public)/product/_components/ProductAddToCart.tsx
  50. 392 255
      src/app/(public)/product/_components/ProductInformation.tsx
  51. 108 53
      src/app/(public)/product/_components/ProductMedia.tsx
  52. 48 29
      src/app/(public)/product/_components/ProductReviewSection.tsx
  53. 1 1
      src/app/(public)/product/_components/ProductShortDescription.tsx
  54. 48 0
      src/app/(public)/product/_components/ProductText.tsx
  55. 158 0
      src/app/(public)/product/_components/PromotionDiscountCardProps .tsx
  56. 148 0
      src/app/(public)/product/_components/ShopServicePanel.tsx
  57. 170 0
      src/app/(public)/product/_components/SwitchButton.tsx
  58. 81 0
      src/app/(public)/product/_components/popup/Guide.tsx
  59. 81 0
      src/app/(public)/product/_components/popup/InstallmentPopup.tsx
  60. 102 0
      src/app/(public)/product/_components/popup/PriceModal.tsx
  61. 173 0
      src/app/(public)/product/_components/popup/ShoppingSecurityModal.tsx
  62. 69 0
      src/app/(public)/product/_components/popup/VipPlusModal.tsx
  63. 70 0
      src/app/(public)/product/_components/question/AskQuestionModal.tsx
  64. 157 0
      src/app/(public)/product/_components/question/ComponentsQuestionModal.tsx
  65. 166 0
      src/app/(public)/product/_components/question/FaqList.tsx
  66. 390 0
      src/app/(public)/product/_components/question/QAModal.tsx
  67. 98 58
      src/app/(public)/product/_components/review/AddProductReview.tsx
  68. 260 0
      src/app/(public)/product/_components/review/AddProductReviewForm.tsx
  69. 53 0
      src/app/(public)/product/_components/review/HairPhotoGallery.tsx
  70. 104 0
      src/app/(public)/product/_components/review/QuestionInputModal.tsx
  71. 42 18
      src/app/(public)/product/_components/review/ReviewAdd.tsx
  72. 156 62
      src/app/(public)/product/_components/review/ReviewDetail.tsx
  73. 97 0
      src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx
  74. 123 0
      src/app/(public)/product/_components/review/ReviewImageUploader.tsx
  75. 449 0
      src/app/(public)/product/_components/review/ReviewModal.tsx
  76. 275 0
      src/app/(public)/product/_components/youmaylike/Recommend.tsx
  77. 62 0
      src/app/(public)/search/_components/HotSearch.tsx
  78. 130 76
      src/app/(public)/search/page.tsx
  79. 40 0
      src/app/api/customer/growth-value/route.ts
  80. 46 0
      src/app/api/customer/orders/[id]/route.ts
  81. 61 0
      src/app/api/customer/orders/route.ts
  82. 59 0
      src/app/api/customer/orders/tracking/route.ts
  83. 6 16
      src/app/api/gift/add/route.ts
  84. 5 16
      src/app/api/gift/lists/route.ts
  85. 20 19
      src/app/api/gift/my-gift-cards/route.ts
  86. 12 66
      src/app/api/graphql/route.ts
  87. 46 0
      src/app/api/home/route.ts
  88. 48 0
      src/app/api/shop/products/[productId]/reviews/route.ts
  89. 38 0
      src/app/api/shop/reviews/route.ts
  90. 13 5
      src/app/layout.tsx
  91. 3 3
      src/components/cart/OpenCart.tsx
  92. 2 1
      src/components/catalog/product/ProductCard.tsx
  93. 1 0
      src/components/catalog/product/ProductsSection.tsx
  94. 1 0
      src/components/catalog/type.ts
  95. 46 49
      src/components/common/AddToCartModal/OpenAddToCartModalButton.tsx
  96. 4 3
      src/components/common/button/ReviewButton.tsx
  97. 113 0
      src/components/home/HomeImageBanner.tsx
  98. 1 0
      src/components/home/ProductCarouselTheme.tsx
  99. 2 2
      src/components/layout/navbar/MobileMenu.tsx
  100. 0 0
      src/components/layout/navbar/MobileSearch.tsx

+ 52 - 9
README.md

@@ -294,8 +294,17 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
   - line: 913
 
 8. apollo client 升级后需要用新的方法设置返回数据的类型 -- 已完成
-9. Apollo client的错误处理机制 (src\graphql\catalog\queries\GetFilterAttributes.ts 这个接口可以测试错误)
+9. Apollo client的错误处理机制:
+   1. 一般try...catch 捕获;2. 如果是像useQuery这种返回结果中有error字段的,中通过error属性处理;3.还有的是通过hook的onError回调处理。
+   但是不管哪种处理方式,获取到的error对象都是一样的。
+    ---- (src\graphql\catalog\queries\GetFilterAttributes.ts 这个接口可以测试错误)
     ---- 下单错误处理(afterpay 使用支付失败的卡测试) afterpay金额40 报错
+    ---- 登录用户token过期错误处理
+    ---- serverGraphqlFetch 错误处理:1.业务错误,需要提示的在页面提示;2.throw的错误交给error.tsx和global-error.tsx处理
+    ---- rest api 错误处理
+    ---- graphql接口返回的错误有的有extensions字段,有的没有 具体可以查看nshop项目packages/Webkul/BagistoApi/src/Exception/下的异常类(一般看是否有getExtensions方法)
+    ---------骨架已搭好,待验证是否好用-------------
+
 10. 产品详情页代码优化 --- 已完成
 11. 代码eslint检查修改 -- 已完成
 12. 下单成功后需要重新创建购物车token -- 无需处理
@@ -319,29 +328,63 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
     - https://github.com/livechat/chat-widget-adapters
 28. cancelOrder接口报错   --  已解决
 29. 购物车数据管理方案 -- 已完成
-30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理;
-31. 当前货币等采用redux管理;
-32. 当前货币切换功能
+30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理; -- 已完成
+31. 当前货币等采用redux管理;-- 废弃,已改为全局ConfigProvider管理
+32. 当前货币切换功能      --- 已完成
 33. 登录用户购物车页获取未支付订单
 34. 结账页下单成功后清空redux购物车数据导致结账页重新渲染问题  -- 已解决
     - 通过修改结账页数据初始化逻辑解决
 35. 结账页默认设置运输方式问题(导致结算联动风暴)
 36. 学习set-state-in-effect,然后检查代码,看是否可以优化(特别是CommonModal组件,看能不能不使用useEffect实现)
-37. paypal provider 支付参数根据环境变量配置
+37. paypal provider 支付参数根据环境变量配置 --- 已完成
 38. CartAndUserActions 组件在支付结果页多次挂载问题
 39. 服务端去掉apollo client。apollo client 只在客户端使用; apollo 错误处理(第9条);
 40. Authorization 写入header优化;
+    ---- https://chatgpt.com/share/6a6acb5d-e184-83ea-acfc-839fe0d34caf
 41. 货币切换功能,货币切换后货币符号没有修改问题
 42. 错误处理 https://nextjs.org/docs/app/getting-started/error-handling
 43. redux中的user删除
 44. 关于订单id,管理后台订单列表里显示的是increment_id(数据库里的字段名),前端页面上用的是id(数据库里的字段名)对应接口里的orderId
 
+45. nextjs缓存与bagisto后台管理打通(后台修改配置通知nextjs清除缓存)
+    -- 参考https://chat.deepseek.com/share/6h1huduahkqln69gu1
+
+
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
 
-## 接入klarna
-1. 从服务端获取client_token
-2. 需要加载js sdk
-3. 获取authorization_token
+## 关于请求接口和错误处理
+接口请求有四种情况:服务端组件里的graphql请求和restful请求;客户端组件里的graphql请求和restful请求。
+### 服务端组件里的graphql请求
+服务端组件里的graphql请求统一使用serverGraphqlFetch方法。
+
+对于php接口返回的错误可以通过serverGraphqlFetch方法返回的error字段解析,根据情况在页面上提示用户;
+
+对于不是php接口引起的错误,也可以说是serverGraphqlFetch方法本身引起的错误,会被抛出,然后被error.tsx或global-error.tsx捕获处理。
+### 服务端组件里的restful请求。
+服务端组件里的restful请求使用restApiFetch方法直接请求php后端接口,根据body字段的结构解析php接口返回的错误;
+
+对于不是php接口引起的错误,也可以说是restApiFetch方法本身引起的错误,会被抛出,然后被error.tsx或global-error.tsx捕获处理。
+### 客户端组件中的graphql请求。
+客户端组件中的graphql请求使用的是apollo client。
+
+apollo client的错误处理([文档](https://www.apollographql.com/docs/react/data/error-handling)):
+- try...catch 捕获;
+- 如果是像useQuery这种返回结果中有error字段的,中通过error属性处理;
+- 通过hook的onError回调处理;
+
+不管哪种方式都可以使用`src\lib\ApolloErrorHandler.ts`里的handleApolloBusinessError方法处理业务错误;handleErrorForErrorLink处理其他错误。
+
+### 客户端组件里的restful请求
+客户端组件里的restful请求使用`src\lib\restApiClient.ts`里的clientFetch方法发起。
+
+客户端发起的restful请求都是请求的next.js的route handler。
+在route handler里请求php的接口。
+
+clientFetch方法没有使用try...catch,不主动抛出异常。在实际调用clientFetch的地方使用try...catch捕获代码错误。在clientFetch的返回结果中解析接口返回的错误。
+
+> fetch方法不会抛出异常。接口的状态码即使是500,也不会抛出异常,也是resolve。所以try...catch包裹fetch不会捕获到状态码非2xx的错误(除非你手动抛出异常,具体可以看MDN文档)。
+
+> graphql请求的状态码只有200。所以即使php接口报错,状态码也是200。
 
 
 ## 关于购物车数据管理方案

BIN
public/image/products_details/afterpay.png


BIN
public/image/products_details/guide.png


BIN
public/image/products_details/installment_description.png


BIN
public/image/products_details/klarna.png


BIN
public/image/products_details/paypal-expressm.png


BIN
public/image/products_details/question.png


BIN
public/image/products_details/reviews.png


+ 10 - 4
src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import clsx from 'clsx';
 import { useRouter } from 'next/navigation'
 import {AirwallexCartNumberElementType} from "@/lib/Airwallex/airwallexInit";
 import PaypalButton from "./PaymentButton/PaypalButton";
@@ -297,11 +298,14 @@ export default function CheckoutPlaceOrder({
     return (
         <>
             <div className="relative w-full">
-                {paymentMethod === 'paypal_smart_button' && 
+        
+                <div className={clsx("w-full",{
+                    "hidden": paymentMethod !== 'paypal_smart_button'
+                })}>
                     <PaypalButton
                         createOrder={paypalCreateOrder}
                     />
-                }
+                </div>
                 {(paymentMethod === 'awxklarna' || paymentMethod === 'awxafterpay') &&
                     <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
                         onClick={airwallexPlaceOrder}
@@ -323,9 +327,11 @@ export default function CheckoutPlaceOrder({
                         Place Order
                     </button>
                 }
-                {paymentMethod === 'klarna' &&
+                <div className={clsx("w-full",{
+                    "hidden": paymentMethod !== 'klarna'
+                })}>
                     <KlarnaButton clickHandler={klarnaPlaceOrder} />
-                }
+                </div>
                 
 
             </div>

+ 171 - 107
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -1,9 +1,17 @@
 "use client";
 
-import {useState, useRef, useEffect } from "react";
-import { LoadingSpinner } from "@components/common/LoadingSpinner";
-import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
+import {useState, useRef, useEffect, useCallback } from "react";
+import clsx from "clsx";
 import { useForm, FormProvider } from "react-hook-form";
+import { useCustomToast } from "@/utils/hooks/useToast";
+import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
+import {useCheckoutPaymentMethod} from  "@/utils/hooks/useCheckoutPaymentMethod";
+import {useCheckoutShippingMethod} from  "@/utils/hooks/useCheckoutShippingMethod";
+import { useAppDispatch } from "@/store/hooks";
+import { updateCart, clearCart } from "@/store/slices/cart-slice";
+import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
 import { 
     ShipAddressFormData,
     FullAddressFormData,
@@ -12,31 +20,26 @@ import {
     PlaceOrderFunction,
     CreatePaymentInitiateVariables
 } from "@/types/checkout/type";
+import { CartDetail,CartAddress } from "@/types/cart/type";
+import { formatCartDetail } from "@/utils/cartDetailTools";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
+import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
+import { LoadingSpinner } from "@components/common/LoadingSpinner";
+
 import AddressResultDisplay from "./AddressResultDisplay";
 import ShippingAddressCheckout from "./ShippingAddressCheckout";
 import BillingAddressCheckout from "./BillingAddressCheckout";
 import {ShippingMethodCheckout,RefShippingMethodsHandle} from "./ShippingMethodCheckout";
-import { useCustomToast } from "@/utils/hooks/useToast";
-import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
-import {useCheckoutPaymentMethod} from  "@/utils/hooks/useCheckoutPaymentMethod";
-import {useCheckoutShippingMethod} from  "@/utils/hooks/useCheckoutShippingMethod";
-import { useAppDispatch } from "@/store/hooks";
-import { updateCart } from "@/store/slices/cart-slice";
 import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
-import LoadingPaymentMethod from "./LoadingPaymentMethod";
+
 import LoadingCheckoutPlaceOrder from "./LoadingCheckoutPlaceOrder";
 import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
 import CommonModal from "@/components/theme/ui/CommonModal";
-import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
-import { formatCartDetail, shippingAddressToCartAddress, billingAddressToCartAddress } from "@/utils/cartDetailTools";
-import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import { CartDetail,CartAddress } from "@/types/cart/type";
-import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
-import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
-import { clearCart } from "@/store/slices/cart-slice";
 import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
-import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
+import CheckoutGiftCardGate from "./GiftCard/CheckoutGiftCardGate";
+import PromotionsDetails from "./PromotionsDetails";
+
 /***
  * 不用useForShipping字段了
  * 以shipping address 为准,根据shippingaddress 设置billing address
@@ -250,7 +253,7 @@ export default function CheckoutWrapper({
     } = useCheckoutPaymentMethod();
 
     const { showToast } = useCustomToast();
-    const { getCheckoutAddress, saveCheckoutAddress } = useCheckoutAddress(loginEmail);
+    const { saveCheckoutAddress } = useCheckoutAddress(loginEmail);
 
     const methodShiippingRef = useRef<RefShippingMethodsHandle>(null);
     const methodPaymentRef = useRef<RefPaymentMethodsHandle>(null);
@@ -283,6 +286,15 @@ export default function CheckoutWrapper({
         changePaymentMethodToloadSdk(selectedPaymentMethod);
     }, [selectedPaymentMethod]);
 
+
+    const dispatchCartStore = useCallback((action: "clear" | "update", payload?:Partial<CartDetail>) => {
+        if(action === "clear") {
+            dispatch(clearCart());
+        } else if(action === "update" && payload) {
+            dispatch(updateCart(payload));
+        }
+    },[dispatch]);
+
     const openAddressModal = () => {
         setAddressModalOpen(true);
 
@@ -308,35 +320,65 @@ export default function CheckoutWrapper({
         setAddressSaving(true);
         try {
 
-            const res = await saveCheckoutAddress(saveAddressParam);
-            console.log('CREATE_CHECKOUT_ADDRESS res ====== ',res);
-            // 地址保存成功后重新获取运输方式和支付方式 
-            await getShippingMethod();
-            await getPaymentMethod();
-            
-            // 保存完地址之后还要重新请求地址,把保存后的地址id同步到表单里;
-            const queryAddressRes = await getCheckoutAddress();
-            if(queryAddressRes !== null) {
-                addressForm.resetField('shippingAddressId',{
-                    defaultValue: queryAddressRes.shippingAddress.shippingAddressId
-                });
-                addressForm.resetField('billingAddressId',{
-                    defaultValue: queryAddressRes.billingAddress.billingAddressId
-                });
-
-      
-                // 同步地址到购物车详情
-                const newBillingAddress = billingAddressToCartAddress(queryAddressRes.billingAddress);
-                const newShippingAddress = shippingAddressToCartAddress(queryAddressRes.shippingAddress);
-                dispatch(updateCart({
-                    billingAddress: newBillingAddress,
-                    shippingAddress: newShippingAddress
-                }));
-                setCartData(Object.assign({...cartData},{
-                    billingAddress: {...newBillingAddress},
-                    shippingAddress: {...newShippingAddress}
-                }));
+            const saveRes = await saveCheckoutAddress(saveAddressParam);
+            console.log('CREATE_CHECKOUT_ADDRESS res ====== ',saveRes);
+            if(!saveRes.error) {
+                // 地址保存成功后重新获取运输方式和支付方式 
+                // await getShippingMethod();
+                // await getPaymentMethod();
+                await Promise.all([getShippingMethod(),getPaymentMethod()]);
+                
+                // 保存完地址之后,把保存后的地址id同步到表单里;
+                const createAddressData = saveRes.data;
+                if(createAddressData !== null) {
+                    addressForm.resetField('shippingAddressId',{
+                        defaultValue: createAddressData.shippingAddressId
+                    });
+                    addressForm.resetField('billingAddressId',{
+                        defaultValue: createAddressData.billingAddressId
+                    });
 
+        
+                    // 同步地址到购物车详情
+                    const newBillingAddress =  {
+                        id: String(createAddressData.billingAddressId),
+                        firstName: createAddressData.billingFirstName,
+                        lastName: createAddressData.billingLastName,
+                        email: createAddressData.billingEmail,
+                        address: createAddressData.billingAddress,
+                        city: createAddressData.billingCity,
+                        state: createAddressData.billingState,
+                        country: createAddressData.billingCountry,
+                        postcode: createAddressData.billingPostcode,
+                        phone: createAddressData.billingPhoneNumber
+                    };
+                    const newShippingAddress = {
+                        id: String(createAddressData.shippingAddressId),
+                        firstName: createAddressData.shippingFirstName,
+                        lastName: createAddressData.shippingLastName,
+                        email: createAddressData.shippingEmail,
+                        address: createAddressData.shippingAddress,
+                        city: createAddressData.shippingCity,
+                        state: createAddressData.shippingState,
+                        country: createAddressData.shippingCountry,
+                        postcode: createAddressData.shippingPostcode,
+                        phone: createAddressData.shippingPhoneNumber,
+                    };
+                    dispatchCartStore('update',{
+                        billingAddress: newBillingAddress,
+                        shippingAddress: newShippingAddress
+                    });
+                    setCartData(Object.assign({...cartData},{
+                        billingAddress: {...newBillingAddress},
+                        shippingAddress: {...newShippingAddress}
+                    }));
+
+                } else {
+                    // 提示用户出错了,刷新页面
+                    showToast('Something wrong. Please refresh the page.', 'danger');
+                }
+            } else {
+                showToast(saveRes.msg, 'danger');
             }
             setAddressModalOpen(false);
         } catch(err) {
@@ -353,21 +395,23 @@ export default function CheckoutWrapper({
 
     const handlePaymentMethodChange =  async (method: string) => { 
         changePaymentMethodToloadSdk(method);
-        let shippingMethod = '-1';
-        if(methodShiippingRef.current) {
-            shippingMethod = methodShiippingRef.current.getSelectShipMethod();
-        }
+        // let shippingMethod = '-1';
+        // if(methodShiippingRef.current) {
+        //     shippingMethod = methodShiippingRef.current.getSelectShipMethod();
+        // }
         overlayLoading.start();
         const saveRes = await saveCheckoutCart({ 
-            shippingMethod: shippingMethod,
+            shippingMethod: '-1',
             paymentMethod: method,
-            couponCode: '-1'
+            couponCode: '-1',
+            giftCardNumber: '-1',
+            memberDiscount: '-1'
         });
         overlayLoading.stop();
         if(!saveRes.error) {
             if(saveRes.data) {
                 const newCartDetail = formatCartDetail(saveRes.data);
-                dispatch(updateCart(newCartDetail));
+                dispatchCartStore('update',newCartDetail);
                 setCartData({...newCartDetail});
             }
             return true;
@@ -377,18 +421,20 @@ export default function CheckoutWrapper({
         }
 
     };
-    const handleShippingMethodChange =  async (method: string) => { 
+    const handleShippingMethodChange =  useCallback(async (method: string) => { 
         overlayLoading.start();
         const saveRes = await saveCheckoutCart({ 
             shippingMethod: method,
             paymentMethod: '-1',
-            couponCode: '-1'
+            couponCode: '-1',
+            giftCardNumber: '-1',
+            memberDiscount: '-1'
         });
         overlayLoading.stop();
         if(!saveRes.error) {
             if(saveRes.data) {
                 const newCartDetail = formatCartDetail(saveRes.data);
-                dispatch(updateCart(newCartDetail));
+                dispatchCartStore('update',newCartDetail);
                 setCartData({...newCartDetail});
             }
             // 保存成功之后需要重新获取支付方式
@@ -397,11 +443,11 @@ export default function CheckoutWrapper({
             showToast(saveRes.msg, 'danger');
         }
 
-    };
+    },[saveCheckoutCart,showToast,getPaymentMethod,dispatchCartStore]);
 
 
     // 校验地址 运输方式,支付方式;
-    const validateCheckout = async () => { 
+    const validateCheckout = useCallback(async () => { 
         const addressValid = await addressForm.trigger();
 
         if(!addressValid) {
@@ -446,26 +492,30 @@ export default function CheckoutWrapper({
             const saveRes = await saveCheckoutCart({ 
                 shippingMethod: '-1',
                 paymentMethod: pmethod,
-                couponCode: '-1'
+                couponCode: '-1',
+                giftCardNumber: '-1',
+                memberDiscount: '-1'
             });
             if(!saveRes.error) {
                 if(saveRes.data) {
                     const newCartDetail = formatCartDetail(saveRes.data);
-                    dispatch(updateCart(newCartDetail));
+                    dispatchCartStore('update',newCartDetail);
                     setCartData({...newCartDetail});
                 }
+            } else {
+                showToast(saveRes.msg, 'danger');
             }
         }
 
         return true;
 
-    };
+    },[addressForm,showToast,saveCheckoutCart,dispatchCartStore]);
     // 苹果支付下单
-    const applepayPlaceOrder = async () => { 
+    const applepayPlaceOrder = useCallback(async () => { 
         const res = await createOrder();
         if(!res.error) {
             //创建订单成功后清空redux 购物车
-            dispatch(clearCart());  
+            dispatchCartStore('clear'); 
             return {
                 orderId: res.data?.gatewayOrderId ?? '',
                 webOrderId: res.data?.orderId ?? ''
@@ -474,24 +524,20 @@ export default function CheckoutWrapper({
             // 抛出错误让第三方支付能够捕获
             throw new Error("CREATE_ORDER_ERROR: " + res.msg);
         } 
-    };
-    // 下单
+    },[createOrder,dispatchCartStore]);
+    // 下单(下单和校验分开)
     const handlePlaceOrder: PlaceOrderFunction = async <T,>(params?: CreatePaymentInitiateVariables): Promise<PlaceOrderResult<T>>  => { 
-        // 校验地址
-        // overlayLoading.start();
-        // const validRes = await validateCheckout();
-        // if(validRes) {
             const res = await createOrder(params);
             if(res.error) {
                 overlayLoading.stop();
                 confirmDialog({
                     title: "Warning",
-                    content: res.msg + " Create order failed. Please try again.",
+                    content: res.msg + ". Create order failed. Please try again.",
                     noCancel: true,
                 }).then(() => {});
             } else {
                 //创建订单成功后清空redux 购物车
-                dispatch(clearCart());  
+                dispatchCartStore('clear');
             }
             const pmethod = methodPaymentRef.current?.getSelectPaymentMethod();
             let resultData: PlaceOrderResult<T>;
@@ -518,12 +564,16 @@ export default function CheckoutWrapper({
                 };
             }
             return resultData;
-        // } else {
-        //     overlayLoading.stop();
-        //     return {valid: false, data: null, otherData: null};
-        // }
     }
 
+    const onChangeGiftcard = async (paylod: CartDetail) => {
+        dispatchCartStore('update',paylod);
+        setCartData({...paylod});
+        // @todo 还要请求支付方式和运输方式
+        // await getShippingMethod();
+        //     await getPaymentMethod();
+        await Promise.all([getShippingMethod(),getPaymentMethod()]);
+    };
 
     // function tt() {
     //     const arr = [
@@ -603,42 +653,53 @@ export default function CheckoutWrapper({
             </div>
             <div className="mt-6 box-border px-4">
                 <h3 className="text-ly-24 font-medium" onClick={testEruda}>Shipping Method</h3>
-                {shippingMethodLoading ?
-                
-                    <CheckoutShippingMethodLoading /> 
-                :
-                    <ShippingMethodCheckout 
-                        ref={methodShiippingRef}  
-                        shipMethods={shippingMethodDatas}
-                        errorMsg={shippingMethodError}  
-                        selectedShippingRate={selectedShippingRate}
-                        onShipMethodChange={handleShippingMethodChange}  
-                    />
-                }
+           
+                <ShippingMethodCheckout 
+                    ref={methodShiippingRef}  
+                    shipMethods={shippingMethodDatas}
+                    errorMsg={shippingMethodError}  
+                    selectedShippingRate={selectedShippingRate}
+                    shippingMethodLoading={shippingMethodLoading}
+                    onShipMethodChange={handleShippingMethodChange}  
+                />
+            </div>
 
+            <div className="mt-6 box-border px-4">
+                <CheckoutGiftCardGate 
+                    giftcardNumber={cartData.giftcardNumber || ''}
+                    formattedGiftcardAmount={cartData.formattedGiftcardAmount} 
+                    onChangeGiftcard={onChangeGiftcard}
+                />
+            </div>
+            
+            <div className="mt-6 box-border px-4">
+                <h3 className="text-ly-24 font-medium">Promotion Details</h3>
+                <div className="w-full box-border mt-3">
+                    <PromotionsDetails cartData={cartData} />
+                </div>
             </div>
 
             <div className="mt-6 box-border px-4">
                 <h3 className="text-ly-24 font-medium">Payment Method</h3>
-                { paymentMethodLoading && <LoadingPaymentMethod /> }
-                { (!paymentMethodLoading && paymentMethodError) && <p className="mt-3 text-red-500 text-ly-12">{paymentMethodError}</p>}
-                { (!paymentMethodLoading && !paymentMethodError) && 
-                    <PaymentMethodCheckout 
-                        ref={methodPaymentRef}
-                        paymentMethods={paymentMethodDatas} 
-                        selectedPaymentMethod={selectedPaymentMethod}
-                        onPaymentMethodChange={handlePaymentMethodChange}
-                    />
-                }
+                <PaymentMethodCheckout 
+                    ref={methodPaymentRef}
+                    paymentMethods={paymentMethodDatas} 
+                    selectedPaymentMethod={selectedPaymentMethod}
+                    paymentMethodLoading={paymentMethodLoading}
+                    loadError={paymentMethodError}
+                    onPaymentMethodChange={handlePaymentMethodChange}
+                />
+                
                 
             </div>
             <div className="mt-3 box-border px-4 pb-8">
                 <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.</p>
                 <div className="mt-6 w-full">
-                    { paymentMethodLoading ?
-                        <LoadingCheckoutPlaceOrder />
-                    :
-                        (selectedPaymentMethod === 'applepay' ?
+                    { paymentMethodLoading && <LoadingCheckoutPlaceOrder />}
+                    
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod !== 'applepay' || paymentMethodLoading
+                    })}>
                         <PaypalApplepayButton 
                             grandTotal={cartData.grandTotal}
                             billingContact={cartData.billingAddress}
@@ -646,14 +707,17 @@ export default function CheckoutWrapper({
                             validateCheckout={validateCheckout}
                             createOrder={applepayPlaceOrder}
                         />
-                        :
+                    </div>
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod === 'applepay' || paymentMethodLoading
+                    })}>
                         <CheckoutPlaceOrder 
                             paymentMethod={selectedPaymentMethod}
                             validateCheckout={validateCheckout}
                             clickPlaceOrder={handlePlaceOrder}
-                        />)
-                        
-                    }
+                        />
+                    </div>  
+                    
                 </div>
             </div>
 

+ 4 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx

@@ -172,6 +172,10 @@ export default function ContinueToPayOrderInfo({
                     <p className="text-ly-12 leading-ly-20">Discount</p>
                     <p className="text-ly-12 leading-ly-20">{currencySymbol}{orderDetail.discountAmount}</p>
                 </div>
+                <div className="flex justify-between mt-3 first:mt-0">
+                    <p className="text-ly-12 leading-ly-20">Gift Card</p>
+                    <p className="text-ly-12 leading-ly-20">-{currencySymbol}{orderDetail.giftcardAmount}</p>
+                </div>
                 {/* <div className="flex justify-between mt-3 first:mt-0">
                     <p className="text-ly-12 leading-ly-20">You Earn</p>
                     <p className="text-ly-12 leading-ly-20">600 Reward Points</p>

+ 16 - 6
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import clsx from "clsx";
 import Image from "next/image";
 import { use,useState,useCallback } from "react";
 import { useForm, get, useWatch } from "react-hook-form";
@@ -505,12 +506,15 @@ export function PaymentMethodContinueTpPay({
                 </p>
 
                 <div className="mt-6 w-full">
-                    {selectedPaymentMethod === 'paypal_smart_button' && 
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod !== 'paypal_smart_button'
+                    })}>
                         <PaypalButton
                             createOrder={paypalRepayOrder}
                             isRePay={true}
                         />
-                    }
+                    </div>
+                    
                     {(selectedPaymentMethod === 'awxklarna' || selectedPaymentMethod === 'awxafterpay') &&
                         <button className="flex items-center justify-center w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold"
                             onClick={airwallexRepay}
@@ -534,7 +538,10 @@ export function PaymentMethodContinueTpPay({
                             Place Order
                         </button>
                     }
-                    {selectedPaymentMethod === 'applepay' &&
+
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod !== 'applepay'
+                    })}>
                         <PaypalApplepayButton 
                             grandTotal={orderDetail.grandTotal}
                             billingContact={billingContact}
@@ -543,12 +550,15 @@ export function PaymentMethodContinueTpPay({
                             createOrder={applepayPlaceOrder}
                             isRePay={true}
                         />
-                    }
-                    {selectedPaymentMethod === 'klarna' &&
+                    </div>
+                   
+                    <div className={clsx("w-full",{
+                        "hidden": selectedPaymentMethod !== 'klarna'
+                    })}>
                         <KlarnaButton 
                             clickHandler={klarnaPlaceOrder}
                         />
-                    }
+                    </div>
 
                 </div>
             </div>

Разлика између датотеке није приказан због своје велике величине
+ 176 - 0
src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx


+ 37 - 23
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -1,13 +1,15 @@
 "use client";
 
+import clsx from "clsx";
 import Image from "next/image";
-import { Ref, useImperativeHandle } from "react";
+import { Ref, useEffect, useImperativeHandle } from "react";
 import { useForm, get } from "react-hook-form";
 import { CheckoutPaymentMethod } from "@/types/checkout/type";
 import { AirwallexCardElements } from "@/lib/Airwallex/airwallexInit";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import AirwallexCardInput from "./PaymentMethodAdditional/AirwallexCardInput";
 import {useAirwallexCard} from "@/lib/Airwallex/useAirwallexCard";
+import LoadingPaymentMethod from "./LoadingPaymentMethod";
 
 /**
  * 关于如何确定默认选中哪个支付方式:
@@ -24,49 +26,39 @@ export function PaymentMethodCheckout({
     ref,
     paymentMethods,
     selectedPaymentMethod,
+    paymentMethodLoading,
+    loadError,
     onPaymentMethodChange,
 }: {
     ref: Ref<RefPaymentMethodsHandle>;
     paymentMethods: CheckoutPaymentMethod[];
     selectedPaymentMethod: string;
+    paymentMethodLoading: boolean;
+    loadError: string | null;
     onPaymentMethodChange: (value: string) => Promise<boolean>;
 }) {
 
     const { showToast } = useCustomToast();
 
 
-    const {airwallexCardState,getCardElements} = useAirwallexCard(true,{
+    const {airwallexCardState,getCardElements} = useAirwallexCard(!paymentMethodLoading,{
         cardNumber: 'airwallex_cardNumber', 
         cvc: 'airwallex_cvc', 
         expiry: 'airwallex_expiry'
     });
   
-    let defaultValue = '';
- 
-    const findItem = paymentMethods.find((item) => item.method === selectedPaymentMethod);
-    if(findItem !== undefined) {
-        defaultValue = selectedPaymentMethod;
-
-    } else {
-        paymentMethods.forEach((item) => {
-            if(item.method === 'paypal_smart_button') {
-                defaultValue = item.method;
-            }
-        });
-    }
-
-  
     const {
         register,
         getValues,
         trigger,
+        reset,
         formState: { errors }
     } = useForm({
         mode: "onChange",
         reValidateMode: "onChange", 
-        defaultValues: {
-            paymentMethod: defaultValue,
-        }
+        // defaultValues: {
+        //     paymentMethod: defaultValue,
+        // }
     });
 
     // 参考 https://github.com/react-hook-form/error-message/blob/master/src/ErrorMessage.tsx
@@ -77,6 +69,23 @@ export function PaymentMethodCheckout({
         onPaymentMethodChange(event.target.value);                                
     };
 
+    useEffect(() => {
+        let defaultValue = '';
+ 
+        const findItem = paymentMethods.find((item) => item.method === selectedPaymentMethod);
+        if(findItem !== undefined) {
+            defaultValue = selectedPaymentMethod;
+
+        } else {
+            paymentMethods.forEach((item) => {
+                if(item.method === 'paypal_smart_button') {
+                    defaultValue = item.method;
+                }
+            });
+        }
+        reset({ paymentMethod: defaultValue });
+    },[paymentMethods,selectedPaymentMethod]);
+
     useImperativeHandle(ref, () => {
         return {
             // 获取用户选择的支付方式
@@ -132,8 +141,13 @@ export function PaymentMethodCheckout({
     const airwallexOtherInfoShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
 
     
-    return (
-        <form name="shipping method" className="w-full">
+    return (<>
+
+        { paymentMethodLoading && <LoadingPaymentMethod /> }
+        { (!paymentMethodLoading && loadError) && <p className="mt-3 text-red-500 text-ly-12">{loadError}</p>}
+        <form name="shipping method" className={clsx("w-full", {
+            "hidden": (paymentMethodLoading || loadError)
+        })}>
         
             <div className="mt-3 w-full">
                 <ul className="w-full">
@@ -186,7 +200,7 @@ export function PaymentMethodCheckout({
             </div>
             
         </form>
-    );
+    </>);
   
 }
 

+ 23 - 4
src/app/(checkout)/checkout/_components/PromotionsDetails.tsx

@@ -1,15 +1,34 @@
 "use client";
 
-
+import { CartDetail } from "@/types/cart/type";
 
 
 // 金额明细
-export default function PromotionsDetails() { 
+export default function PromotionsDetails({cartData}:{
+    cartData: CartDetail
+}) { 
 
 
     return (
-        <div className="">
-
+        <div className="w-full box-border">
+            <ul className="w-full">
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Subtotal</span>
+                    <span>{cartData.formattedSubtotal}</span>
+                </li>
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Shipping Method</span>
+                    <span>{cartData.formattedShippingAmount}</span>
+                </li>
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Gift Card</span>
+                    <span className="flex items-center flex-none h-6 bg-ly-gold px-2 text-ly-14">-{cartData.formattedGiftcardAmount}</span>
+                </li>
+                <li className="flex justify-between items-center text-ly-12 mt-4 first:mt-0">
+                    <span>Grand Total</span>
+                    <span>{cartData.formattedGrandTotal}</span>
+                </li>
+            </ul>
         </div>
     );
 }

+ 18 - 5
src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx

@@ -1,10 +1,11 @@
 "use client";
 
-
+import clsx from "clsx";
 import { useEffect, useImperativeHandle, Ref  } from "react";
 import { useForm, get } from "react-hook-form";
 
 import {CheckoutShippingRate} from "@/types/checkout/type";
+import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
 
 
 
@@ -24,12 +25,14 @@ export function ShippingMethodCheckout({
     errorMsg,
     shipMethods,
     selectedShippingRate,
+    shippingMethodLoading,
     onShipMethodChange
 }: {
     ref: Ref<RefShippingMethodsHandle>;
     errorMsg: string | null;
     shipMethods: CheckoutShippingRate[];
     selectedShippingRate: string | null;
+    shippingMethodLoading: boolean;
     onShipMethodChange: (value: string) => void;
 }) {
 
@@ -61,7 +64,7 @@ export function ShippingMethodCheckout({
         }
         
     },[]);
-    // 根据cartDetail 的loading判断,是true 返回,是false时才往下执行
+    
     useEffect(() => { 
         if(shipMethods.length > 0 ) {
             let defaultValue = '';
@@ -72,6 +75,10 @@ export function ShippingMethodCheckout({
                     defaultValue = selectedShippingRate;
                 }
             }
+            if(defaultValue) {
+                reset({ shippingMethod: defaultValue });
+            }
+            /*
             if(!defaultValue) {
                 findItem = shipMethods.find((item) => item.price === 0) || shipMethods[0];
                 defaultValue = findItem.method;
@@ -83,12 +90,18 @@ export function ShippingMethodCheckout({
             if(defaultValue !== selectedShippingRate) {
                 onShipMethodChange(defaultValue);
             }
+            */
         }
         
     }, [shipMethods,selectedShippingRate]);
     
-    return (
-        <form name="shipping method" className="w-full mt-3">
+    return (<>
+       
+        {shippingMethodLoading && <CheckoutShippingMethodLoading />}
+    
+        <form name="shipping method" className={clsx("w-full mt-3",{
+            "hidden": shippingMethodLoading
+        })}>
             {errorMsg && <p className="text-red-500 text-ly-12">{errorMsg}</p>}
             {shipMethods.length === 0 && <p className="text-red-500 text-ly-12">Please complete address data to get shipping methods list.</p>}
             <div className="w-full">
@@ -129,7 +142,7 @@ export function ShippingMethodCheckout({
             </div>
             
         </form>
-    );
+    </>);
   
 }
 

+ 20 - 1
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -1,5 +1,6 @@
 import {Suspense} from "react";
 import { getServerSession } from "next-auth";
+import Link from "next/link";
 import { redirect, RedirectType } from 'next/navigation';
 import { authOptions } from "@utils/auth";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
@@ -43,7 +44,7 @@ export default async function ContinueToPay({searchParams}: {
 
     const query = await searchParams;
     const orderid = query?.orderid;
-    const {data: orderDetailResponse} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
+    const {data: orderDetailResponse, error: orderDetailError} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
         query: GET_ORDER_DETAILS,
         variables: {
             id: orderid as string
@@ -62,7 +63,25 @@ export default async function ContinueToPay({searchParams}: {
         }
     });
       
+    if(orderDetailError) {
+        return (
+            <div className="box-border px-4 w-full">
+            
+                
+                <div className="w-12 h-12 mx-auto mt-9">
+                    <svg className="failure block w-full h-full" width="200" height="200" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27635"><path d="M736 332.8L556.8 512l179.2 179.2-44.8 44.8L512 556.8l-179.2 179.2-44.8-44.8L467.2 512 288 332.8l44.8-44.8L512 467.2l179.2-179.2 44.8 44.8z" p-id="27636"></path><path d="M512 0a512 512 0 1 1 0 1024A512 512 0 0 1 512 0z m0 64a448 448 0 1 0 0 896A448 448 0 0 0 512 64z" p-id="27637"></path></svg>
+                </div>
+                <p className="text-ly-16 text-center mt-6">
+                    Get Pending Order failed:
+                    <span className="text-ly-16">{orderDetailError.message}</span>
+                </p>
 
+                <Link href="/" className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
+                    Continue Shopping
+                </Link>
+            </div>
+        );
+    }
     return (
         <div className="w-full pb-8">
         

+ 1 - 2
src/app/(checkout)/checkout/page.tsx

@@ -25,8 +25,7 @@ export default async function CheckoutPage() {
 
 
     const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
-        query: GET_CART_ITEM,
-        operationName:'GetCartItem'
+        query: GET_CART_ITEM
     });
     if(cartDetailsRes.createReadCart === null) {
         redirect('/', RedirectType.replace);

+ 24 - 134
src/app/(public)/_components/HomeBestSellers.tsx

@@ -5,129 +5,19 @@ import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/Ope
 // 导入Swiper基础样式
 import "swiper/css";
 import "swiper/css/navigation";
-
-// 模拟产品数据(12个产品,分2块,每块6个)
-const productData = [
-  // 第一块(6个)
-  {
-    id: 1,
-    img: "https://picsum.photos/400/500?random=1",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 2,
-    img: "https://picsum.photos/400/500?random=2",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 3,
-    img: "https://picsum.photos/400/500?random=3",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 4,
-    img: "https://picsum.photos/400/500?random=4",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 5,
-    img: "https://picsum.photos/400/500?random=5",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 6,
-    img: "https://picsum.photos/400/500?random=6",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-
-  // 第二块(6个)
-  {
-    id: 7,
-    img: "https://picsum.photos/400/500?random=7",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 8,
-    img: "https://picsum.photos/400/500?random=8",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 9,
-    img: "https://picsum.photos/400/500?random=9",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 10,
-    img: "https://picsum.photos/400/500?random=10",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 11,
-    img: "https://picsum.photos/400/500?random=11",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 12,
-    img: "https://picsum.photos/400/500?random=12",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-];
-
-// 拆分数据为2个块(每块6个)
-const productBlocks = [
-  productData.slice(0, 6), // 第一块
-  productData.slice(6, 12), // 第二块
-];
-
-const HomeBestSellers = () => {
+import { HomeProduct } from "@/types/home/type";
+import Link from "next/link";
+interface HomeBestSellersProps {
+  bestSellers: HomeProduct[];
+}
+const HomeBestSellers = ({ bestSellers }: HomeBestSellersProps) => {
+  if (!bestSellers || bestSellers.length === 0) return null;
+  // 4. 真实数据按每6个拆分一组,替换原来的模拟拆分逻辑
+  const chunkSize = 6;
+  const productBlocks: HomeProduct[][] = [];
+  for (let i = 0; i < bestSellers.length; i += chunkSize) {
+    productBlocks.push(bestSellers.slice(i, i + chunkSize));
+  }
   return (
     <div className="w-full max-w-[1200px] mx-auto px-4 py-8">
       {/* 标题 */}
@@ -159,14 +49,14 @@ const HomeBestSellers = () => {
                 <div key={product.id} className="flex flex-col gap-2">
                   {/* 产品图片 */}
                   <img
-                    src={product.img}
-                    alt={product.title}
+                    src={product.image_url}
+                    alt={product.name}
                     className="w-full h-auto object-cover rounded-sm"
                   />
                   {/* 产品标题 */}
-                  <p className="text-sm text-gray-800 truncate">
-                    {product.title}
-                  </p>
+                  <Link href={"/aaaa"||""} className="text-sm text-gray-800 truncate">
+                    {product.name}
+                  </Link>
                   {/* 评分 + 评论数 */}
                   <div className="flex items-center gap-1">
                     <div className="flex">
@@ -183,22 +73,22 @@ const HomeBestSellers = () => {
                       ))}
                     </div>
                     <span className="text-xs text-gray-600">
-                      {product.reviewCount}
+                      {product.review_count}
                     </span>
                   </div>
                   {/* 价格 + 购物车图标 */}
                   <div className="flex items-center justify-between">
                     <div className="flex items-center gap-1">
-                      <span className="font-semibold">${product.price}</span>
+                      <span className="font-semibold">
+                        ${product.min_price}
+                      </span>
                       <span className="text-xs text-gray-500 line-through">
-                        ${product.originalPrice}
+                        ${product.price}
                       </span>
                     </div>
                     {/* 购物车图标 */}
                     <OpenAddToCartModalButton
-                      productUrlKey={
-                        "http://nshop.test/hd-transparent-deep-wave-&-curly-lace-frontal-closure-wig"
-                      }
+                      productId={product.id}
                       icon={
                         <svg
                           xmlns="http://www.w3.org/2000/svg"

+ 46 - 10
src/app/(public)/_components/HomeContent.tsx

@@ -12,7 +12,11 @@ import RecommendedProducts from "./RecommendedProducts";
 import CategoryTabSwiper  from "./CategoryTabSwiper ";
 import "swiper/css";
 import "swiper/css/navigation";
-
+import {HomeApiData,HomeProduct } from "@/types/home/type"
+// 定义组件入参类型
+interface HomeContentProps {
+  homeData: HomeApiData;
+}
 // 模拟假数据
 const MOCK_DATA = {
   // 服务标签
@@ -88,12 +92,44 @@ const MOCK_DATA = {
   ],
 };
 
-export default function HoHomeContentme() {
+export default function HomeContent({ homeData }: HomeContentProps) {
+    const { 
+      // channel,
+       sections,
+        // categories 
+      } = homeData;
+      // 过滤出 banner 板块
+  const bannerSection = sections.find(sec => sec.name === "banner");
+  const bannerImages = bannerSection?.images ?? [];
+       // 过滤出 banner 板块
+  const jinGangSection = sections.find(sec => sec.id === 13);
+  const jinGang = jinGangSection?.images ?? [];
+    // 2. 筛选热销板块,假设后端name为 best_sellers,自行修改匹配后端标识
+  const bestSellerSection = sections.find(sec => sec.name === "BestSellers");
+  const bestSellers: HomeProduct[] = bestSellerSection?.products ?? [];
+        // 过滤出 banner 板块
+  const middleBannerSection = sections.find(sec => sec.id === 15);
+  const middleBanner = middleBannerSection?.images ?? [];
+   // 3. 筛选new Arrival板块
+  const newArrivalSection = sections.find(sec => sec.name === "New_Arrival");
+  const newArrival: HomeProduct[] = newArrivalSection?.products ?? [];
+   // 4. 筛选7板块
+  const zeroWearImageSection = sections.find(sec => sec.id === 18);
+  const zeroWearImage= zeroWearImageSection?.images ?? [];
+   // 4. 筛选7板块
+  const zeroWearSection = sections.find(sec => sec.name === "7");
+  const zeroWear: HomeProduct[] = zeroWearSection?.products ?? [];
+   // 4. 筛选trendingSection板块
+  const trendingSection = sections.find(sec => sec.name === "Trending_Now");
+  const trending: HomeProduct[] = trendingSection?.products ?? [];
+   // 4. Recommended
+  const recommendedSection = sections.find(sec => sec.name === "Recommended_For_You");
+  const recommended: HomeProduct[] = recommendedSection?.products ?? [];
   return (
     <div className="min-h-screen bg-white font-sans">
       {/* 顶部Banner */}
       <div className="w-full relative">
-        <TopImageSwiper />
+        <TopImageSwiper  bannerImages={bannerImages}  />
       </div>
 
       {/* 服务标签栏 */}
@@ -106,14 +142,14 @@ export default function HoHomeContentme() {
         ))}
       </div>
       {/* 分类快捷入口 */}
-      <JinGangSwiper />
+      <JinGangSwiper jinGang={jinGang} />
       {/* 热销商品区 */}
-      <HomeBestSellers/>
-      <HomeMiddleBanner />
-      <HomeNewArrival/>
-      <MiddleImageSwiper/>
-      <Trending/>
-      <RecommendedProducts/>
+      <HomeBestSellers bestSellers={bestSellers} />
+      <HomeMiddleBanner  middleBanner={middleBanner} />
+      <HomeNewArrival newArrival={newArrival} />
+      <MiddleImageSwiper zeroWear={zeroWear}  zeroWearImage={zeroWearImage} />
+      <Trending trending={trending}/>
+      <RecommendedProducts recommended={recommended} />
       <CategoryTabSwiper />
     </div>
   );

+ 21 - 27
src/app/(public)/_components/HomeMiddleBanner.tsx

@@ -5,16 +5,22 @@ import { Navigation } from "swiper/modules";
 import "swiper/css";
 import "swiper/css/navigation";
 import Image from "next/image";
-export default function HomeMiddleBanner() {
+import { HomeSectionImage } from "@/types/home/type";
+interface middleBannerProps {
+  middleBanner: HomeSectionImage[];
+}
+export default function HomeMiddleBanner({ middleBanner }: middleBannerProps) {
   const [activeIndex, setActiveIndex] = useState(0);
 
   const [swiperRef] = useState(null);
+  if (!middleBanner || middleBanner.length === 0) return null;
+  const middleBannerImages = middleBanner;
 
-  const handleSlideChange = (swiper:any) => {
+  const handleSlideChange = (swiper: any) => {
     setActiveIndex(swiper.activeIndex);
   };
 
-  const handleIndicatorClick = (index:any) => {
+  const handleIndicatorClick = (index: any) => {
     if (swiperRef) {
       setActiveIndex(index);
     }
@@ -29,30 +35,18 @@ export default function HomeMiddleBanner() {
         onSlideChange={handleSlideChange}
         className="mb-6"
       >
-        <SwiperSlide>
-          <div className="relative w-full h-42">
-            <Image
-              src={
-                "https://cdn.asteriahair.com/media/wysiwyg/home/1750041184.png"
-              }
-              alt={"bbbb"}
-              fill
-              className="w-full h-auto object-cover rounded-md"
-            />
-          </div>
-        </SwiperSlide>
-        <SwiperSlide>
-          <div className="relative w-full h-42">
-            <Image
-              src={
-                "https://cdn.asteriahair.com/media/wysiwyg/home/1750041184.png"
-              }
-              alt={"bbbb"}
-              fill
-              className="w-full h-auto object-cover rounded-md"
-            />
-          </div>
-        </SwiperSlide>
+        {middleBannerImages.map((item, idx) => (
+          <SwiperSlide key={idx}>
+            <div className="relative w-full h-42">
+              <Image
+                src={item.image}
+                alt={item.title}
+                fill
+                className="w-full h-auto object-cover rounded-md"
+              />
+            </div>
+          </SwiperSlide>
+        ))}
       </Swiper>
       <div className="flex justify-center gap-2">
         <button

+ 19 - 132
src/app/(public)/_components/HomeNewArrival.tsx

@@ -5,129 +5,18 @@ import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/Ope
 // 导入Swiper基础样式
 import "swiper/css";
 import "swiper/css/navigation";
-
-// 模拟产品数据(12个产品,分2块,每块6个)
-const productData = [
-  // 第一块(6个)
-  {
-    id: 1,
-    img: "https://picsum.photos/400/500?random=1",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 2,
-    img: "https://picsum.photos/400/500?random=2",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 3,
-    img: "https://picsum.photos/400/500?random=3",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 4,
-    img: "https://picsum.photos/400/500?random=4",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 5,
-    img: "https://picsum.photos/400/500?random=5",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 6,
-    img: "https://picsum.photos/400/500?random=6",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-
-  // 第二块(6个)
-  {
-    id: 7,
-    img: "https://picsum.photos/400/500?random=7",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 8,
-    img: "https://picsum.photos/400/500?random=8",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 9,
-    img: "https://picsum.photos/400/500?random=9",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 10,
-    img: "https://picsum.photos/400/500?random=10",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 11,
-    img: "https://picsum.photos/400/500?random=11",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-  {
-    id: 12,
-    img: "https://picsum.photos/400/500?random=12",
-    title: "14-36 In Water Wave Huma...",
-    rating: 4.5,
-    reviewCount: 342,
-    price: 143.35,
-    originalPrice: 143.35,
-  },
-];
-
-// 拆分数据为2个块(每块6个)
-const productBlocks = [
-  productData.slice(0, 4), // 第一块
-  productData.slice(4, 8), // 第二块
-];
-
-const HomeNewArrival = () => {
+import {HomeProduct} from "@/types/home/type"
+interface NewArrivalProps {
+  newArrival: HomeProduct[];
+}
+const HomeNewArrival = ({ newArrival }: NewArrivalProps) => {
+     if (!newArrival || newArrival.length === 0) return null;
+        // 4. 真实数据按每6个拆分一组,替换原来的模拟拆分逻辑
+    const chunkSize = 6;
+    const productBlocks:  HomeProduct[][] = [];
+    for (let i = 0; i < newArrival.length; i += chunkSize) {
+      productBlocks.push(newArrival.slice(i, i + chunkSize));
+    }
   return (
     <div className="w-full max-w-[1200px] mx-auto px-4 py-8">
       {/* 标题 */}
@@ -159,13 +48,13 @@ const HomeNewArrival = () => {
                 <div key={product.id} className="flex flex-col gap-2">
                   {/* 产品图片 */}
                   <img
-                    src={product.img}
-                    alt={product.title}
+                    src={product.image_url}
+                    alt={product.name}
                     className="w-full h-auto object-cover rounded-sm"
                   />
                   {/* 产品标题 */}
                   <p className="text-sm text-gray-800 truncate">
-                    {product.title}
+                    {product.name}
                   </p>
                   {/* 评分 + 评论数 */}
                   <div className="flex items-center gap-1">
@@ -183,22 +72,20 @@ const HomeNewArrival = () => {
                       ))}
                     </div>
                     <span className="text-xs text-gray-600">
-                      {product.reviewCount}
+                      {product.review_count}
                     </span>
                   </div>
                   {/* 价格 + 购物车图标 */}
                   <div className="flex items-center justify-between">
                     <div className="flex items-center gap-1">
-                      <span className="font-semibold">${product.price}</span>
+                      <span className="font-semibold">${product.min_price}</span>
                       <span className="text-xs text-gray-500 line-through">
-                        ${product.originalPrice}
+                        ${product.price}
                       </span>
                     </div>
                     {/* 购物车图标 */}
                     <OpenAddToCartModalButton
-                      productUrlKey={
-                        "http://nshop.test/hd-transparent-deep-wave-&-curly-lace-frontal-closure-wig"
-                      }
+                        productId={product.id}
                       icon={
                         <svg
                           xmlns="http://www.w3.org/2000/svg"

+ 89 - 93
src/app/(public)/_components/JinGangSwiper.tsx

@@ -6,73 +6,83 @@ import { Navigation } from "swiper/modules";
 import "swiper/css";
 import "swiper/css/navigation";
 import Image from "next/image";
+import { HomeSectionImage } from "@/types/home/type";
+interface jinGangProps {
+  jinGang: HomeSectionImage[];
+}
+// const MOCK_DATA = [
+//   [
+//     {
+//       name: "Hd Lace Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "Ready To Go",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "Colored Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "U&V Part Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "New Arrival",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "360 Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
+//     },
+//   ],
 
-const MOCK_DATA = [
-  [
-    {
-      name: "Hd Lace Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
-    },
-    {
-      name: "Ready To Go",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
-    },
-    {
-      name: "Colored Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
-    },
-    {
-      name: "U&V Part Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
-    },
-    {
-      name: "New Arrival",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
-    },
-    {
-      name: "360 Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095740_8181b0e039800e07.png",
-    },
-  ],
-
-  [
-    {
-      name: "13x4 Lace Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
-    },
-    {
-      name: "Body Wave Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
-    },
-    {
-      name: "Deep Wave Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
-    },
-    {
-      name: "Straight Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
-    },
-    {
-      name: "Curly Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
-    },
-    {
-      name: "Bob Wig",
-      img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
-    },
-  ],
-];
+//   [
+//     {
+//       name: "13x4 Lace Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "Body Wave Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "Deep Wave Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "Straight Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "Curly Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
+//     },
+//     {
+//       name: "Bob Wig",
+//       img: "https://cdn.asteriahair.com/uploads/202602/11/20260211095800_8181b0e039800e07.png",
+//     },
+//   ],
+// ];
 
-export default function JinGangSwiper() {
+export default function JinGangSwiper({ jinGang }: jinGangProps) {
   const [activeIndex, setActiveIndex] = useState(0);
 
   const [swiperRef, setSwiperRef] = useState<SwiperType | null>(null);
+  if (!jinGang || jinGang.length === 0) return null;
+  // 4. 真实数据按每6个拆分一组,替换原来的模拟拆分逻辑
+  const chunkSize = 6;
+  const productBlocks: HomeSectionImage[][] = [];
+  for (let i = 0; i < jinGang.length; i += chunkSize) {
+    productBlocks.push(jinGang.slice(i, i + chunkSize));
+  }
 
-  const handleSlideChange = (swiper:any) => {
+  const handleSlideChange = (swiper: any) => {
     setActiveIndex(swiper.activeIndex);
   };
 
-  const handleIndicatorClick = (index:any) => {
+  const handleIndicatorClick = (index: any) => {
     if (swiperRef) {
       swiperRef.slideTo(index);
       setActiveIndex(index);
@@ -85,46 +95,32 @@ export default function JinGangSwiper() {
         modules={[Navigation]}
         spaceBetween={10}
         slidesPerView={1}
-         onSwiper={(swiper) => setSwiperRef(swiper)}
+        onSwiper={(swiper) => setSwiperRef(swiper)}
         onSlideChange={handleSlideChange}
         className="mb-6"
       >
-        <SwiperSlide>
-          <div className="grid grid-cols-3 gap-2">
-            {MOCK_DATA[0].map((item, idx) => (
-              <div key={`block1-${idx}`} className="relative w-full aspect-[3/4]">
-                <Image
-                  src={item.img}
-                  alt={item.name}
-                  fill
-                  className="object-cover rounded-md"
-                />
-                {/* <div className="absolute bottom-2 left-2 text-white text-sm font-medium bg-black/50 px-2 py-1 rounded">
-                  {item.name}
-                </div> */}
-              </div>
-            ))}
-          </div>
-        </SwiperSlide>
-
-        {/* 第二大块:6个分类卡片(2行3列) */}
-        <SwiperSlide>
-          <div className="grid grid-cols-3 gap-2">
-            {MOCK_DATA[1].map((item, idx) => (
-              <div key={`block2-${idx}`} className="relative w-full aspect-[3/4]">
-                <Image
-                  src={item.img}
-                  alt={item.name}
-                  fill
-                  className="object-cover rounded-md"
-                />
-                {/* <div className="absolute bottom-2 left-2 text-white text-sm font-medium bg-black/50 px-2 py-1 rounded">
+        {productBlocks.map((block, blockIndex) => (
+          <SwiperSlide key={blockIndex}>
+            <div className="grid grid-cols-3 gap-2">
+              {block.map((item, idx) => (
+                <div
+                  key={`block1-${idx}`}
+                  className="relative w-full aspect-[3/4]"
+                >
+                  <Image
+                    src={item.image}
+                    alt={item.title}
+                    fill
+                    className="object-cover rounded-md"
+                  />
+                  {/* <div className="absolute bottom-2 left-2 text-white text-sm font-medium bg-black/50 px-2 py-1 rounded">
                   {item.name}
                 </div> */}
-              </div>
-            ))}
-          </div>
-        </SwiperSlide>
+                </div>
+              ))}
+            </div>
+          </SwiperSlide>
+        ))}
       </Swiper>
 
       {/* 底部指示器按钮:2个,激活态黑色,未激活灰色 */}

+ 42 - 37
src/app/(public)/_components/MiddleImageSwiper.tsx

@@ -6,41 +6,48 @@ import "swiper/css/pagination";
 import "swiper/css/navigation";
 import "./css/swiper-custom.css";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+import {HomeProduct,HomeSectionImage} from "@/types/home/type"
+interface zeroWearProps {
+  zeroWear: HomeProduct[];
+  zeroWearImage:HomeSectionImage[]
+}
+// const productList = [
+//   {
+//     id: 1,
+//     img: "https://picsum.photos/400/500?random=1",
+//     title: "14-36 In Water Wave Hum...",
+//     price: 143.35,
+//   },
+//   {
+//     id: 2,
+//     img: "https://picsum.photos/400/500?random=2",
+//     title: "14-36 In Water Wave Hum.",
+//     price: 143.35,
+//   },
+//   {
+//     id: 3,
+//     img: "https://picsum.photos/400/500?random=3",
+//     title: "14-36 In Water Wave Hum...",
+//     price: 143.35,
+//   },
+//   {
+//     id: 4,
+//     img: "https://picsum.photos/400/500?random=4",
+//     title: "14-36 In Water Wave Hum.",
+//     price: 143.35,
+//   },
+// ];
 
-const productList = [
-  {
-    id: 1,
-    img: "https://picsum.photos/400/500?random=1",
-    title: "14-36 In Water Wave Hum...",
-    price: 143.35,
-  },
-  {
-    id: 2,
-    img: "https://picsum.photos/400/500?random=2",
-    title: "14-36 In Water Wave Hum.",
-    price: 143.35,
-  },
-  {
-    id: 3,
-    img: "https://picsum.photos/400/500?random=3",
-    title: "14-36 In Water Wave Hum...",
-    price: 143.35,
-  },
-  {
-    id: 4,
-    img: "https://picsum.photos/400/500?random=4",
-    title: "14-36 In Water Wave Hum.",
-    price: 143.35,
-  },
-];
-
-const MiddleImageSwiper = () => {
+const MiddleImageSwiper = ({ zeroWear,zeroWearImage }: zeroWearProps) => {
+  if (!zeroWear || zeroWear.length === 0) return null;
+  const  productList = zeroWear
+  const WearImage = zeroWearImage;
   return (
     <div className="w-full pr-4 pl-4 mx-auto  rounded-lg  ">
       <div className="w-full">
         <img
-          src="https://cdn.asteriahair.com/uploads/202602/11/20260211095330_7ec9031a4ab36e44.gif"
-          alt="ZEROWEAR hair banner"
+          src={WearImage[0].image || 'https://cdn.asteriahair.com/uploads/202602/11/20260211095330_7ec9031a4ab36e44.gif'}
+          alt={WearImage[0].title || 'ZEROWEAR hair banner'}
           className="w-full h-auto object-cover block"
         />
       </div>
@@ -64,28 +71,26 @@ const MiddleImageSwiper = () => {
               <SwiperSlide key={item.id}>
                 <div className="bg-white rounded overflow-hidden  flex flex-col">
                   <img
-                    src={item.img}
-                    alt={item.title}
+                    src={item.image_url}
+                    alt={item.name}
                     className="w-full aspect-[3/4] object-cover"
                   />
 
                   <div className="p-2 flex flex-col gap-2">
                     <p className="text-sm text-gray-800 truncate">
-                      {item.title}
+                      {item.name}
                     </p>
                     {/* 价格 + 购物车图标 */}
                     <div className="flex items-center justify-between">
                       <div className="flex items-center gap-1">
-                        <span className="font-semibold">${item.price}</span>
+                        <span className="font-semibold">${item.min_price}</span>
                         <span className="text-xs text-gray-500 line-through">
                           ${item.price}
                         </span>
                       </div>
                       {/* 购物车图标 */}
                       <OpenAddToCartModalButton
-                        productUrlKey={
-                          "http://nshop.test/hd-transparent-deep-wave-&-curly-lace-frontal-closure-wig"
-                        }
+                         productId={item.id}
                         icon={
                           <svg
                             xmlns="http://www.w3.org/2000/svg"

+ 20 - 15
src/app/(public)/_components/RecommendedProducts.tsx

@@ -2,6 +2,10 @@
 import React, { useState, useEffect, useCallback } from "react";
 import Image from "next/image";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+import {HomeProduct} from "@/types/home/type"
+interface recommendedProps {
+  recommended: HomeProduct[];
+}
 // 商品类型定义
 interface ProductItem {
   id: number;
@@ -50,9 +54,10 @@ const mockGetProductApi = async (page: number, pageSize: number) => {
 
 const PAGE_SIZE = 6;
 
-export default function RecommendedProducts() {
+export default function RecommendedProducts({ recommended }: recommendedProps) {
+  
   // 已渲染的全部商品
-  const [productList, setProductList] = useState<ProductItem[]>([]);
+  const [productList, _setProductList] = useState<HomeProduct[]>(recommended);
   // 当前页码
   const [page, setPage] = useState(1);
   // 数据总条数
@@ -70,9 +75,9 @@ export default function RecommendedProducts() {
       const res = await mockGetProductApi(currentPage, PAGE_SIZE);
       setTotal(res.total);
       // 函数式更新,稳定依赖
-      setProductList((prev) =>
-        currentPage === 1 ? res.list : [...prev, ...res.list],
-      );
+      // setProductList((prev) =>
+      //   currentPage === 1 ? res.list : [...prev, ...res.list],
+      // );
     } catch (err) {
       // 屏蔽 no-console 警告
       // eslint-disable-next-line no-console
@@ -96,7 +101,7 @@ export default function RecommendedProducts() {
     setPage(nextPage);
     fetchPageData(nextPage);
   };
-
+  if (!recommended || recommended.length === 0) return null;
   // 五星星星渲染
   const renderStars = (count: number) => {
     return Array.from({ length: count }).map((_, idx) => (
@@ -124,8 +129,8 @@ export default function RecommendedProducts() {
             {/* Next.Image 商品图容器 */}
             <div className="relative w-full aspect-[3/4] overflow-hidden rounded-md">
               <Image
-                src={item.img}
-                alt={item.title}
+                src={item.image_url}
+                alt={item.name}
                 fill
                 sizes="(max-width:768px) 50vw, 33vw"
                 className="object-cover"
@@ -133,13 +138,13 @@ export default function RecommendedProducts() {
             </div>
 
             {/* 标题 */}
-            <p className="text-sm text-gray-800 truncate">{item.title}</p>
+            <p className="text-sm text-gray-800 truncate">{item.name}</p>
 
             {/* 星级 + 评论数 */}
             <div className="flex items-center gap-1">
-              {renderStars(item.star)}
+              {renderStars(item.rating)}
               <span className="text-sm text-gray-600 ml-1 underline">
-                {item.reviewCount}
+                {item.review_count}
               </span>
             </div>
 
@@ -171,15 +176,15 @@ export default function RecommendedProducts() {
             {/* 价格 + 购物车图标 */}
             <div className="flex items-center justify-between">
               <div className="flex items-center gap-1">
-                <span className="font-semibold">${item.price}</span>
+                <span className="font-semibold">${item.min_price}</span>
                 <span className="text-xs text-gray-500 line-through">
-                  ${item.originPrice}
+                  ${item.price}
                 </span>
               </div>
               {/* 购物车图标 */}
               <OpenAddToCartModalButton
-                productUrlKey={
-                  "http://nshop.test/hd-transparent-deep-wave-&-curly-lace-frontal-closure-wig"
+                productId={
+                  item.id
                 }
                 icon={
                   <svg

+ 22 - 11
src/app/(public)/_components/TopImageSwiper.tsx

@@ -2,15 +2,26 @@ import React from 'react';
 import { Swiper, SwiperSlide } from 'swiper/react';
 import 'swiper/css';
 // import { Navigation, Pagination, Mousewheel, Keyboard } from 'swiper/modules';
-
-const TopImageSwiper = () => {
+// 定义图片类型
+type BannerImageItem = {
+  link: string;
+  image: string;
+  title: string;
+};
+// 组件入参
+interface TopImageSwiperProps {
+  bannerImages: BannerImageItem[];
+}
+const TopImageSwiper = ({ bannerImages }: TopImageSwiperProps) => {
+  // 兜底:没有图片不渲染,防止报错
+  if (!bannerImages || bannerImages.length === 0) return null;
   // ✅ 直接在这里放你的图片数组,想加多少加多少
-  const images = [
-    'https://cdn.asteriahair.com/uploads/202607/01/20260701165056_761989a0fdc11d5e.jpg',
-    'https://cdn.asteriahair.com/uploads/202607/01/20260701165110_3fff435adf276b89.jpg',
-    'https://cdn.asteriahair.com/uploads/202607/01/20260701165123_c424f6f1d0b29522.jpg',
-    'https://cdn.asteriahair.com/uploads/202607/01/20260701165056_761989a0fdc11d5e.jpg',
-  ];
+  // const images = [
+  //   'https://cdn.asteriahair.com/uploads/202607/01/20260701165056_761989a0fdc11d5e.jpg',
+  //   'https://cdn.asteriahair.com/uploads/202607/01/20260701165110_3fff435adf276b89.jpg',
+  //   'https://cdn.asteriahair.com/uploads/202607/01/20260701165123_c424f6f1d0b29522.jpg',
+  //   'https://cdn.asteriahair.com/uploads/202607/01/20260701165056_761989a0fdc11d5e.jpg',
+  // ];
 
   return (
     <div className="w-full max-w-[1200px] mx-auto ">
@@ -25,11 +36,11 @@ const TopImageSwiper = () => {
         className="h-auto md:h-[80vh]  overflow-hidden"
       >
         {/* ✅ 自动遍历图片数组,一行代码搞定所有图片 */}
-        {images.map((img, index) => (
+        {bannerImages .map((item, index) => (
           <SwiperSlide key={index}>
             <img
-              src={img}
-              alt={`slide-${index}`}
+                 src={item.image}
+              alt={item.title}
               className="w-full h-full object-cover"
             />
           </SwiperSlide>

+ 63 - 56
src/app/(public)/_components/Trending.tsx

@@ -6,52 +6,66 @@ import "swiper/css/scrollbar";
 import "swiper/css/navigation";
 import Image from "next/image";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+import {HomeProduct} from "@/types/home/type"
+interface trendingProps {
+  trending: HomeProduct[];
+}
+// // 商品数据,每个卡片携带videoUrl视频地址
+// type ProductItem = {
+//   id: number;
+//   img: string;
+//   title: string;
+//   price: number;
+//   videoUrl: string;
+// };
 
-// 商品数据,每个卡片携带videoUrl视频地址
-type ProductItem = {
-  id: number;
-  img: string;
-  title: string;
-  price: number;
-  videoUrl: string;
-};
-
-const productList: ProductItem[] = [
-  {
-    id: 1,
-    img: "/img/hair-video-1.jpg",
-    title: "14-36 In Water Wave Human Hair HD Inv...",
-    price: 146.1,
-    videoUrl: "/video/hair-demo-1.mp4",
-  },
-  {
-    id: 2,
-    img: "/img/hair-video-2.jpg",
-    title: "14-36 In Water Wave Human Hair HD Inv...",
-    price: 146.1,
-    videoUrl: "/video/hair-demo-2.mp4",
-  },
-  {
-    id: 3,
-    img: "/img/hair-video-3.jpg",
-    title: "14-36 In Water Wave Human Hair HD Inv...",
-    price: 146.1,
-    videoUrl: "/video/hair-demo-3.mp4",
-  },
-  {
-    id: 4,
-    img: "/img/hair-video-4.jpg",
-    title: "14-36 In Water Wave Human Hair HD Inv...",
-    price: 146.1,
-    videoUrl: "/video/hair-demo-4.mp4",
-  },
-];
+// const productList: ProductItem[] = [
+//   {
+//     id: 1,
+//     img: "/img/hair-video-1.jpg",
+//     title: "14-36 In Water Wave Human Hair HD Inv...",
+//     price: 146.1,
+//     videoUrl: "/video/hair-demo-1.mp4",
+//   },
+//   {
+//     id: 2,
+//     img: "/img/hair-video-2.jpg",
+//     title: "14-36 In Water Wave Human Hair HD Inv...",
+//     price: 146.1,
+//     videoUrl: "/video/hair-demo-2.mp4",
+//   },
+//   {
+//     id: 3,
+//     img: "/img/hair-video-3.jpg",
+//     title: "14-36 In Water Wave Human Hair HD Inv...",
+//     price: 146.1,
+//     videoUrl: "/video/hair-demo-3.mp4",
+//   },
+//   {
+//     id: 4,
+//     img: "/img/hair-video-4.jpg",
+//     title: "14-36 In Water Wave Human Hair HD Inv...",
+//     price: 146.1,
+//     videoUrl: "/video/hair-demo-4.mp4",
+//   },
+// ];
 
-const Trending = () => {
-  // 视频弹窗状态
+const Trending = ({ trending }: trendingProps) => {
+   // 视频弹窗状态
   const [openVideoModal, setOpenVideoModal] = useState(false);
   const [currentVideoSrc, setCurrentVideoSrc] = useState("");
-  const videoRef = useRef<HTMLVideoElement>(null);
+   const videoRef = useRef<HTMLVideoElement>(null);
+   // 弹窗打开自动播放
+  useEffect(() => {
+    if (openVideoModal && videoRef.current) {
+      videoRef.current.play().catch((err) => console.log("播放拦截", err));
+    }
+  }, [openVideoModal]);
+  
+  if (!trending || trending.length === 0) return null;
+  const  productList = trending
+ 
+ 
 
   // 打开弹窗并赋值视频地址
   const handlePlayClick = (videoUrl: string) => {
@@ -68,12 +82,7 @@ const Trending = () => {
     }
   };
 
-  // 弹窗打开自动播放
-  useEffect(() => {
-    if (openVideoModal && videoRef.current) {
-      videoRef.current.play().catch((err) => console.log("播放拦截", err));
-    }
-  }, [openVideoModal]);
+ 
 
   return (
     <div className="w-full max-w-[960px] mx-auto">
@@ -134,13 +143,13 @@ const Trending = () => {
                   {/* 图片区域 + 中间播放按钮 */}
                   <div className="relative rounded-xl w-full aspect-[4/5] overflow-hidden rounded-xl">
                     <img
-                      src={item.img}
-                      alt={item.title}
+                      src={item.image_url}
+                      alt={item.name}
                       className="w-full h-full object-cover"
                     />
                     {/* 居中圆形播放按钮 */}
                     <button
-                      onClick={() => handlePlayClick(item.videoUrl)}
+                      onClick={() => handlePlayClick("/video/hair-demo-1.mp4"||item.videoUrl)}
                       className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
                       w-24 h-24 rounded-full bg-white/60 backdrop-blur-sm
                       flex items-center justify-center transition hover:bg-white/80"
@@ -161,21 +170,19 @@ const Trending = () => {
                   {/* 底部文字价格区域 */}
                   <div className="p-3 flex flex-col rounded-xl mt-3 bg-[#f0f8f5] gap-3">
                     <p className="text-base text-gray-900 truncate">
-                      {item.title}
+                      {item.name}
                     </p>
                     {/* 价格 + 购物车图标 */}
                     <div className="flex items-center justify-between">
                       <div className="flex items-center gap-1">
-                        <span className="font-semibold">${item.price}</span>
+                        <span className="font-semibold">${item.min_price}</span>
                         <span className="text-xs text-gray-500 line-through">
                           ${item.price}
                         </span>
                       </div>
                       {/* 购物车图标 */}
                       <OpenAddToCartModalButton
-                        productUrlKey={
-                          "http://nshop.test/hd-transparent-deep-wave-&-curly-lace-frontal-closure-wig"
-                        }
+                         productId={item.id}
                         icon={
                           <svg
                             xmlns="http://www.w3.org/2000/svg"

src/app/(public)/category/[collection]/page.tsx → src/app/(public)/category copy/[collection]/page.tsx


Разлика између датотеке није приказан због своје велике величине
+ 71 - 0
src/app/(public)/category/[slug]/[id]/_components/CategoryDesc.tsx


+ 129 - 0
src/app/(public)/category/[slug]/[id]/_components/FaqList.tsx

@@ -0,0 +1,129 @@
+"use client";
+import { useState } from "react";
+
+// 1. 定义FAQ数据类型
+export interface FaqItem {
+  id: string;
+  question: string;
+  answer?: string;
+  likeCount: number;
+}
+
+// 2. 单个FAQ折叠项子组件(内部完全管理展开逻辑,不向外暴露状态)
+function FaqAccordionItem({
+  item,
+  isOpen,
+  onToggle,
+}: {
+  item: FaqItem;
+  isOpen: boolean;
+  onToggle: () => void;
+}) {
+  // 统一上下箭头SVG,增加旋转动画
+  const ArrowIcon = (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="16"
+      height="16"
+      viewBox="0 0 16 16"
+      fill="none"
+      className="transition-transform duration-300 ease-in-out"
+      style={{ transform: isOpen ? "rotate(0deg)" : "rotate(180deg)" }}
+    >
+      <path
+        d="M2.99834 11.4952C2.74906 11.4952 2.54509 11.6991 2.54509 11.9484L2.54509 13.0604C2.54509 13.3097 2.74906 13.5137 2.99834 13.5137L13.0604 13.5137C13.3097 13.5137 13.5137 13.3097 13.5137 13.0604L13.5137 11.9484C13.5137 11.6991 13.3097 11.4952 13.0604 11.4952L2.99834 11.4952Z"
+        fill="#E5E5E5"
+      ></path>
+      <path
+        d="M13.0596 9.87793C13.3089 9.87793 13.391 9.71437 13.2421 9.51441L8.29445 2.87045C8.14556 2.67051 7.90192 2.67051 7.75305 2.87045L2.80538 9.51441C2.65648 9.71437 2.73862 9.87793 2.98791 9.87793L13.0596 9.87793Z"
+        fill="#E5E5E5"
+      ></path>
+    </svg>
+  );
+
+  // 点赞爱心SVG提取
+  const LikeIcon = (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="16"
+      height="16"
+      fill="currentColor"
+      viewBox="0 0 16 16"
+    >
+      <path d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z" />
+    </svg>
+  );
+
+  return (
+    <div className="border border-gray-100 rounded-lg bg-[#F9F9F9] p-4 mb-3">
+      {/* 标题行:Q图标 + 问题 + 上下箭头 */}
+      <button
+        type="button"
+        onClick={onToggle}
+        className="w-full flex items-start justify-between text-left gap-3"
+      >
+        {/* 绿色圆形Q图标 */}
+        <div className="w-8 h-8 rounded-full bg-emerald-700 flex items-center justify-center shrink-0 text-white">
+          Q
+        </div>
+
+        {/* 问题文字 */}
+        <h3 className="text-base font-medium text-emerald-800 flex-1">
+          {item.question}
+        </h3>
+
+        {/* 带动画切换箭头 */}
+        <div className="shrink-0 mt-1">{ArrowIcon}</div>
+      </button>
+
+      {/* 展开答案区域 */}
+      {isOpen && item.answer && (
+        <div className="mt-4">
+          {/* A标识区域 */}
+          <div className="flex gap-2 mb-3">
+            <div className="w-8 h-8 rounded-full bg-white border border-gray-900 flex items-center justify-center shrink-0">
+              A
+            </div>
+            <p className="text-gray-800 text-sm leading-relaxed">
+              {item.answer}
+            </p>
+          </div>
+          {/* 点赞栏 */}
+          <div className="flex items-center justify-end gap-2 text-emerald-700 text-sm">
+            <span>{item.likeCount}</span>
+            {LikeIcon}
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}
+
+// 外层FAQ列表容器(所有折叠状态内部维护,不对外透出)
+export default function FaqList({ faqList }: { faqList: FaqItem[] }) {
+  // 内部维护:当前展开的FAQ ID,仅单条互斥展开
+  const [openId, setOpenId] = useState<string | null>(null);
+
+  // 只展示前3条数据
+  const displayList = faqList.slice(0, 3);
+
+  // 内部切换折叠逻辑,无需父组件参与
+  const toggleFaq = (id: string) => {
+    setOpenId(openId === id ? null : id);
+  };
+
+  return (
+    <div className="w-full max-w-2xl mx-auto">
+      <div className="space-y-1 mt-4 px-4">
+        {displayList.map((item) => (
+          <FaqAccordionItem
+            key={item.id}
+            item={item}
+            isOpen={openId === item.id}
+            onToggle={() => toggleFaq(item.id)}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}

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

@@ -0,0 +1,212 @@
+"use client";
+
+import { useState, useEffect, useRef } from "react";
+
+import Grid from "@components/theme/ui/grid/Grid";
+import ProductGridItems from "@components/catalog/product/ProductGridItems";
+import NewMobileFilter from "@components/theme/filters/NewMobileFilter";
+import NewSortOrder from "@components/theme/filters/NewSortOrder";
+
+import { SortByFields } from "@utils/constants";
+import { isArray } from "@/utils/type-guards";
+import { useRouter, usePathname } from "next/navigation";
+interface QueryState {
+  search: string;
+
+  sort: string;
+
+  filters: Record<string, string>;
+
+  cursor?: string;
+}
+interface ProductListingProps {
+  categoryId: string;
+
+  filterAttributes: any[];
+
+  initialProducts: any[];
+
+  initialPageInfo: any;
+
+  initialTotal: number;
+
+  initialQuery: QueryState;
+}
+
+export default function ProductListing({
+  categoryId,
+  filterAttributes,
+  initialProducts,
+  initialPageInfo,
+  initialTotal,
+  initialQuery,
+}: ProductListingProps) {
+  // 新增:页面刷新初始化源头打印
+  console.log("【页面刷新初始initialQuery】", initialQuery);
+  console.log("【刷新初始filters(传给筛选器的源头)】", initialQuery.filters);
+  const [query, setQuery] = useState(initialQuery);
+
+  const [products, setProducts] = useState(initialProducts);
+
+  const [pageInfo, setPageInfo] = useState(initialPageInfo);
+
+  const [_total, setTotal] = useState(initialTotal);
+  const firstRender = useRef(true);
+  const fetchProducts = async (currentQuery: QueryState) => {
+    try {
+      const res = await fetch("/api/products", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          categoryId,
+
+          search: currentQuery.search,
+
+          sort: currentQuery.sort,
+
+          filters: currentQuery.filters,
+
+          first: 12,
+
+          after: null,
+        }),
+      });
+      console.log("筛选排序触发调接口");
+      const result = await res.json();
+
+      const newProducts =
+        result.data?.products?.edges?.map((e: any) => e.node) || [];
+
+      setProducts(newProducts);
+
+      setPageInfo(result.data?.products?.pageInfo);
+
+      setTotal(result.data?.products?.totalCount || 0);
+    } catch (error) {
+      console.error("fetch products error", error);
+    }
+  };
+  useEffect(() => {
+    if (firstRender.current) {
+      firstRender.current = false;
+      console.log("刷新页面不触发调接口");
+
+      return;
+    }
+    fetchProducts(query);
+  }, [query.filters, query.sort, query.search]);
+
+  const router = useRouter();
+  const pathname = usePathname();
+  console.log("query-----:", query);
+  const updateQuery = (nextQuery: any) => {
+    setQuery(nextQuery);
+
+    const params = new URLSearchParams();
+
+    if (nextQuery.search) {
+      params.set("q", nextQuery.search);
+    }
+
+    if (nextQuery.sort) {
+      params.set("sort", nextQuery.sort);
+    }
+
+    Object.entries(nextQuery.filters).forEach(([key, value]) => {
+      if (value) {
+        params.set(key, value as string);
+      }
+    });
+
+    router.replace(`${pathname}?${params.toString()}`, {
+      scroll: false,
+    });
+  };
+  //   view more 逻辑
+  const loadMore = async () => {
+    const res = await fetch("/api/products", {
+      method: "POST",
+
+      headers: {
+        "Content-Type": "application/json",
+      },
+
+      body: JSON.stringify({
+        categoryId,
+
+        search: query.search,
+
+        sort: query.sort,
+
+        filters: query.filters,
+
+        first: 15,
+
+        after: pageInfo.endCursor,
+      }),
+    });
+    console.log("触发view more调接口");
+    const result = await res.json();
+
+    const moreProducts =
+      result.data?.products?.edges?.map((e: any) => e.node) || [];
+
+    setProducts((prev) => [...prev, ...moreProducts]);
+
+    setPageInfo(result.data.products.pageInfo);
+  };
+  return (
+    <>
+      <div className="flex items-center justify-between gap-4 py-8 md:hidden w-full max-w-screen-2xl mx-auto px-4">
+        <NewMobileFilter
+          filterAttributes={filterAttributes}
+          filters={query.filters}
+          onChange={(filters) => {
+            updateQuery({
+              ...query,
+
+              filters,
+
+              cursor: undefined,
+            });
+          }}
+        />
+        <NewSortOrder
+          sortOrders={SortByFields}
+          title="Sort by"
+          value={query.sort}
+          onChange={(sort) => {
+            updateQuery({
+              ...query,
+
+              sort,
+
+              cursor: undefined,
+            });
+          }}
+        />
+      </div>
+
+      {isArray(products) && products.length > 0 ? (
+        <Grid className="grid grid-flow-row grid-cols-2 gap-5 lg:gap-11.5 w-full max-w-screen-2xl mx-auto md:grid-cols-3 lg:grid-cols-4 px-4 xss:px-7.5">
+          <ProductGridItems products={products} />
+        </Grid>
+      ) : (
+        <div className="px-4">
+          <div className="flex h-40 items-center justify-center rounded-lg border border-dashed border-neutral-300">
+            <p className="text-neutral-500">
+              No products found in this category.
+            </p>
+          </div>
+        </div>
+      )}
+      {pageInfo?.hasNextPage && (
+        <div className="flex justify-center my-10">
+          <button onClick={loadMore}>View More</button>
+        </div>
+      )}
+    </>
+  );
+}

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

@@ -0,0 +1,301 @@
+import { Metadata } from "next";
+import { notFound } from "next/navigation";
+// import { isArray } from "@/utils/type-guards";
+// import FilterList from "@components/theme/filters/FilterList";
+// import Pagination from "@components/catalog/Pagination";
+import ProductListing from "./_components/ProductListing";
+import { ProductsResponse } from "@components/catalog/type";
+import {
+  GET_FILTER_PRODUCTS,
+  // GET_TREE_CATEGORIES,
+  GET_CATEHORY_BY_ID,
+} from "@/graphql";
+import {
+  // cachedGraphQLRequest,
+  // cachedCategoryRequest,
+  getFilterAttributes,
+} from "@/utils/hooks/useCache";
+import { SortByFields } from "@utils/constants";
+// import { CategoryDetail } from "@components/theme/search/CategoryDetail";
+import CategoryDesc from "./_components/CategoryDesc";
+import FaqList, { FaqItem } from "./_components/FaqList";
+import { Suspense } from "react";
+import FilterListSkeleton from "@components/common/skeleton/FilterSkeleton";
+// import { TreeCategoriesResponse } from "@/types/theme/category-tree";
+// import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
+import {
+  // extractNumericId,
+  // findCategoryBySlug,
+  buildProductFilters,
+} from "@utils/helper";
+import { serverGraphqlFetch } from "@/utils/bagisto/index";
+import {
+  // CategoryNode,
+  CategorySingleResponse,
+} from "@/types/theme/category-tree";
+
+/**列表页 */
+export async function generateMetadata({
+  params,
+}: {
+  params: Promise<{ slug: string; id: string }>;
+}): Promise<Metadata> {
+  const { slug: _categorySlug, id: categoryId } = await params;
+  const { data: categoryData } = await serverGraphqlFetch<
+    CategorySingleResponse,
+    { id: number }
+  >({
+    query: GET_CATEHORY_BY_ID,
+    variables: {
+      id: Number(categoryId),
+    },
+  });
+
+  //   const {data:treeData} = await cachedGraphQLRequest<TreeCategoriesResponse>(
+  //     "category",
+  //     GET_TREE_CATEGORIES,
+  //     { parentId: 1 }
+  //   );
+
+  //   const categories = treeData?.treeCategories || [];
+  //   const categoryItem = findCategoryBySlug(categories, categorySlug);
+  const categoryItem = categoryData.category;
+  if (!categoryItem) return notFound();
+
+  const translation = categoryItem.translation;
+
+  return {
+    title: translation?.metaTitle || translation?.name,
+    description: translation?.description || `${translation?.name} products`,
+  };
+}
+
+export default async function CategoryPage({
+  searchParams,
+  params,
+}: {
+  params: Promise<{ slug: string; id: string }>;
+  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+  //   const { collection: categorySlug } = await params;
+  const { slug: _categorySlug, id: categoryId } = await params;
+  const resolvedParams = await searchParams;
+  function getSearchParam(
+    value: string | string[] | undefined,
+  ): string | undefined {
+    return Array.isArray(value) ? value[0] : value;
+  }
+  /*
+  const [{data:treeData}, filterAttributes] = await Promise.all([
+    cachedGraphQLRequest<TreeCategoriesResponse>(
+      "category",
+      GET_TREE_CATEGORIES,
+      { parentId: 1 }
+    ),
+    getFilterAttributes(), // 这个不能用
+  ]);
+
+  const categories = treeData?.treeCategories || [];
+  const categoryItem = findCategoryBySlug(categories, categorySlug);
+  */
+  const filterAttributes = await getFilterAttributes(); // 这个不能用
+
+  console.log("filterAttributes---------------:", filterAttributes);
+  const { data: categoryData } = await serverGraphqlFetch<
+    CategorySingleResponse,
+    { id: number }
+  >({
+    query: GET_CATEHORY_BY_ID,
+    variables: {
+      id: Number(categoryId),
+    },
+  });
+  const categoryItem = categoryData.category;
+  if (!categoryItem) return notFound();
+
+  console.log("categoryItem +++++++++++", categoryItem);
+  //   const numericId = extractNumericId(categoryItem.id);
+  const numericId = String(categoryItem._id);
+
+  // const {
+  //   q: searchValue,
+  //   page,
+  //   cursor,
+  //   before,
+  // } = (resolvedParams || {}) as {
+  //   [key: string]: string;
+  // };
+  const searchValue = getSearchParam(resolvedParams?.q) ?? "";
+
+  // const page = getSearchParam(resolvedParams?.page);
+
+  const cursor = getSearchParam(resolvedParams?.cursor);
+
+  const before = getSearchParam(resolvedParams?.before);
+  const itemsPerPage = 12;
+  // const currentPage = page ? parseInt(page) - 1 : 0;
+  const sortValue = getSearchParam(resolvedParams?.sort) ?? "name-asc";
+  const selectedSort =
+    SortByFields.find((s) => s.key === sortValue) || SortByFields[0];
+
+  const { filterObject: baseFilterObject } = buildProductFilters(
+    resolvedParams || {},
+  );
+
+  const filterObject: Record<string, string> = {
+    ...baseFilterObject,
+  };
+
+  if (numericId) {
+    filterObject.category_id = numericId;
+  }
+
+  const filterInput = JSON.stringify(filterObject);
+
+  // 这个也改成 serverGraphqlFetch
+  const productVariables = {
+    query: searchValue || "",
+    filter: filterInput,
+    ...(before
+      ? {
+          last: itemsPerPage,
+          before,
+        }
+      : {
+          first: itemsPerPage,
+          after: cursor || null,
+        }),
+    sortKey: selectedSort.sortKey,
+    reverse: selectedSort.reverse,
+  };
+  const { data } = await serverGraphqlFetch<
+    ProductsResponse,
+    typeof productVariables
+  >({
+    query: GET_FILTER_PRODUCTS,
+    variables: productVariables,
+  });
+  // const [{ data }] = await Promise.all([
+  //   cachedCategoryRequest<ProductsResponse>(categorySlug, GET_FILTER_PRODUCTS, {
+  //     query: searchValue || "",
+  //     filter: filterInput,
+  //     ...(before
+  //       ? { last: itemsPerPage, before: before }
+  //       : { first: itemsPerPage, after: cursor }),
+  //     sortKey: selectedSort.sortKey,
+  //     reverse: selectedSort.reverse,
+  //   }),
+  // ]);
+  console.log(
+    "GET_FILTER_PRODUCTS ----- ",
+    searchValue,
+    filterInput,
+    itemsPerPage,
+    before,
+    cursor,
+    selectedSort,
+  );
+
+  const products = data?.products?.edges?.map((e) => e.node) || [];
+  const pageInfo = data?.products?.pageInfo;
+  const totalCount = data?.products?.totalCount || 0;
+  const translation = categoryItem.translation;
+  console.log("initialQuery------------", {
+    search: searchValue,
+    sort: sortValue,
+    filters: baseFilterObject,
+  });
+  // 模拟FAQ假数据
+  const mockFaqData: FaqItem[] = [
+    {
+      id: "1",
+      question: "How long is the delivery time for human hair wigs?",
+      answer:
+        "Standard shipping takes 7-12 working days, expedited shipping supports 3-5 days delivery worldwide.",
+      likeCount: 248,
+    },
+    {
+      id: "2",
+      question: "Is the HD lace wig invisible on all skin tones?",
+      answer:
+        "Our HD transparent lace matches all skin tones, melts perfectly without foundation.",
+      likeCount: 186,
+    },
+    {
+      id: "3",
+      question: "Can I dye and bleach the hair extensions?",
+      answer:
+        "100% human hair can be dyed darker or bleached lighter, we recommend professional hairdresser operation.",
+      likeCount: 312,
+    },
+    {
+      id: "4",
+      question: "What payment methods do you support?",
+      answer:
+        "Credit card, PayPal, Apple Pay, Google Pay and bank transfer are all available.",
+      likeCount: 97,
+    },
+    {
+      id: "5",
+      question: "How to return or exchange products?",
+      answer:
+        "Contact our customer service within 30 days after receiving the package for free exchange.",
+      likeCount: 154,
+    },
+  ];
+  return (
+    <>
+      {/* <MobileSearchBar /> */}
+      <section>
+        <Suspense fallback={<FilterListSkeleton />}>
+          {/* <CategoryDetail
+            categoryItem={{
+              description: translation?.description ?? "",
+              name: translation?.name ?? "",
+            }}
+          /> */}
+          <CategoryDesc
+            title={translation?.name ?? ""}
+            description={
+              "Alipearl Hair provides many style best quality but cheap lace wigs,including lace front wigs, lace closure wigs, full lace human hair wigs, HD transparent lace wigs, 13x4 frontal wigs, 13x6 lace front wigs, 360 lace wigs, all textures available straight, body wave, deep wave, water wave, curly hair wigs, all lengths from 8 inch to 40 inch, different hair colors natural black, brown, blonde, highlight, ginger, burgundy etc."
+            } //translation?.description ?? ""}
+          />
+        </Suspense>
+        {/* <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full max-w-screen-2xl mx-auto px-4">
+          <FilterList filterAttributes={filterAttributes} />
+          <SortOrder sortOrders={SortByFields} title="Sort by" />
+        </div> */}
+        <ProductListing
+          categoryId={numericId}
+          filterAttributes={filterAttributes}
+          initialProducts={products}
+          initialPageInfo={pageInfo}
+          initialTotal={totalCount}
+          initialQuery={{
+            search: searchValue,
+            sort: sortValue,
+            filters: baseFilterObject,
+            cursor,
+          }}
+        />
+
+        {/* {isArray(products) &&
+          (totalCount > itemsPerPage || pageInfo?.hasNextPage) && (
+            <nav
+              aria-label="Collection pagination"
+              className="my-10 block items-center sm:flex"
+            >
+              <Pagination
+                itemsPerPage={itemsPerPage}
+                itemsTotal={totalCount || 0}
+                currentPage={currentPage}
+                nextCursor={pageInfo?.endCursor}
+                prevCursor={pageInfo?.startCursor}
+              />
+            </nav>
+          )} */}
+        <FaqList faqList={mockFaqData} />
+      </section>
+    </>
+  );
+}

+ 110 - 11
src/app/(public)/customer/account/_components/AccountVipPopup.tsx

@@ -2,9 +2,13 @@
 // import Link from "next/link";
 // 轮播逻辑
 import { Swiper, SwiperSlide } from "swiper/react";
-import { Navigation } from "swiper/modules";
+import { Navigation, Scrollbar } from "swiper/modules";
+import { Swiper as SwiperType } from "swiper/types";
+import { useEffect, useState } from "react";
+import Image from "next/image";
 import "swiper/css";
 import "swiper/css/navigation";
+import { clientFetch } from "@/lib/restApiClient";
 
 // 模拟你的每一页数据,方便维护
 const vipList = [
@@ -109,6 +113,7 @@ const vipList = [
     ],
   },
 ];
+
 export default function AccountVipPopup({
   visible,
   onClose,
@@ -116,9 +121,61 @@ export default function AccountVipPopup({
   visible: boolean;
   onClose: () => void;
 }) {
+  // swiper实例
+  const [_swiperRef, setSwiperRef] = useState<SwiperType | null>(null);
+  // 当前slide下标
+  const [activeIndex, setActiveIndex] = useState(0);
+  const [_loading, setLoading] = useState(true);
+  const [VipGrothData, setVipGrothData] = useState<any>(null);
   // 如果不显示,直接 return null
+  useEffect(() => {
+    const fetchGrothValue = async () => {
+      try {
+        setLoading(true);
+        const res = await clientFetch("/api/customer/growth-value");
+        if (res.data.success) {
+          setVipGrothData(res.data.data);
+          console.log("vip成长值数据:", res.data.data);
+        }
+      } catch (error) {
+        console.error("获取vip成长值失败:", error);
+      } finally {
+        setLoading(false);
+      }
+    };
+    fetchGrothValue();
+  }, []);
   if (!visible) return null;
+  // 滑动切换触发
+  const handleSlideChange = (swiper: SwiperType) => {
+    setActiveIndex(swiper.activeIndex);
+    console.log("aaa slide huadongle ");
+  };
+  const slideItem = vipList[activeIndex];
+  const slideLevelNum = slideItem?.level ?? 1;
 
+  const progressPercent = VipGrothData
+    ? Math.min(
+        Math.max(
+          (VipGrothData.growth_value /
+            VipGrothData.next_level.min_growth_value) *
+            100,
+          0,
+        ),
+        100,
+      )
+    : 0;
+  const userCurrentLevelNum =
+    Number(VipGrothData?.current_level.name.replace(/[^0-9]/g, "")) || 0;
+  let showText = "";
+  console.log("userCurrentLevelNum", userCurrentLevelNum, slideLevelNum);
+  if (userCurrentLevelNum === slideLevelNum) {
+    showText = `Growth Value ${VipGrothData?.growth_value}/${VipGrothData?.next_level.min_growth_value}`;
+  } else if (userCurrentLevelNum > slideLevelNum) {
+    showText = "aaa";
+  } else {
+    showText = `Integral to 2000`;
+  }
   return (
     // 全屏遮罩
     <div className="fixed inset-0 z-50 bg-black/50">
@@ -157,14 +214,17 @@ export default function AccountVipPopup({
     bg-[length:100%_100%]"
               >
                 <div className="absolute top-0.75 left-8 text-4.5 text-[#FFD1B9] font-semibold">
-                  Vip <span className="levelcard">4</span>
+                  Vip <span className="levelcard">{userCurrentLevelNum}</span>
                 </div>
                 <div className="flex flex-col justify-between items-center text-white">
                   <div className="w-18 h-18 mt-6">
-                    <img
-                      className="max-w-full border-0 align-top"
-                      src="https://cdn.alipearlhair.com/media/wysiwyg/newap/202303131637.png "
+                    <Image
+                      src="https://cdn.alipearlhair.com/media/wysiwyg/newap/202303131637.png"
                       alt=""
+                      width={0}
+                      height={0}
+                      sizes="100vw"
+                      className="max-w-full border-0 align-top w-full h-auto"
                     />
                   </div>
                   <p className="text-4.5 mt-3 font-semibold"></p>
@@ -178,19 +238,31 @@ export default function AccountVipPopup({
                 <div className="box-border w-full mt-10.5 mb-9.75">
                   <div className="text-center text-sm text-white mb-3">
                     <span className="account-center-progress-text-pre">
-                      Growth Value 100309/500001
+                      {showText}
                     </span>
                   </div>
                   <div className="relative h-1 bg-[#5F4040]">
-                    <div className="w-4 h-4 rounded-full bg-black absolute top-[-0.1867rem] left-0 block"></div>
-                    <div className="rounded-[1.2821vw] w-[35%] h-full bg-[rgba(255,255,255,1)]"></div>
+                    <div
+                      className="w-4 h-4 rounded-full bg-black absolute top-1/2 -translate-y-1/2  block"
+                      style={{ left: `${progressPercent}%` }}
+                    ></div>
+                    <div
+                      className="rounded-[1.2821vw] h-full bg-[rgba(255,255,255,1)]"
+                      style={{ width: `${progressPercent}%` }}
+                    ></div>
                   </div>
                   {/* 轮播 */}
                   <div className="swiper-container-center vip-swiper">
                     <Swiper
-                      modules={[Navigation]}
-                      navigation // 开启左右箭头
+                      modules={[Navigation, Scrollbar]}
                       slidesPerView={1}
+                      navigation={{
+                        prevEl: ".btn-prev", // 左箭头class
+                        nextEl: ".btn-next", // 右箭头class
+                        disabledClass: "opacity-20", // 滑到首尾自动加这个类,Tailwind直接用
+                      }}
+                      onSwiper={(swiper) => setSwiperRef(swiper)}
+                      onSlideChange={handleSlideChange}
                       spaceBetween={0}
                       className="swiper-container-horizontal"
                     >
@@ -203,7 +275,7 @@ export default function AccountVipPopup({
                           data-level={item.level}
                         >
                           {/* 标题 */}
-                          <h3 className="text-lg font-medium mb-4">
+                          <h3 className="text-lg font-medium mb-4 mt-4">
                             {item.title}
                           </h3>
 
@@ -229,6 +301,33 @@ export default function AccountVipPopup({
                           </div>
                         </SwiperSlide>
                       ))}
+                      {/* 自定义左右按钮,放在Swiper内部,同级slide */}
+                      <button className="btn-prev absolute left-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
+                        {/* 自定义左箭头SVG */}
+                        <svg
+                          width="16"
+                          height="16"
+                          viewBox="0 0 24 24"
+                          fill="none"
+                          stroke="#fff"
+                          stroke-width="2"
+                        >
+                          <path d="M15 18l-6-6 6-6" />
+                        </svg>
+                      </button>
+                      <button className="btn-next absolute right-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
+                        {/* 自定义右箭头SVG */}
+                        <svg
+                          width="16"
+                          height="16"
+                          viewBox="0 0 24 24"
+                          fill="none"
+                          stroke="#fff"
+                          stroke-width="2"
+                        >
+                          <path d="M9 18l6-6-6-6" />
+                        </svg>
+                      </button>
                     </Swiper>
                   </div>
                 </div>

+ 1 - 1
src/app/(public)/customer/account/mycoupon/_components/GiftCardList.tsx

@@ -32,7 +32,7 @@ const GiftCardList: React.FC = () => {
   useEffect(() => {
     const load = async () => {
       const resp = await clientFetch("/api/gift/my-gift-cards");
-      const dataObj = resp.data.data;
+      const dataObj = resp.data;
       setGiftCards((prev) => [...prev, ...dataObj]);
       console.log("完整返回:", dataObj);
     };

+ 2 - 2
src/app/(public)/customer/account/pointsreceivelist/_components/RedeemGiftCard.tsx

@@ -19,10 +19,10 @@ const RedeemGiftCard: React.FC = () => {
   useEffect(() => {
     const load = async () => {
       const resp = await clientFetch("/api/gift/lists");
-      const dataObj = resp.data.data.lists;
+      const dataObj = resp.data.lists;
       const dataArray = Object.values(dataObj);
       setList(dataObj);
-      setmyPoints(resp.data.data.myPoints);
+      setmyPoints(resp.data.myPoints);
        console.log("完整返回:", dataArray,myPoints);
     };
 

+ 33 - 33
src/app/(public)/customer/address/eidt/[id]/page.tsx

@@ -40,39 +40,39 @@ const CustomerAddressEditPage = () => {
     }));
   }, [COUNTRIES]);
   const [actionType, setActionType] = useState<"save" | "delete">("save");
-    // 修改地址到接口
-    const UpdataAddress = async (formData: AddressFormData) => {
-      const res = await clientFetch(`/api/customer/token/address?id=${id}`, {
-        method: "PUT",
-        headers: {
-          "Content-Type": "application/json",
-        },
-        body: JSON.stringify(formData),
-      });
-      const json = await res;
-      console.log("提交成功,返回结果:", json);
-      if(json?.data?.success){
-          window.location.reload();
-      }else{
-        alert("error");
-      }
-      return json;
-    };
+  // 修改地址到接口
+  const UpdataAddress = async (formData: AddressFormData) => {
+    const res = await clientFetch(`/api/customer/token/address?id=${id}`, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(formData),
+    });
+    const json = await res;
+    console.log("提交成功,返回结果:", json);
+    if (json?.data?.success) {
+      window.location.reload();
+    } else {
+      alert("error");
+    }
+    return json;
+  };
   // 删除地址接口
-  const  DeleteAddress =async ()=>{
-     const res = await clientFetch(`/api/customer/token/address?id=${id}`, {
-        method: "DELETE",
-        headers: {
-          "Content-Type": "application/json",
-        }
-      });
-      const json = await res;
-      console.log("删除成功,返回结果:", json);
-  }
+  const DeleteAddress = async () => {
+    const res = await clientFetch(`/api/customer/token/address?id=${id}`, {
+      method: "DELETE",
+      headers: {
+        "Content-Type": "application/json",
+      },
+    });
+    const json = await res;
+    console.log("删除成功,返回结果:", json);
+  };
   // 表单提交
   const addressFormOnSubmit = async (formData: AddressFormData) => {
-    console.log("actionType",actionType);
-    
+    console.log("actionType", actionType);
+
     if (actionType === "save") {
       UpdataAddress(formData);
     } else {
@@ -121,7 +121,7 @@ const CustomerAddressEditPage = () => {
   }, [id, reset]);
   return (
     <div className="w-full h-full">
-      <div  className="bg-[#fff] pr-2.5 pl-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
+      <div className="bg-[#fff] pr-2.5 pl-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
         <Link
           href={"/customer/address"}
           // onClick={() => window.history.back()}
@@ -266,7 +266,7 @@ const CustomerAddressEditPage = () => {
                 type="button"
                 onClick={() => {
                   setActionType("save");
-                  document.getElementById("realSubmit")?.click(); 
+                  document.getElementById("realSubmit")?.click();
                 }}
               >
                 Save Address
@@ -277,7 +277,7 @@ const CustomerAddressEditPage = () => {
                 onClick={() => {
                   if (!confirm("确定删除该地址?")) return;
                   setActionType("delete");
-                  document.getElementById("realSubmit")?.click(); 
+                  document.getElementById("realSubmit")?.click();
                 }}
               >
                 Delete Address

+ 122 - 41
src/app/(public)/customer/order/history/_components/OrderHistory.tsx

@@ -2,7 +2,7 @@
 import { useEffect, useState, useRef, useCallback } from "react";
 import { useRouter } from "next/navigation";
 import Link from "next/link";
-import { useSearchParams } from "next/navigation";
+import { clientFetch } from "@/lib/restApiClient";
 
 // 模拟订单数据
 const mockOrders = [
@@ -15,7 +15,7 @@ const mockOrders = [
         name: "Highlight Wigs For Women Headband Wigs with Blonde Highlights",
         specs: "10, #022, Straight, 5 Free Headbands",
         price: "88 points + $154.05x1",
-        image:"",
+        image: "",
       },
     ],
     progress: "Preparing Order",
@@ -30,7 +30,7 @@ const mockOrders = [
         name: "Alipearl Lace Front Wig 180% Density Body Wave Human Hair Wigs Pre...",
         specs: "10+180%, Medium, No Adjustable Band",
         price: "$128.82x1",
-        image:"",
+        image: "",
       },
     ],
     progress: "Preparing Order",
@@ -45,7 +45,7 @@ const mockOrders = [
         name: "3 Bundles Deep Wave Hair With 4*4 Lace Closure Alipearl Brazilian Hair",
         specs: "8 8 8, Free Part, 8",
         price: "$142.14x1",
-        image:"",
+        image: "",
       },
       {
         name: "Princess Braids Wig Blonde Barbie Pre-Styled Highlight Wig 13×4 Front Wig",
@@ -58,7 +58,7 @@ const mockOrders = [
         name: "Deep Wave Wig 100 Human Hair Swiss Lace Wig Lace Front Wig",
         specs: "10, Medium, 100%",
         price: "$252.00x1",
-         image:"",
+        image: "",
       },
       {
         name: "Braids Wig Blonde Barbie Pre-Styled Highlight Wig 13×4 Front Wig",
@@ -71,7 +71,7 @@ const mockOrders = [
         name: "random free wigs-2",
         specs: "",
         price: "$0.00x1",
-        image:"",
+        image: "",
       },
     ],
     progress: "Preparing Order",
@@ -85,64 +85,129 @@ const tabs = [
   { key: "all", label: "All" },
   { key: "processing", label: "Processing" },
   { key: "complete", label: "Complete" },
-  { key: "Payment", label: "Payment Review" }, // 补充模拟标签,测试滚动
+  { key: "pending_payment", label: "Payment Review" }, // 补充模拟标签,测试滚动
 ];
 
 export default function OrderHistory() {
+  const [loadMoreLoading, setLoadMoreLoading] = useState(false);
+  const [page, setPage] = useState(1);
+  const [total, setTotal] = useState(0);
+  const PAGE_SIZE = 10;
+  // 是否还有下一页
+  const hasMore = page * PAGE_SIZE < total;
+  //  防抖定时器ref
+  const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
+  const DEBOUNCE_DELAY = 500; // 防抖延迟毫秒
+  const sentinelRef = useRef<HTMLDivElement>(null);
+  const listWrapRef = useRef<HTMLDivElement>(null);
   const router = useRouter();
   const [activeTab, setActiveTab] = useState("all");
-  const [filteredOrders, setFilteredOrders] = useState(mockOrders);
-  // 组件内新增
-  const searchParams = useSearchParams();
+  const [filteredOrders, setFilteredOrders] = useState<any[]>([]);
   const lastHashRef = useRef<string>("");
-  // 模拟接口请求:不同标签传不同参数
-  const fetchOrders = (tabKey: string) => {
-    console.log("请求接口参数:", tabKey); // 实际项目替换为真实接口调用
-    // 这里简化逻辑,仅过滤Processing状态(实际接口返回对应数据)
-    if (tabKey === "processing") {
-      setFilteredOrders(mockOrders.filter((o) => o.status === "Processing"));
-    } else if (tabKey === "complete") {
-      setFilteredOrders([]); // 模拟无完成订单
-    } else {
-      setFilteredOrders(mockOrders);
+  const fetchOrders = async (tabKey: string, isLoadMore = false) => {
+    if (loadMoreLoading) return;
+    setLoadMoreLoading(true);
+    console.log("请求接口参数:", tabKey, page, isLoadMore);
+    try {
+      const resp = await clientFetch(
+        `/api/customer/orders?page=${page}&limit=${PAGE_SIZE}&status=${tabKey}`,
+      );
+      let records = resp.data.records ?? [];
+      setTotal(resp.data.total ?? 0);
+      // 模拟数据后面换成真正的接口数据
+      setTotal(100);
+      records = mockOrders; //
+      if (isLoadMore) {
+        setFilteredOrders((prev) => [...prev, ...records]);
+      } else {
+        setFilteredOrders(records);
+        setPage(1);
+        listWrapRef.current?.scrollTo({ top: 0 });
+      }
+    } catch (err) {
+      console.error("请求失败", err);
+    } finally {
+      setLoadMoreLoading(false);
     }
   };
-  // 把更新状态+请求的逻辑抽离,Effect 只调用这个函数,不直接写setState
+
   const updateTabByHash = useCallback(
     (hashKey: string) => {
+      console.log("执行切换tab逻辑", hashKey);
       const validTab = tabs.find((t) => t.key === hashKey.toLowerCase());
       const targetKey = validTab ? validTab.key : "all";
-      // setState 全部放在回调函数里,不在 useEffect 顶层同步执行
-      setActiveTab(targetKey);
-      fetchOrders(targetKey);
+      // setActiveTab(targetKey);
+      fetchOrders(targetKey, false);
     },
-    [tabs, fetchOrders],
+    [tabs],
   );
-
-  // 监听哈希变化,Effect 内部无任何直接setState
   useEffect(() => {
-    const currentHash = searchParams.get("") || "all";
-    // 对比上一次缓存的hash,避免重复执行
-    if (lastHashRef.current !== currentHash) {
-      lastHashRef.current = currentHash;
-      // 仅调用抽离好的函数,Effect内部没有setActiveTab
-      updateTabByHash(currentHash);
+    const getHash = () =>
+      window.location.hash.replace("#", "").toLowerCase() || "all";
+    const hash = getHash();
+    if (lastHashRef.current !== hash) {
+      lastHashRef.current = hash;
+      updateTabByHash(hash);
     }
-  }, [searchParams, updateTabByHash]);
+  }, [updateTabByHash]);
+
+  // 专门用来销毁定时器,防止内存泄漏,依赖空数组只执行一次挂载/卸载
+  useEffect(() => {
+    return () => {
+      if (debounceTimerRef.current) {
+        clearTimeout(debounceTimerRef.current);
+      }
+    };
+  }, []);
+  useEffect(() => {
+    const sentinel = sentinelRef.current;
+    const wrap = listWrapRef.current;
+    if (!sentinel || !wrap) return;
+
+    const observer = new IntersectionObserver(
+      (entries) => {
+        const entry = entries[0];
+        if (entry.isIntersecting && hasMore && !loadMoreLoading) {
+          const nextPage = page + 1;
+          setPage(nextPage);
+          fetchOrders(activeTab, true);
+        }
+      },
+      { root: wrap, rootMargin: "200px 0px 0px 0px" },
+    );
 
+    observer.observe(sentinel);
+    return () => observer.disconnect();
+  }, [activeTab, hasMore, loadMoreLoading, page]);
   // 点击标签切换
   const handleTabClick = (tabKey: string) => {
-    // window.location.hash = tabKey; // 更新URL哈希
-    router.push(`#${tabKey}`, { scroll: false });
+    // 点击立刻更新激活态,按钮样式实时切换
     setActiveTab(tabKey);
-    fetchOrders(tabKey);
+
+    // 清除上一次防抖任务
+    if (debounceTimerRef.current) {
+      clearTimeout(debounceTimerRef.current);
+    }
+
+    // 防抖:延迟更新URL + 请求接口
+    debounceTimerRef.current = setTimeout(() => {
+      const hash = tabKey.toLowerCase();
+      if (lastHashRef.current !== hash) {
+        lastHashRef.current = hash;
+        router.push(`#${tabKey}`, { scroll: false });
+        updateTabByHash(hash); // 仅负责调接口
+      }
+    }, DEBOUNCE_DELAY);
   };
 
   return (
     <>
       <div className="min-h-screen bg-gray-50">
         {/* 顶部返回栏 + 标题 */}
-        <Link href={"/customer/account"} className="sticky top-0 z-20 bg-white px-4 py-3 border-b flex justify-between items-center w-full">
+        <Link
+          href={"/customer/account"}
+          className="sticky top-0 z-20 bg-white px-4 py-3 border-b flex justify-between items-center w-full"
+        >
           <button className="text-black text-xl mr-4">
             <svg
               className="w-6 h-6"
@@ -184,8 +249,8 @@ export default function OrderHistory() {
         </div>
 
         {/* 订单列表 */}
-        <div className="px-4 py-3">
-          {filteredOrders.length === 0 ? (
+        <div className="px-4 py-3  h-[72vh] overflow-y-auto" ref={listWrapRef}>
+          {filteredOrders.length === 0 && !loadMoreLoading ? (
             <div className="text-center py-8 text-gray-500">
               No orders found
             </div>
@@ -239,7 +304,7 @@ export default function OrderHistory() {
                 {/* 订单进度条 */}
                 <Link
                   href={`/customer/order/track/order_id/${order.id}`}
-                  className="mb-4 h-8 bg-gray-100 rounded-full h-2 flex items-center px-2"
+                  className="mb-4  bg-gray-100 rounded-full h-8 flex items-center px-2"
                 >
                   <span className="ml-2 text-xs text-gray-700 flex-1 flex justify-between">
                     {order.progress}
@@ -300,6 +365,22 @@ export default function OrderHistory() {
               </div>
             ))
           )}
+          {/* 无限滚动哨兵:监听触底 */}
+          <div ref={sentinelRef} className="h-1"></div>
+
+          {/* 加载更多 loading */}
+          {loadMoreLoading && (
+            <div className="text-center py-4 text-gray-500">
+              Loading more orders...
+            </div>
+          )}
+
+          {/* 无下一页提示 */}
+          {!hasMore && filteredOrders.length > 0 && !loadMoreLoading && (
+            <div className="text-center py-4 text-gray-400 text-sm">
+              No more orders
+            </div>
+          )}
         </div>
       </div>
     </>

Разлика између датотеке није приказан због своје велике величине
+ 119 - 24
src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx


+ 59 - 0
src/app/(public)/customer/order/track/order_id/[id]/_components/TrackModal.tsx

@@ -0,0 +1,59 @@
+export interface TrackRecord {
+  status: string;
+  location: string;
+  time: string;
+}
+
+
+interface TrackModalProps {
+  visible: boolean;
+  trackList: TrackRecord[];
+  onClose: () => void;
+}
+
+export default function TrackModal({ visible, trackList, onClose }: TrackModalProps) {
+  if (!visible) return null;
+
+  return (
+    <div
+      className="fixed inset-0 bg-black/60 flex items-center justify-center z-[9999]"
+      onClick={onClose}
+    >
+      <div
+        className="w-[min(90%,600px)] max-h-[80vh] bg-white rounded-lg flex flex-col"
+        onClick={(e) => e.stopPropagation()}
+      >
+        <div className="flex items-center justify-between px-5 py-4 border-b border-gray-200">
+          <h3 className="text-base font-semibold m-0">Package Tracking Info (USPS)</h3>
+          <button
+            onClick={onClose}
+            className="bg-transparent border-none text-xl cursor-pointer px-2 hover:text-gray-500 transition-colors"
+          >
+            ×
+          </button>
+        </div>
+
+        <div className="px-5 py-4 overflow-y-auto flex-1">
+          {trackList.length === 0 ? (
+            <p className="text-gray-500 text-center py-8">No logistics data available</p>
+          ) : (
+            trackList.map((item, idx) => (
+              <div key={idx} className="py-3">
+                <div className="flex justify-between gap-3 mb-1.5">
+                  <span className="font-medium">{item.status}</span>
+                  <span className="text-sm text-gray-600 whitespace-nowrap">{item.time}</span>
+                </div>
+                <div className="text-sm text-gray-500">
+                  Location: {item.location || "Unknown Hub"}
+                </div>
+                {idx !== trackList.length - 1 && (
+                  <div className="w-full h-px bg-gray-200 mt-3"></div>
+                )}
+              </div>
+            ))
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 55 - 55
src/app/(public)/customer/order/track/order_id/[id]/page.tsx

@@ -1,74 +1,74 @@
 import OrderHeader from "../../../view/order_id/[id]/_components/OrderHeader";
 import AccountSwiper from "@components/customer/accountSwiper";
-import TrackDetail from "./_components/TrackDetail"
+import TrackDetail from "./_components/TrackDetail";
+import { restApiFetch } from "@utils/bagisto/index";
+import type { OrderData,OrderApiResponse } from "@/types/api/customer/order/vieworder";
 type Params = {
   id: string;
 };
+interface OrderAddress {
+  first_name: string;
+  last_name: string;
+  phone: string;
+  address1: string | null;
+  address2: string | null;
+  city: string;
+  state: string;
+  country: string;
+  postcode: string;
+  company: string;
+}
+export interface AddressData {
+  name: string;
+  line1: string;
+  line2: string;
+}
+
+function formatOrderAddress(addr: OrderAddress): AddressData {
+  // 拼接姓名
+  const fullName = `${addr.first_name} ${addr.last_name}`;
 
+  // line1:街道 + 城市 + 州
+  const addrParts: string[] = [];
+  if (addr.address1) addrParts.push(addr.address1);
+  if (addr.address2) addrParts.push(addr.address2);
+  addrParts.push(`${addr.city}, ${addr.state}`);
+  const line1 = addrParts.join(", ");
+
+  // line2:邮编 + 国家
+  const line2 = `${addr.postcode}, ${addr.country}`;
+
+  return {
+    name: fullName,
+    line1,
+    line2,
+  };
+}
 export default async function TrackOrderDetail({ params }: { params: Params }) {
   // 拿到动态订单ID:211600
-  const orderId = params.id;
+  // 解包拿到真实订单ID
+  const realParams = await params;
+  let orderId = realParams.id;
+  orderId = "9";
+  console.log("orderId:", orderId);
 
-  // === 服务端请求订单接口,传入订单号 ===
-//   const res = await fetch(
-//     `${process.env.API_URL}/api/order/detail?id=${orderId}`,
-//     {
-//       next: { revalidate: 0 },
-//     },
-//   );
-  //   const orderData = await res.json();
-  console.log("orderId:",orderId);
-  // const orderData = {
-  //   shipTo: "Zhang Zhenfei,\nAaaaaaaa, Zzzzzz, Alaska\n90602, United States",
-  //   orderNo: orderId, // 自动使用路由传入的订单号
-  //   status: "Processing",
-  //   orderDate: "March 6, 2026",
-  //   shippingMethod: "USPS/FedEx(2-7 Business Days)",
-  //   paymentMethod: "Klarna",
-  //   CustomerEmail:"18567768109@163.Com",
-  //   paymentDetail: {
-  //     Customer: "18567768109",
-  //     Email: "@163.Com",
-  //     "Payment Method": "PayoneerKlarna",
-  //     "Payment Status": "Processing",
-  //     "Order No": "100055676",
-  //     "Customer Firstname": "Zhang",
-  //     "Customer Lastname": "Zhenfei",
-  //   },
-  //   // 多商品数组,自动兼容1件/多件商品
-  //   items: [
-  //     {
-  //       name: "Human Hair Headband Wigs Curly Bob Wig Straight Hair Wigs",
-  //       spec: "8, Straight, 5 Free Headbands.",
-  //       price: "$114.11",
-  //       qty: 1,
-  //     },
-  //     // 你可以取消注释,测试多商品渲染
-  //     /*
-  //     {
-  //       name: "Deep Wave Lace Front Human Hair Wig",
-  //       spec: "13x4 Lace, Medium Cap, 180% Density",
-  //       price: "$252.00",
-  //       qty: 2,
-  //     },
-  //     */
-  //   ],
-  //   price: {
-  //     subtotal: "$114.11",
-  //     shipping: "$0.00",
-  //     insurance: "$3.42",
-  //     total: "$117.53",
-  //   },
-  // };
+  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  const res = await restApiFetch<OrderApiResponse>({
+    api: `/customer/orders/${orderId}`,
+    method: "GET",
+  });
+  const orderData: OrderData = res.body.data;
+  const shippingAddr: AddressData = formatOrderAddress(orderData.shipping_address);
+  console.log("父组件 orderData:", orderData, !!orderData);
   return (
     <div className="bg-gray-50 min-h-screen">
       {/* 顶部固定导航栏(客户端组件:返回按钮) */}
       <OrderHeader title="Track Order" />
 
       <div className="px-5 py-4">
-        <TrackDetail />
+        <TrackDetail address={shippingAddr} id={orderId} />
       </div>
-         <AccountSwiper/>
+      <AccountSwiper />
     </div>
   );
 }

+ 28 - 27
src/app/(public)/customer/order/view/order_id/[id]/_components/OrderInfoBlock.tsx

@@ -1,49 +1,50 @@
+// OrderInfoBlock.tsx
+import type { OrderData } from "@/types/api/customer/order/vieworder";
+
 interface Props {
-  data: {
-    shipTo: string;
-    orderNo: string;
-    status: string;
-    orderDate: string;
-    shippingMethod: string;
-    paymentMethod: string;
-    CustomerEmail:string;
-    paymentDetail: Record<string, string>;
-  };
+  orderData: OrderData; // 直接传入完整订单对象
 }
 
-export default function OrderInfoBlock({ data }: Props) {
+export default function OrderInfoBlock({ orderData }: Props) {
+  console.log("orderData:::",orderData);
+  
+  // 拼接收货信息
+  const shipToText = `${orderData.shipping_address.first_name} ${orderData.shipping_address.last_name}, 
+Phone: ${orderData.shipping_address.phone}, 
+${orderData.shipping_address.city}, ${orderData.shipping_address.state}, ${orderData.shipping_address.country} ${orderData.shipping_address.postcode}`;
+
   return (
     <>
       <h2 className="text-xl font-bold mb-6 border-b pb-3">Order Information</h2>
 
-      {/* 两列网格布局,所有行严格对齐,Payment区域不会错位 */}
+      {/* 两列网格布局 */}
       <div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-5">
         <div className="font-medium text-gray-700">Ship To:</div>
-        <div>{data.shipTo}</div>
+        <div>{shipToText}</div>
 
         <div className="font-medium text-gray-700">Order Number:</div>
-        <div>{data.orderNo}</div>
+        <div>{orderData.increment_id}</div>
 
         <div className="font-medium text-gray-700">Status:</div>
-        <div>{data.status}</div>
+        <div>{orderData.status}</div>
 
         <div className="font-medium text-gray-700">Order Date:</div>
-        <div>{data.orderDate}</div>
+        <div>{orderData.created_at}</div>
 
         <div className="font-medium text-gray-700">Customer Email:</div>
-        <div>{data.CustomerEmail}</div>
+        <div>{orderData.customer_email}</div>
+
         <div className="font-medium text-gray-700">Shipping Method:</div>
-        <div>{data.shippingMethod}</div>
+        <div>{orderData.shipping_title}</div>
+
         <div className="font-medium text-gray-700">Payment Method:</div>
-        <div className="bg-pink-300 px-3 py-1 rounded w-fit">Klarna</div>
-        {/* <div className="flex flex-col gap-2">
-          <span className="bg-pink-300 px-3 py-1 rounded w-fit">Klarna.</span>
-          <div className="space-y-1">
-            {Object.entries(data.paymentDetail).map(([key, val]) => (
-              <div key={key}>{key} {val}</div>
-            ))}
-          </div>
-        </div> */}
+        <div className="bg-pink-300 px-3 py-1 rounded w-fit">
+          {orderData.payment.method_title}
+        </div>
+
+        {/* 可选:显示总价信息 */}
+        <div className="font-medium text-gray-700">Total Amount:</div>
+        <div>{orderData.order_currency_code} {orderData.grand_total}</div>
       </div>
     </>
   );

+ 48 - 16
src/app/(public)/customer/order/view/order_id/[id]/_components/PriceSummary.tsx

@@ -1,39 +1,71 @@
-interface PriceData {
-  subtotal: string;
-  shipping: string;
-  insurance: string;
-  total: string;
-}
+// PriceSummary.tsx
+import type { OrderData } from "@/types/api/customer/order/vieworder";
 
 interface Props {
-  priceData: PriceData;
+  orderData: OrderData;
 }
 
-export default function PriceSummary({ priceData }: Props) {
+export default function PriceSummary({ orderData }: Props) {
+  const currency = orderData.order_currency_code;
+  const subtotal = orderData.sub_total;
+  const shipping = orderData.shipping_amount;
+  const grandTotal = orderData.grand_total;
+  // 接口无保险费用,设为0
+  const insurance = 0;
+
   return (
     <div className="mt-6">
       <div className="space-y-3 border-t pt-4">
         <div className="flex justify-between text-gray-700">
           <span>Subtotal</span>
-          <span>{priceData.subtotal}</span>
+          <span>
+            {currency} {subtotal}
+          </span>
         </div>
         <div className="flex justify-between text-gray-700">
           <span>Shipping & Handling</span>
-          <span>{priceData.shipping}</span>
-        </div>
-        <div className="flex justify-between text-gray-700">
-          <span>Insurance (Insurance for Lost)</span>
-          <span>{priceData.insurance}</span>
+          <span>
+            {currency} {shipping}
+          </span>
         </div>
+        {insurance > 0 && (
+          <div className="flex justify-between text-gray-700">
+            <span>Insurance (Insurance for Lost)</span>
+            <span>
+              {currency} {insurance}
+            </span>
+          </div>
+        )}
+        {/* 可额外增加 Tax / Discount 行 */}
+        {/* Tax:不等于0才显示 */}
+        {orderData.tax_amount !== 0 && (
+          <div className="flex justify-between text-gray-700">
+            <span>Tax</span>
+            <span>
+              {currency} {orderData.tax_amount}
+            </span>
+          </div>
+        )}
+        {/* Discount:不等于0才显示 */}
+        {orderData.discount_amount !== 0 && (
+          <div className="flex justify-between text-gray-700">
+            <span>Discount</span>
+            <span>
+              {currency} {orderData.discount_amount}
+            </span>
+          </div>
+        )}
       </div>
 
       {/* 底部总价栏,浅黄色背景 */}
       <div className="bg-amber-50 mt-4 py-3 px-2 rounded">
         <div className="flex justify-between items-center">
           <p className="text-lg font-bold">Grand Total</p>
-          <p className="text-lg font-bold">{priceData.total}</p>
+          <p className="text-lg font-bold">
+            {currency} {grandTotal}
+          </p>
         </div>
       </div>
     </div>
   );
-}
+}

+ 14 - 25
src/app/(public)/customer/order/view/order_id/[id]/_components/ProductList.tsx

@@ -1,40 +1,29 @@
-interface ProductItem {
-  name: string;
-  spec: string;
-  price: string;
-  qty: number;
-  image:string;
-}
+// ProductList.tsx
+import type { OrderData } from "@/types/api/customer/order/vieworder";
 
 interface Props {
-  items: ProductItem[];
+  orderItems: OrderData["items"];
+  currencyCode: string;
 }
 
-export default function ProductList({ items }: Props) {
+export default function ProductList({ orderItems, currencyCode }: Props) {
   return (
     <div className="mb-6 space-y-4">
-      {items.map((item, index) => (
-        <div className="flex items-center justify-between gap-3" key={index}>
-          {item.image ? (
-            <img
-              src={item.image}
-              alt={item.name}
-              className="w-22.5 h-23 object-cover rounded shrink-0"
-            />
-          ) : (
-            <div className="w-31 h-23 bg-gray-100 rounded shrink-0"></div>
-          )}
+      {orderItems.map((item, index) => (
+        <div className="flex items-center gap-3" key={item.id ?? index}>
+          {/* 暂无商品原图:用占位框 */}
+          <div className="w-24 h-24 bg-gray-100 rounded shrink-0"></div>
 
-          <div>
+          <div className="flex-1">
             <p className="font-medium">{item.name}</p>
-            <p className="text-sm text-gray-600 my-1">{item.spec}</p>
+            <p className="text-sm text-gray-600 my-1">SKU: {item.sku ?? "-"}</p>
             <div className="flex justify-between">
-              <span>{item.price}</span>
-              <span>X {item.qty}</span>
+              <span>{currencyCode} {item.price}</span>
+              <span>× {item.qty_ordered}</span>
             </div>
           </div>
         </div>
       ))}
     </div>
   );
-}
+}

+ 22 - 59
src/app/(public)/customer/order/view/order_id/[id]/page.tsx

@@ -2,67 +2,27 @@ import OrderHeader from "./_components/OrderHeader";
 import OrderInfoBlock from "./_components/OrderInfoBlock";
 import ProductList from "./_components/ProductList";
 import PriceSummary from "./_components/PriceSummary";
-
+import {restApiFetch} from "@utils/bagisto/index";
+import type {  OrderData,OrderApiResponse } from "@/types/api/customer/order/vieworder";
 type Params = {
   id: string;
 };
-
 export default async function OrderDetail({ params }: { params: Params }) {
-  // 拿到动态订单ID:211600
-  const orderId = params.id;
+  // 解包拿到真实订单ID
+  const realParams = await params;
+  let orderId = realParams.id;
+  orderId = '9';
+  console.log("orderId:", orderId);
+
+  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  const res = await restApiFetch<OrderApiResponse>({
+    api:`/customer/orders/${orderId}`,
+    method:"GET",
+  }
+  );
 
-  // === 服务端请求订单接口,传入订单号 ===
-//   const res = await fetch(
-//     `${process.env.API_URL}/api/order/detail?id=${orderId}`,
-//     {
-//       next: { revalidate: 0 },
-//     },
-//   );
-  //   const orderData = await res.json();
-  console.log("orderId:",orderId);
-  const orderData = {
-    shipTo: "Zhang Zhenfei,\nAaaaaaaa, Zzzzzz, Alaska\n90602, United States",
-    orderNo: orderId, // 自动使用路由传入的订单号
-    status: "Processing",
-    orderDate: "March 6, 2026",
-    shippingMethod: "USPS/FedEx(2-7 Business Days)",
-    paymentMethod: "Klarna",
-    CustomerEmail:"18567768109@163.Com",
-    paymentDetail: {
-      Customer: "18567768109",
-      Email: "@163.Com",
-      "Payment Method": "PayoneerKlarna",
-      "Payment Status": "Processing",
-      "Order No": "100055676",
-      "Customer Firstname": "Zhang",
-      "Customer Lastname": "Zhenfei",
-    },
-    // 多商品数组,自动兼容1件/多件商品
-    items: [
-      {
-        name: "Human Hair Headband Wigs Curly Bob Wig Straight Hair Wigs",
-        spec: "8, Straight, 5 Free Headbands.",
-        price: "$114.11",
-        image:"",
-        qty: 1,
-      },
-      // 你可以取消注释,测试多商品渲染
-      /*
-      {
-        name: "Deep Wave Lace Front Human Hair Wig",
-        spec: "13x4 Lace, Medium Cap, 180% Density",
-        price: "$252.00",
-        qty: 2,
-      },
-      */
-    ],
-    price: {
-      subtotal: "$114.11",
-      shipping: "$0.00",
-      insurance: "$3.42",
-      total: "$117.53",
-    },
-  };
+  const orderData: OrderData = res.body.data;
+  console.log("父组件 orderData:", orderData, !!orderData); 
   return (
     <div className="bg-gray-50 min-h-screen">
       {/* 顶部固定导航栏(客户端组件:返回按钮) */}
@@ -70,7 +30,7 @@ export default async function OrderDetail({ params }: { params: Params }) {
 
       <div className="px-5 py-4">
         {/* 收货信息、支付信息区块 */}
-        <OrderInfoBlock data={orderData} />
+        <OrderInfoBlock orderData={orderData} />
 
         {/* 分享按钮区域 */}
         <div className="my-6">
@@ -84,10 +44,13 @@ export default async function OrderDetail({ params }: { params: Params }) {
         </div>
 
         {/* 商品列表:循环渲染,自动兼容1件或多件商品 */}
-        <ProductList items={orderData.items} />
+            <ProductList
+          orderItems={orderData.items}
+          currencyCode={orderData.order_currency_code}
+        />
 
         {/* 金额汇总 */}
-        <PriceSummary priceData={orderData.price} />
+        <PriceSummary orderData={orderData} />
       </div>
     </div>
   );

+ 21 - 6
src/app/(public)/page.tsx

@@ -1,25 +1,40 @@
 // import { GET_THEME_CUSTOMIZATION } from "@/graphql";
 // import RenderThemeCustomization from "@components/home/RenderThemeCustomization";
-import HomeImageBanner from "@components/home/HomeImageBanner";
+// import HomeImageBanner from "@components/home/HomeImageBanner";
 // import { ThemeCustomizationResponse } from "@/types/theme/theme-customization";
 // import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
-import HomeContent from "./_components/HomeContent"
+import { DatePicker } from "@heroui/date-picker";
+import HomeContent from "./_components/HomeContent";
+import { restApiFetch } from "@utils/bagisto/index";
+import {HomeApiData} from "@/types/home/type"
 export const revalidate = 3600;
 
+
 export default async function Home() {
-  // const 
+  // const
   // const {data} = await cachedGraphQLRequest<ThemeCustomizationResponse>(
   //   "home",
   //   GET_THEME_CUSTOMIZATION,
   //   { first: 20 }
   // );
+// 2. 服务端 fetch 请求我们自己的 Next.js API
+const res = await restApiFetch({
+  api: `/home`,
+  method: "GET",
+  isCookies: true, // 关键开关
+  headers: {
+    "X-Channel": "wap",
+  },
+});
+console.log("res----------------------",res);
+const homeData: HomeApiData = res.body.data;
 
   return (
     <>
-      <HomeImageBanner />
+      {/* <HomeImageBanner /> */}
+      <DatePicker label={"Birth date"} labelPlacement={"outside"} />
       {/* <RenderThemeCustomization themeCustomizations={data?.themeCustomizations ?? {edges: []}} /> */}
-      <HomeContent />
+      <HomeContent homeData={homeData} />
     </>
-    
   );
 }

+ 7 - 1
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -9,7 +9,9 @@ import { useConfig } from "@utils/hooks/useConfig";
 import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
 import Faqs from "./Faqs";
 
-
+/**
+ * 订单详情里的金额的货币符号不会根据用户选择的货币改变 ,而是固定为下单时使用的货币
+ */
 
 function getProductAdditionalInfo(productItem: ProductItemAdditional) { 
     const attributeKeys = Object.keys(productItem.attributes);
@@ -186,6 +188,10 @@ export default function OrderDetailWrapper({
                     <p className="text-ly-12 leading-ly-20">Discount</p>
                     <p className="text-ly-12 leading-ly-20">{currency && currency.symbol}{orderDetailRes?.discountAmount}</p>
                 </div>
+                <div className="flex justify-between mt-3 first:mt-0">
+                    <p className="text-ly-12 leading-ly-20">Gift Card</p>
+                    <p className="text-ly-12 leading-ly-20">-{currency && currency.symbol}{orderDetailRes?.giftcardAmount}</p>
+                </div>
                 {/* <div className="flex justify-between mt-3 first:mt-0">
                     <p className="text-ly-12 leading-ly-20">You Earn</p>
                     <p className="text-ly-12 leading-ly-20">600 Reward Points</p>

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

@@ -1,15 +1,14 @@
 import { notFound } from "next/navigation";
 import { Suspense } from "react";
 // import clsx from "clsx";
-
+// import type { ProductReviewList } from "@/types/products/productDetail";
+// import { getProductReviews } from "@utils/hooks/getProductReviews";
+import { restApiFetch } from "@utils/bagisto/index";
 import {
   ProductDetailSkeleton,
   RelatedProductSkeleton,
 } from "@/components/common/skeleton/ProductSkeleton";
-import {
-  BASE_SCHEMA_URL,
-  PRODUCT_TYPE,
-} from "@/utils/constants";
+import { BASE_SCHEMA_URL, PRODUCT_TYPE } from "@/utils/constants";
 import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
 import { cachedProductRequest } from "@/utils/hooks/useCache";
 import {
@@ -19,26 +18,26 @@ import {
 } from "@components/catalog/type";
 import { RelatedProductsSection } from "@components/catalog/product/RelatedProductsSection";
 import { HeroCarouselShimmer } from "@components/common/slider";
-import {ProductMedia} from "@/app/(public)/product/_components/ProductMedia";
+import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
 import { ProductInformation } from "@/app/(public)/product/_components/ProductInformation";
 import { ProductShortDescription } from "@/app/(public)/product/_components/ProductShortDescription";
 import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
-import { ProductReviewSection } from "@/app/(public)/product/_components/ProductReviewSection";
-import {
-    formatFlexibleVariants
-} from "@/utils/variantTools";
+import SwitchButton from "../_components/SwitchButton";
+import ShopServicePanel from "../_components/ShopServicePanel";
+import Recommend from "../_components/youmaylike/Recommend"
+import { formatFlexibleVariants } from "@/utils/variantTools";
 
 // export const dynamic = 'auto'
 // // 'auto' | 'force-dynamic' | 'error' | 'force-static'
-export const dynamic = 'force-dynamic';
+export const dynamic = "force-dynamic";
 async function getSingleProduct(urlKey: string) {
-
   try {
-    const {data:dataById} = await cachedProductRequest<SingleProductResponse>(
-      // urlKey, // 产品名称
-      GET_PRODUCT_BY_URL_KEY, // gql查询语句
-      { urlKey: urlKey },
-    );
+    const { data: dataById } =
+      await cachedProductRequest<SingleProductResponse>(
+        // urlKey, // 产品名称
+        GET_PRODUCT_BY_URL_KEY, // gql查询语句
+        { urlKey: urlKey },
+      );
 
     const product = dataById?.product || null;
 
@@ -68,7 +67,17 @@ export default async function ProductPage({
   const product = await getSingleProduct(fullPath);
   console.log('ProductPage product +++++++++ ', product);
   if (!product) return notFound();
-
+  // const allReviews: ProductReviewList = await getProductReviews(
+  //   String(product._id),
+  // );
+  // 3792
+  const res = await restApiFetch({
+  api: `/shop/products/${3792}/reviews?page=1&per_page=10&has_images=0&sort=all`,
+  method: "GET",
+});
+const allReviews = res?.body?.data;
+console.log("res----------------------",allReviews);
+  const questionTotal = 73;
   // const imageUrl = getImageUrl(product?.baseImageUrl, baseUrl, NOT_IMAGE);
 
   // JSON-LD数据格式,便于搜索引擎识别页面信息,利于seo
@@ -80,16 +89,20 @@ 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 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 reviews = Array.isArray(product?.reviews?.edges)
-    // ? product?.reviews.edges.map((e) => e.node)
-    // : [];
+  // const reviews = Array.isArray(product?.reviews?.edges)
+  // ? product?.reviews.edges.map((e) => e.node)
+  // : [];
 
   // const VariantImages = isArray(product?.variants?.edges)
   //   ? product?.variants.edges.map(
@@ -99,40 +112,49 @@ export default async function ProductPage({
 
   return (
     <>
-        <script //SEO
-            dangerouslySetInnerHTML={{
-            __html: JSON.stringify(productJsonLd),
-            }}
-            type="application/ld+json"
-        />
-        <div className="w-full">
-            <Suspense fallback={<HeroCarouselShimmer />}>
-                <ProductMedia mediaData={mediaImgs} />
-            </Suspense>
-        </div>
-
-
-        
-
-        <div className="">
-            <Suspense fallback={<ProductDetailSkeleton />}>
-                <ProductInformation key={product._id}
-                    productId={product._id}
-                    name={product?.name || ''}
-                    productOptions={productOptions}
-                    flexibleVariants={flexibleVariants}
-                    isSaleable={product.isSaleable}
-                />
-                <ProductShortDescription shortDescription={product.shortDescription || ''} />
-                <ProductDetail detail={product.description || ''} />
-
-                <ProductReviewSection productId={String(product._id)} />
-            </Suspense>
-        </div>
-
-        <Suspense fallback={<RelatedProductSkeleton />}>
-            <RelatedProductsSection fullPath={fullPath} />
+      <script //SEO
+        dangerouslySetInnerHTML={{
+          __html: JSON.stringify(productJsonLd),
+        }}
+        type="application/ld+json"
+      />
+      <div className="w-full">
+        <Suspense fallback={<HeroCarouselShimmer />}>
+          <ProductMedia mediaData={mediaImgs} />
+        </Suspense>
+      </div>
+
+      <div className="">
+        <Suspense fallback={<ProductDetailSkeleton />}>
+          <ProductInformation
+            key={product._id}
+            productId={product._id}
+            name={product?.name || ""}
+            productOptions={productOptions}
+            flexibleVariants={flexibleVariants}
+            isSaleable={product.isSaleable}
+          />
+          <ShopServicePanel />
+          <ProductShortDescription
+            shortDescription={product.shortDescription || ""}
+          />
+
+          <SwitchButton
+            productId={"3792" || String(product._id)}
+            reviewList={allReviews}
+            reviewTotal={allReviews.length}
+            questionTotal={questionTotal}
+            questionList={["aaa"]}
+          />
+          <ProductDetail detail={product.description || ""} />
+          {/* <ProductReviewSection reviews={allReviews} productId={String(product._id)} /> */}
         </Suspense>
+        <Recommend/>
+      </div>
+        
+      <Suspense fallback={<RelatedProductSkeleton />}>
+        <RelatedProductsSection fullPath={fullPath} />
+      </Suspense>
     </>
   );
 }

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

@@ -0,0 +1,63 @@
+"use client";
+
+export default function NewUserBanner() {
+  return (
+    <div className="w-full flex items-center justify-between mt-1 bg-[#F9F9F9] px-3 py-2 ">
+      {/* 左侧:用户头像SVG + 文案 */}
+      <div className="flex items-center gap-2">
+        {/* 用户头像绿色SVG */}
+
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          width="16"
+          height="16"
+          viewBox="0 0 16 16"
+          fill="none"
+        >
+          <rect
+            x="0"
+            y="0"
+            width="16"
+            height="16"
+            fill="#CCCCCC"
+            fill-opacity="0"
+          ></rect>
+          <ellipse
+            cx="7.998697280883789"
+            cy="4.666656494140625"
+            rx="2.666666030883789"
+            ry="2.666656494140625"
+            fill="#036141"
+          ></ellipse>
+          <path
+            d="M13.3346 14.0003C13.3346 11.0548 10.9468 8.66699 8.0013 8.66699C5.05578 8.66699 2.66797 11.0548 2.66797 14.0003L13.3346 14.0003Z"
+            fill="#036141"
+          ></path>
+        </svg>
+        {/* 绿色文案 */}
+        <span className="text-[#036141] text-ly-14 leading-ly-22 font-medium">
+          New User Deal · Sign In & Save $200
+        </span>
+      </div>
+
+      {/* 右侧右箭头SVG按钮 */}
+      <button className="shrink-0 hover:opacity-70 transition-opacity">
+        <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="M9 18L15 12L9 6"
+          ></path>
+        </svg>
+      </button>
+    </div>
+  );
+}

+ 181 - 24
src/app/(public)/product/_components/ProductAddToCart.tsx

@@ -1,30 +1,187 @@
 "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,
-    isLoading,
-    onAddToCart,
-    onBuyNow
-}: { 
-    isAvailable: boolean; 
-    isLoading: boolean;
-    onAddToCart: () => void;
-    onBuyNow: () => void;
-}) { 
-    const btnClass = 'flex-none flex items-center justify-center w-40 h-11.5 rounded-4xl text-ly-16 font-bold text-white';
-    return (
-        <div className="fixed w-full bottom-0 left-0 bg-white box-border pt-3 pl-3 pr-3 pb-5 border-t border-solid border-ly-middlegray z-1000">
-            {isAvailable ? (
-                <div className="flex justify-between">
-                    <button onClick={onBuyNow} type="button" className={clsx(btnClass,'bg-ly-deepgreen', { 'opacity-25':isLoading})}>Buy Now</button>
-                    <button onClick={onAddToCart} type="button" className={clsx(btnClass,'bg-ly-middlegreen', { 'opacity-25':isLoading})}>Add To Cart</button>
-                </div>
-            ): (
-                <p>The selected options are not available!</p>
-            )}
-            
+  isAvailable,
+  isLoading,
+  onAddToCart,
+  onBuyNow,
+  priceInfo, // 父组件传入价格数据
+}: {
+  isAvailable: boolean;
+  isLoading: boolean;
+  onAddToCart: () => void;
+  onBuyNow: () => void;
+  priceInfo: VariantPriceInfo; // 价格数据源
+}) {
+  const btnClass =
+    "flex items-center justify-center rounded-full text-ly-16 font-bold text-white h-[46px]";
+  // 滚动控制显示
+  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(
+        "fixed w-full left-0 bg-white box-border pt-3 px-4 pb-6 border-t border-solid border-ly-middlegray z-[1000] transition-all duration-300 ease-in-out",
+        isShowBar ? "bottom-0" : "-bottom-[160px]",
+      )}
+    >
+      {/* 顶部价格区域 */}
+      <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="USD"
+          />
+        )}
+
+        <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="USD"
+            />
+          )}
+          {/* 现价 */}
+          <Price
+            className="  text-base font-bold mr-1"
+            amount={String(totalNowPrice)}
+            currencyCode="USD"
+          />
+          {/* 右侧下拉箭头SVG */}
+          <span onClick={() => setCouponModalOpen(true)}>
+            <ArrowDownSvg />
+          </span>
+        </div>
+      </div>
+
+      {/* 底部按钮行:左侧对话图标 + 两个圆角按钮 */}
+      {isAvailable ? (
+        <div className="flex items-center gap-3">
+          {/* 左侧对话图标 */}
+          <ChatSvg />
+          {/* Buy Now 深色绿按钮 */}
+          <button
+            onClick={onBuyNow}
+            type="button"
+            className={clsx(btnClass, "flex-1 bg-ly-deepgreen", {
+              "opacity-25": isLoading,
+            })}
+          >
+            Buy Now
+          </button>
+          {/* Add To Cart 亮绿色按钮 */}
+          <button
+            onClick={onAddToCart}
+            type="button"
+            className={clsx(btnClass, "flex-1 bg-ly-middlegreen", {
+              "opacity-25": isLoading,
+            })}
+          >
+            Add to Cart
+          </button>
         </div>
-    );
-}
+      ) : (
+        <p className="text-center text-red-500 py-2">
+          The selected options are not available!
+        </p>
+      )}
+      <PriceModal
+        visible={couponModalOpen}
+        onClose={() => setCouponModalOpen(false)}
+        priceInfo={priceInfo}
+      />
+    </div>
+  );
+}

+ 392 - 255
src/app/(public)/product/_components/ProductInformation.tsx

@@ -2,296 +2,433 @@
 
 import { useState, useMemo } from "react";
 import clsx from "clsx";
-import { 
-    ProductOption, 
-    ResolvedVariant
-} from "@/components/catalog/type";
+import { 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 { redirect, RedirectType } from 'next/navigation'
+import { redirect, RedirectType } 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 {
-    isValueAvailable,
-    isOptionValueAvailable,
-    getFirstAvailable,
-    getAvailableVariants,
-    isCombinationAvailable,
-    findNearestAvailableVariant
+  isValueAvailable,
+  isOptionValueAvailable,
+  getFirstAvailable,
+  getAvailableVariants,
+  isCombinationAvailable,
+  findNearestAvailableVariant,
 } from "@/utils/variantTools";
 import { Price } from "@components/theme/ui/Price";
 
-
 export function ProductInformation({
-    name,
-    productId,
-    productOptions,
-    flexibleVariants,
-    isSaleable
+  name,
+  productId,
+  productOptions,
+  flexibleVariants,
+  isSaleable,
 }: {
-    name: string;
-    productId: number;
-    productOptions: ProductOption[];
-    flexibleVariants: ResolvedVariant[];
-    isSaleable: string | undefined;
+  name: string;
+  productId: number;
+  productOptions: ProductOption[];
+  flexibleVariants: ResolvedVariant[];
+  isSaleable: string | undefined;
 }) {
-    const { isCartLoading, onAddToCart } = useAddProduct();
-    const { showToast } = useCustomToast();
-    
-    // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
-    const [lastClickedOptionId, setLastClickedOptionId] = useState<number>(productOptions[0]?.id );
+  const { isCartLoading, onAddToCart } = useAddProduct();
+  const { showToast } = useCustomToast();
+
+  // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
+  const [lastClickedOptionId, setLastClickedOptionId] = useState<number>(
+    productOptions[0]?.id,
+  );
 
-    const [productQty, setProductQty] = useState(1);
+  const [productQty, setProductQty] = useState(1);
+  const [modalOpen, setModalOpen] = useState(false);
+  const [guideModalOpen, setGuideModalOpen] = useState(false);
+  // 获取所有可用变体(quantity > 0)
+  const availableVariants = useMemo(() => {
+    return getAvailableVariants(flexibleVariants);
+  }, [flexibleVariants]);
 
-    // 获取所有可用变体(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> = {};
 
-    // 当前选中的选项值映射: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 defaultSelected = getFirstAvailable(productOptions, flexibleVariants);;
-        if (defaultSelected) {
-            res = defaultSelected;
+  // 计算每个选项值的禁用状态
+  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 {
-            // 极端情况:没有任何可用组合,默认全选每组第一个(但仍需标记为不可用,由禁用逻辑处理)
-            productOptions.forEach((opt) => {
-                res[opt.id] = opt.values[0]?.id;
-            });
+          // 校验其他组的选项值是否可用
+          map[value.id] = !isOptionValueAvailable(
+            option.id,
+            value.id,
+            selected,
+            productOptions,
+            availableVariants,
+          );
         }
-        console.log('set default selected');
-        return res;
+      });
     });
+    return map;
+  }, [selected, lastClickedOptionId, productOptions, availableVariants]);
 
-    // 计算每个选项值的禁用状态
-    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 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;} = useMemo(() => {
-        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 = Number(variant.priceIndices[0].regular_min_price) * productQty;
-            totalNowPrice = Number(variant.priceIndices[0].min_price) * productQty;
-        }
-        if(totalLinePrice !== totalNowPrice) {
-            save = -(totalLinePrice - totalNowPrice); 
-        }
+  // 判断当前选中的组合是否可购买(用于控制购买按钮等)
+  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]);
 
-        return {
-            variant, 
-            totalLinePrice: totalLinePrice, 
-            totalNowPrice: totalNowPrice,
-            save: save
-        };
-    }, [selected, flexibleVariants, productQty]);
+  // 获取当前选中变体和价格等信息
+  // 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 =
+        Number(variant.priceIndices[0].regular_min_price) * productQty;
+      totalNowPrice = Number(variant.priceIndices[0].min_price) * productQty;
+      const dayPrice = totalNowPrice / 30; // 30天一期,日均成本
+      displayDayPrice = Number(dayPrice.toFixed(2));
+    }
+    if (totalLinePrice !== totalNowPrice) {
+      save = -(totalLinePrice - totalNowPrice);
+    }
 
-    const classNameOptionValueBtn = "text-ly-14 leading-ly-24 border border-solid rounded-lg pt-1.5 pb-1.5 pl-3 pr-3 bg-white";
+    return {
+      variant,
+      totalLinePrice: 170 || totalLinePrice,
+      totalNowPrice: 119 || totalNowPrice,
+      save: 51 || save,
+      displayDayPrice: displayDayPrice,
+    };
+  }, [selected, flexibleVariants, productQty]);
 
+  const classNameOptionValueBtn =
+    "text-ly-14 leading-ly-24 border border-solid rounded-lg pt-1.5 pb-1.5 pl-3 pr-3 bg-white";
 
-    const handlerProductQty = (action: string) => {
-        if (action === "increase") {
-            setProductQty(productQty + 1);
-        } else if (action === "decrease" && productQty > 1) {
-            setProductQty(productQty - 1);
-        }
+  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') {
-        
-        const params = { 
-            productId: String(productId), 
-            quantity: productQty,
-            variantId: currentVariantInfo.variant?._id,
-        };
-        const res = await onAddToCart(params);
-        console.log('onAddToCart result --- ', res);
-        if(action === 'buynow') {
-            if(res) {
-                const responseData = res.data?.createAddProductInCart?.addProductInCart;
-                if(responseData && responseData.success) {
-                    redirect('/checkout?step=address', RedirectType.push);
-                }
-            }
+  async function addProductToCart(action: string = "addtocart") {
+    const params = {
+      productId: String(productId),
+      quantity: productQty,
+      variantId: currentVariantInfo.variant?._id,
+    };
+    const res = await onAddToCart(params);
+    console.log("onAddToCart result --- ", res);
+    if (action === "buynow") {
+      if (res) {
+        const responseData = res.data?.createAddProductInCart?.addProductInCart;
+        if (responseData && responseData.success) {
+          redirect("/checkout?step=address", RedirectType.push);
         }
-    } 
+      }
+    }
+  }
 
-    const addToCartHandler = async () => {
-   
-        if(!isCurrentSelectionAvailable) {
-            showToast("The selected options are not available!", "warning");
-            return;
-        }
+  const addToCartHandler = async () => {
+    if (!isCurrentSelectionAvailable) {
+      showToast("The selected options are not available!", "warning");
+      return;
+    }
 
-        if(currentVariantInfo.variant && !isCartLoading) {
-            addProductToCart();
-        }
-    };
+    if (currentVariantInfo.variant && !isCartLoading) {
+      addProductToCart();
+    }
+  };
 
-    const buyNowHandler = () => {
-        if(!isCurrentSelectionAvailable) {
-            showToast("The selected options are not available!", "warning");
-            return;
-        }
-        if(currentVariantInfo.variant && !isCartLoading) {
-            addProductToCart('buynow');
-        }
-    };
+  const buyNowHandler = () => {
+    if (!isCurrentSelectionAvailable) {
+      showToast("The selected options are not available!", "warning");
+      return;
+    }
+    if (currentVariantInfo.variant && !isCartLoading) {
+      addProductToCart("buynow");
+    }
+  };
+  const currencySymbol = "$";
 
+  return (
+    <>
+      <div className="box-border w-full pr-4 pl-4">
+        <h3 className="text-ly-14 text-black mt-4 leading-ly-22">
+          {name} {isSaleable}
+        </h3>
+        <ProductText ProductText="" />
+        <div className="flex items-center mt-2">
+          <Price
+            className="text-ly-16 leading-ly-24 font-bold"
+            amount={String(currentVariantInfo.totalNowPrice)}
+            currencyCode="USD"
+          />
+          {currentVariantInfo.totalLinePrice !==
+            currentVariantInfo.totalNowPrice && (
+            <>
+              <Price
+                className="text-ly-12 leading-ly-20 text-ly-gray line-through ml-1"
+                amount={String(currentVariantInfo.totalLinePrice)}
+                currencyCode="USD"
+              />
+              <Price
+                className="text-ly-12 leading-ly-20 text-ly-gray line-through ml-1"
+                amount={String(currentVariantInfo.totalLinePrice)}
+                currencyCode="USD"
+              />
+              <Price
+                className="text-ly-12 leading-ly-16 bg-ly-gold pr-1.5 pl-1.5 ml-3"
+                amount={String(currentVariantInfo.save)}
+                currencyCode="USD"
+              />
+            </>
+          )}
+        </div>
 
-    return (<>
-        <div className="box-border w-full pr-4 pl-4">
-            <h3 className="text-ly-12 mt-4 leading-ly-22">{name} {isSaleable}</h3>
-      
-            <div className="flex items-center mt-2">
-                <Price 
-                    className="text-ly-16 leading-ly-24 font-bold"
-                    amount={String(currentVariantInfo.totalNowPrice)} 
-                    currencyCode="USD"
-                />
-                {
-                    currentVariantInfo.totalLinePrice !== currentVariantInfo.totalNowPrice && (
-                        <>
-                            <Price 
-                                className="text-ly-12 leading-ly-20 text-ly-gray line-through ml-1"
-                                amount={String(currentVariantInfo.totalLinePrice)} 
-                                currencyCode="USD"
-                            /><Price 
-                                className="text-ly-12 leading-ly-20 text-ly-gray line-through ml-1"
-                                amount={String(currentVariantInfo.totalLinePrice)} 
-                                currencyCode="USD"
-                            />
-                            <Price 
-                                className="text-ly-12 leading-ly-16 bg-ly-gold pr-1.5 pl-1.5 ml-3"
-                                amount={String(currentVariantInfo.save)} 
-                                currencyCode="USD"
-                            />
-                        </>
-                    )
-                }
-                
-            </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 pr-4 pl-4 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-12 font-medium ">{option.label}</div>
 
-        <div className="box-border w-full pr-4 pl-4 mt-6">
-            {productOptions.map((option) => (
-            <div key={option.id}>
-                <div className="text-ly-12 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 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="text-ly-12 font-medium mt-4">Quantity*</div>
-            <div className="w-full flex mt-3">
-                <div className="flex border border-solid rounded-lg">
-                    <button onClick={() => handlerProductQty('decrease')} type="button" className="flex-none w-7.5 h-9 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-9 leading-ly-36 text-center" value={productQty} readOnly />
-                    <button onClick={() => handlerProductQty('increase')} type="button" className="flex-none w-7.5 h-9 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 className="w-full flex justify-between items-center mt-3">
+          <div className="text-ly-14 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>
-        <ProductAddToCart 
-            isAvailable={isCurrentSelectionAvailable} 
-            isLoading={isCartLoading}
-            onAddToCart={addToCartHandler}
-            onBuyNow={buyNowHandler}
-        />
-    </>);
-}
+      </div>
+      <ProductAddToCart
+        isAvailable={isCurrentSelectionAvailable}
+        isLoading={isCartLoading}
+        onAddToCart={addToCartHandler}
+        onBuyNow={buyNowHandler}
+        priceInfo={{
+          totalNowPrice: currentVariantInfo.totalNowPrice,
+          totalLinePrice: currentVariantInfo.totalLinePrice,
+          save: currentVariantInfo.save,
+        }}
+      />
+    </>
+  );
+}

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

@@ -1,58 +1,113 @@
 "use client";
 
-import Image from 'next/image'
-import 'swiper/css';
-// import 'swiper/css/pagination';
-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 Image from "next/image";
+import "swiper/css";
+import "swiper/css/pagination";
+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 { ProductMediaType } from "@/components/catalog/type";
-import {
-  baseUrl,
-  getImageUrl
-} from "@/utils/constants";
-export function ProductMedia({ mediaData }: {mediaData: Array<ProductMediaType>; }) {
+import { baseUrl, getImageUrl } from "@/utils/constants";
+import { useState, useRef } from "react";
 
-    const pagination: PaginationOptions = {
-        el: '.product-media-swiper-pagination',
-        type: 'custom',
-        renderCustom: function (swiper: SwiperType, current: number, total: number) {
-            return current + '/' + total;
-        },
-    };
-    let mediaJSX = <></>;
-    if(mediaData.length) {
-        mediaJSX = (
-            <Swiper
-                pagination={pagination}
-                modules={[Pagination]}
-                className="product-media-swiper"
-                onSlideChange={() => console.log('slide change')}
-                onSwiper={(swiper) => console.log(swiper)}
-            >
-                {
-                    mediaData.map((img) => {
-                        return (
-                            <SwiperSlide key={img.id}>
-                                <Image className="block w-full"
-                                    src={getImageUrl(img.publicPath,baseUrl)}
-                                    width={390}
-                                    height={520}
-                                    loading="eager"
-                                    alt="Picture of the author"
-                                />
-                            </SwiperSlide>
-                        );
-                    }) 
-                }
-                
-                <div slot="container-end">
-                    <div className="product-media-swiper-pagination z-1 absolute bottom-6 left-1/2 transform-[translateX(-50%)] pt-1.5 pb-1.5 pl-3 pr-3 rounded-3xl text-ly-14 text-white bg-ly-deepgreen/50"></div>
-                </div>
-            </Swiper>
-        );
-    }
-    return mediaJSX;
+export function ProductMedia({
+  mediaData,
+}: {
+  mediaData: Array<ProductMediaType>;
+}) {
+  // 保存大图swiper实例,控制跳转
+  const mainSwiperRef = useRef<SwiperType | null>(null);
+  // 保存缩略图swiper实例,同步滚动
+  const thumbSwiperRef = useRef<SwiperType | null>(null);
+  // 当前激活下标
+  const [activeIndex, setActiveIndex] = useState(0);
 
-}
+  const pagination: PaginationOptions = {
+    el: ".product-media-swiper-pagination",
+    type: "custom",
+    renderCustom: function (
+      swiper: SwiperType,
+      current: number,
+      total: number,
+    ) {
+      return current + "/" + total;
+    },
+  };
+
+  const mediaJSX = mediaData.length ? (
+    <div className="flex flex-col gap-1 w-full">
+      {/* 上方主图轮播 */}
+      <Swiper
+        pagination={pagination}
+        modules={[Pagination]}
+        className="product-media-swiper relative w-full"
+        onSlideChange={(swiper) => {
+          // 1. 更新激活下标
+          setActiveIndex(swiper.activeIndex);
+          // 2. 缩略图同步滚动到当前slide
+          thumbSwiperRef.current?.slideTo(swiper.activeIndex);
+        }}
+        onSwiper={(swiper) => {
+          mainSwiperRef.current = swiper;
+        }}
+      >
+        {mediaData.map((img) => (
+          <SwiperSlide key={img.id}>
+            <Image
+              className="block w-full object-cover"
+              src={getImageUrl(img.publicPath, baseUrl)}
+              width={390}
+              height={520}
+              loading="eager"
+              alt="Product wig image"
+            />
+          </SwiperSlide>
+        ))}
+        {/* 自定义分页数字 */}
+        <div slot="container-end">
+          <div className="product-media-swiper-pagination z-1 absolute top-3 right-4 pt-1.5 pb-1.5 pl-3 pr-3 rounded-3xl text-ly-14 text-white bg-ly-deepgreen/50"></div>
+        </div>
+      </Swiper>
+      <div className=" pl-4">
+        {/* 下方缩略图轮播 */}
+        <Swiper
+          onSwiper={(swiper) => (thumbSwiperRef.current = swiper)}
+          slidesPerView={6} // 固定6个缩略图
+          spaceBetween={8}
+          watchSlidesProgress
+          className="thumb-swiper w-full"
+        >
+          {mediaData.map((img, idx) => (
+            <SwiperSlide key={img.id} className="w-[100px] flex-shrink-0">
+              <div
+                className={`cursor-pointer border-2 rounded overflow-hidden transition-all ${
+                  activeIndex === idx
+                    ? "border-green-600 scale-[1.02]"
+                    : "border-transparent opacity-70 hover:opacity-100"
+                }`}
+                onClick={() => {
+                  // 点击缩略图,主图跳转对应下标
+                  mainSwiperRef.current?.slideTo(idx);
+                }}
+              >
+                <Image
+                  src={getImageUrl(img.publicPath, baseUrl)}
+                  width={100}
+                  height={130}
+                  loading="eager"
+                  alt={`Thumbnail ${idx + 1}`}
+                  className="w-full h-full object-cover"
+                />
+              </div>
+            </SwiperSlide>
+          ))}
+        </Swiper>
+      </div>
+    </div>
+  ) : (
+    <></>
+  );
+
+  return mediaJSX;
+}

+ 48 - 29
src/app/(public)/product/_components/ProductReviewSection.tsx

@@ -1,32 +1,51 @@
-import ReviewAdd from "@/app/(public)/product/_components/review/ReviewAdd";
 import ReviewDetail from "@/app/(public)/product/_components/review/ReviewDetail";
 import { NoReview } from "@/app/(public)/product/_components/review/NoReview";
-import { getProductReviews } from "@utils/hooks/getProductReviews";
+// import { getProductReviews } from "@utils/hooks/getProductReviews";
+import type { ProductReviewList } from "@/types/products/productDetail";
+import React from "react";
+// 组件入参TS规范
+interface ProductReviewSectionProps {
+  productId: string;
+  reviews: ProductReviewList;
+  modalOpen: boolean;
+  closeModal: () => void;
+  onOpenBaseModal: () => void;
+  activeTab: string;
+  onChangeTab: (tabKey: string) => void;
+}
 
-
-export async function ProductReviewSection({
-    productId, 
-} : {
-    productId:string;
-}) { 
-    const getAllreviews = await getProductReviews(productId)
-    return (
-        <>
-            <div className="box-border w-full pr-4 pl-4">
-                <ReviewAdd productId={productId} />
-                {getAllreviews.length > 0 ? (
-                  <>
-                    <ReviewDetail
-                      reviewDetails={getAllreviews}
-                      totalReview={getAllreviews.length}
-                      productId={productId}
-                    />
-                  </>
-                ) : ( <NoReview />)}
-
-            </div>
-            
-        </>
-
-    );
-}
+function ProductReviewSection({
+  productId,
+  reviews,
+  modalOpen,
+  closeModal,
+  activeTab,
+  onOpenBaseModal,
+  onChangeTab,
+}: ProductReviewSectionProps) {
+  // const getAllreviews = await getProductReviews(productId)
+  console.log("productId-------------:", productId, reviews);
+  return (
+    <>
+      <div className="box-border w-full">
+        {reviews.length > 0 ? (
+          <>
+            <ReviewDetail
+              reviewDetails={reviews}
+              totalReview={reviews.length}
+              productId={productId}
+              modalOpen={modalOpen}
+              onOpenBaseModal={onOpenBaseModal}
+              onClose={closeModal}
+              activeTab={activeTab}
+              onChangeTab={onChangeTab} // 关键:把切换tab的函数传给弹窗
+            />
+          </>
+        ) : (
+          <NoReview />
+        )}
+      </div>
+    </>
+  );
+}
+export default React.memo(ProductReviewSection);

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

@@ -8,7 +8,7 @@ export function ProductShortDescription({
     return (
         <>
             <div className="box-border w-full pr-4 pl-4">
-                <h3>Product Description</h3>
+                <h3>Description</h3>
                 <div dangerouslySetInnerHTML={{ __html: shortDescription }}></div>
             </div>
             

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

@@ -0,0 +1,48 @@
+"use client";
+import { useState } from "react";
+
+export default function ProductText({ ProductText }: { ProductText: string }) {
+  // 模拟商品长标题
+  const titleText =
+    ProductText ||
+    "Wiggins 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);
+
+  return (
+    <div className="flex mt-2 items-start py-2 pr-2 pl-3 gap-2 w-full bg-[#F9F9F9]">
+      {/* 标题文本容器 */}
+      <div
+        className={`flex-1 text-ly-14 leading-ly-22  font-normal overflow-hidden transition-all ${
+          isExpand ? "" : "line-clamp-1 text-ellipsis whitespace-nowrap"
+        }`}
+      >
+        {titleText}
+      </div>
+
+      {/* 展开/收起SVG按钮 */}
+      <button
+        onClick={() => setIsExpand(!isExpand)}
+        className="shrink-0  transition-transform duration-300"
+        style={{ transform: isExpand ? "rotate(180deg)" : "rotate(0deg)" }}
+      >
+        {/* 向下对勾/箭头SVG */}
+
+        <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>
+      </button>
+    </div>
+  );
+}

Разлика између датотеке није приказан због своје велике величине
+ 158 - 0
src/app/(public)/product/_components/PromotionDiscountCardProps .tsx


Разлика између датотеке није приказан због своје велике величине
+ 148 - 0
src/app/(public)/product/_components/ShopServicePanel.tsx


Разлика између датотеке није приказан због своје велике величине
+ 170 - 0
src/app/(public)/product/_components/SwitchButton.tsx


+ 81 - 0
src/app/(public)/product/_components/popup/Guide.tsx

@@ -0,0 +1,81 @@
+"use client";
+import Image from "next/image";
+import { useEffect } from "react";
+
+interface GuidePopup {
+  open: boolean;
+  onClose: () => void;
+}
+
+export default function Guide({ open, onClose }: GuidePopup) {
+  // 弹窗打开时锁定滚动
+  useEffect(() => {
+    if (open) document.body.style.overflow = "hidden";
+    else document.body.style.overflow = "";
+    return () => {
+      document.body.style.overflow = "";
+    };
+  }, [open]);
+
+  // 点击遮罩关闭
+  const handleMaskClick = (e: React.MouseEvent) => {
+    if (e.target === e.currentTarget) onClose();
+  };
+
+  if (!open) return null;
+
+  return (
+    <div
+      onClick={handleMaskClick}
+      className="fixed inset-0 bg-black/60 z-[9999] flex items-center justify-center p-4"
+    >
+      {/* 弹窗容器 */}
+      <div className="relative w-full max-w-sm bg-white rounded-xl overflow-hidden pb-4">
+        {/* 右上角关闭按钮 */}
+        <div className="flex items-center justify-between px-3 py-3 border-b border-neutral-200">
+          <h2 className="text-ly-16 leading-ly-24 font-medium text-black">
+            Guide
+          </h2>
+          {/* 关闭按钮 X */}
+          <button
+            onClick={onClose}
+            className="absolute top-3 right-3 w-8 h-8 rounded-full bg-white/80 flex items-center justify-center text-black text-xl z-10 hover:bg-white transition"
+          >
+            <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.2"
+                stroke-linejoin="round"
+                stroke-linecap="round"
+                d="M7 7L17 17"
+              ></path>
+              <path
+                stroke="rgba(0, 0, 0, 1)"
+                stroke-width="1.2"
+                stroke-linejoin="round"
+                stroke-linecap="round"
+                d="M7 17L17 7"
+              ></path>
+            </svg>
+          </button>
+        </div>
+        {/* 弹窗介绍图,替换为你的实际图片地址 */}
+        <div className="h-106 overflow-y-auto mt-3 px-3">
+        <Image
+          src="/image/products_details/guide.png"
+          alt="Buy Now Pay Later Info"
+          width={960}
+          height={1307}
+          className="w-full h-auto block"
+        />
+        </div>
+      </div>
+    </div>
+  );
+}

+ 81 - 0
src/app/(public)/product/_components/popup/InstallmentPopup.tsx

@@ -0,0 +1,81 @@
+"use client";
+import Image from "next/image";
+import { useEffect } from "react";
+
+interface InstallmentPopup {
+  open: boolean;
+  onClose: () => void;
+}
+
+export default function InstallmentPopup({ open, onClose }: InstallmentPopup) {
+  // 弹窗打开时锁定滚动
+  useEffect(() => {
+    if (open) document.body.style.overflow = "hidden";
+    else document.body.style.overflow = "";
+    return () => {
+      document.body.style.overflow = "";
+    };
+  }, [open]);
+
+  // 点击遮罩关闭
+  const handleMaskClick = (e: React.MouseEvent) => {
+    if (e.target === e.currentTarget) onClose();
+  };
+
+  if (!open) return null;
+
+  return (
+    <div
+      onClick={handleMaskClick}
+      className="fixed inset-0 bg-black/60 z-[9999] flex items-center justify-center p-4"
+    >
+      {/* 弹窗容器 */}
+      <div className="relative w-full max-w-sm bg-white rounded-xl overflow-hidden pb-4">
+        {/* 右上角关闭按钮 */}
+        <div className="flex items-center justify-between px-3 py-3 border-b border-neutral-200">
+          <h2 className="text-ly-16 leading-ly-24 font-medium text-black">
+            Installment Description
+          </h2>
+          {/* 关闭按钮 X */}
+          <button
+            onClick={onClose}
+            className="absolute top-3 right-3 w-8 h-8 rounded-full bg-white/80 flex items-center justify-center text-black text-xl z-10 hover:bg-white transition"
+          >
+            <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.2"
+                stroke-linejoin="round"
+                stroke-linecap="round"
+                d="M7 7L17 17"
+              ></path>
+              <path
+                stroke="rgba(0, 0, 0, 1)"
+                stroke-width="1.2"
+                stroke-linejoin="round"
+                stroke-linecap="round"
+                d="M7 17L17 7"
+              ></path>
+            </svg>
+          </button>
+        </div>
+        {/* 弹窗介绍图,替换为你的实际图片地址 */}
+        <div className="h-106 overflow-y-auto mt-3 px-3">
+        <Image
+          src="/image/products_details/installment_description.png"
+          alt="Buy Now Pay Later Info"
+          width={960}
+          height={1307}
+          className="w-full h-auto block"
+        />
+        </div>
+      </div>
+    </div>
+  );
+}

+ 102 - 0
src/app/(public)/product/_components/popup/PriceModal.tsx

@@ -0,0 +1,102 @@
+"use client";
+// import clsx from "clsx";
+import { Price } from "@components/theme/ui/Price";
+
+// 价格数据类型,复用之前商品价格结构
+export type PriceInfo = {
+  totalLinePrice: number; // 原价 Retail price
+  totalNowPrice: number;  // 折后价 Deal Price
+  save: number;           // 优惠 Max Discount,现价=原价时自动为0
+};
+
+interface PriceModalModalProps {
+  visible: boolean;
+  onClose: () => void;
+  priceInfo: PriceInfo;
+}
+
+export default function PriceModal({
+  visible,
+  onClose,
+  priceInfo,
+}: PriceModalModalProps) {
+  if (!visible) return null;
+
+  const { totalLinePrice, totalNowPrice, save } = priceInfo;
+  // 现价=原价时折扣置0
+  const realDiscount = totalLinePrice === totalNowPrice ? 0 : save;
+
+  return (
+    // 全屏灰色遮罩
+    <div
+      className="fixed inset-0 bg-black/50 z-[1000] flex items-end justify-center"
+      onClick={(e) => e.target === e.currentTarget && onClose()}
+    >
+      {/* 弹窗白色容器 */}
+      <div className="bg-white w-full max-w-170 rounded-t-2xl p-4 pt-3 relative">
+        {/* 标题 + 右上角关闭叉号 */}
+        <div className="flex items-center justify-between mb-3">
+          <h2 className="text-base leading-6 font-medium">
+            Use coupon to enjoy the following benefits
+          </h2>
+          <button
+            onClick={onClose}
+            className="text-2xl hover:opacity-60 transition-opacity"
+            aria-label="Close modal"
+          >
+            ×
+          </button>
+        </div>
+
+        {/* 分割线 */}
+        <div className="border-b border-gray-200 mb-3"></div>
+
+        {/* 价格计算区域 白色背景块 */}
+        <div className="bg-gray-50 py-4 px-4 rounded-lg mb-2">
+          <div className="flex items-center justify-around text-center gap-2 flex-wrap">
+            {/* 原价 Retail price */}
+            <div>
+              <Price
+                className="text-ly-20 leading-ly-25 font-normal block "
+                amount={String(totalLinePrice)}
+                currencyCode="USD"
+              />
+              <p className="text-ly-12 leading-ly-20 font-normal">Retail price</p>
+            </div>
+
+            {/* 减号 */}
+            <span className="text-4xl font-bold">-</span>
+
+            {/* 最大优惠 Max Discount */}
+            <div>
+              <Price
+                className="text-ly-20 leading-ly-25 font-normal "
+                amount={String(realDiscount)}
+                currencyCode="USD"
+              />
+              <p className="text-ly-12 leading-ly-20 font-normal">Max Discount</p>
+            </div>
+
+            {/* 等于号 */}
+            <span className="text-4xl font-bold">=</span>
+
+            {/* 最终折后价 Deal Price */}
+            <div>
+              <Price
+                className="text-ly-20 leading-ly-25 font-bold"
+                amount={String(totalNowPrice)}
+                currencyCode="USD"
+              />
+              <p className="text-ly-12 leading-ly-20 font-normal">Deal Price</p>
+            </div>
+          </div>
+        </div>
+
+        {/* 底部提示文字 */}
+        <p className="text-ly-12 leading-ly-20 font-normal">
+          Discounts shown are estimated, final price may vary with the coupon you use.
+        </p>
+      </div>
+    </div>
+  );
+}

+ 173 - 0
src/app/(public)/product/_components/popup/ShoppingSecurityModal.tsx

@@ -0,0 +1,173 @@
+"use client";
+import { useState, useEffect, useRef } from "react";
+
+type TabKey = "shipping" | "return" | "paylater" | "hair";
+interface BaseModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+export default function ShoppingSecurityModal({
+  isOpen,
+  onClose,
+}: BaseModalProps) {
+  const [activeTab, setActiveTab] = useState<TabKey>("shipping");
+  // 存储每个tab按钮DOM ref
+  const tabRefMap = useRef<Map<TabKey, HTMLButtonElement | null>>(new Map());
+
+  // 弹窗打开锁定滚动
+  useEffect(() => {
+    if (isOpen) document.body.style.overflow = "hidden";
+    else document.body.style.overflow = "";
+    return () => {
+      document.body.style.overflow = "";
+    };
+  }, [isOpen]);
+
+  if (!isOpen) return null;
+
+  const tabList = [
+    { key: "shipping" as TabKey, label: "Free Fast Shipping" },
+    { key: "return" as TabKey, label: "30 Days Return" },
+    { key: "paylater" as TabKey, label: "Secure Pay Later" },
+    { key: "hair" as TabKey, label: "100% Human Hair" },
+  ];
+
+  // 点击Tab:切换状态 + 自动滚动到可视区
+  const handleTabClick = (key: TabKey) => {
+    setActiveTab(key);
+    const targetBtn = tabRefMap.current.get(key);
+    if (targetBtn) {
+      targetBtn.scrollIntoView({
+        behavior: "smooth",
+        block: "nearest",
+        inline: "center", // 让选中tab居中显示,也可改为"nearest"贴边
+      });
+    }
+  };
+
+  const tabContent: Record<TabKey, React.ReactNode> = {
+    shipping: (
+      <>
+        <div className="bg-neutral-50 rounded-lg py-3 pl-3 pr-1.5 ">
+          <ul className="mb-3">
+            <li>• Free Worldwide Shipping</li>
+            <li>• Orders Process within 24 Hours</li>
+            <li>• Fast Delivery from US/China Warehouse</li>
+            <li>• Real-Time Tracking Included</li>
+            <li>• Express Options Available in Select Regions</li>
+          </ul>
+        </div>
+        <p className="">
+          Need help? Contact:{" "}
+          <a href="mailto:service@asteriahair.com" className="underline">
+            service@asteriahair.com
+          </a>
+        </p>
+      </>
+    ),
+    return: (
+      <>
+        <div className="bg-neutral-50 rounded-lg py-3 pl-3 pr-1.5 ">
+          <ul className=" mb-3">
+            <li>• Full Refund or Exchange within 30 days</li>
+            <li>• Free Return Shipping Label (US Only)</li>
+            <li>• Fast & Hassle-Free Return Process</li>
+          </ul>
+        </div>
+        <p className="">
+          Note: Items must be unused and returned in original condition
+        </p>
+      </>
+    ),
+    paylater: (
+      <>
+        <div className="bg-neutral-50 rounded-lg py-3 pl-3 pr-1.5">
+          <ul className="mb-3">
+            <li>• Shop Freely without Upfront Full Payment</li>
+            <li>• Buy Now Pay Later: PayPal · Afterpay · Klarna · Apple Pay</li>
+            <li>
+              • Credit/Debit Card: MasterCard · Visa · American Express · JCB
+            </li>
+          </ul>
+        </div>
+        <p className="">
+          Note: You must be 18+ and meet eligibility requirements. Late fees may
+          apply. Estimated payments exclude insurance, which can be selected at
+          checkout.
+        </p>
+      </>
+    ),
+    hair: (
+      <div className="bg-neutral-50 rounded-lg py-3 pl-3 pr-1.5 ">
+        <ul className="">
+          <li>• Guaranteed Premium 100% Virgin Human Hair</li>
+          <li>• Soft · Natural · Long-Lasting · Restyled · Dyed</li>
+          <li>• No Synthetic Fiber Mixed</li>
+          <li>• Raw Hair Collection Available</li>
+          <li>• Third-Party Tested for Quality & Safety</li>
+        </ul>
+      </div>
+    ),
+  };
+
+  return (
+    <div className="fixed inset-0 z-1001 flex items-center justify-center">
+      <div onClick={onClose} className="absolute inset-0 bg-black/50" />
+
+      <div className="absolute bottom-0 w-full max-w-lg bg-white rounded-t-xl overflow-hidden z-10 mx-4">
+        {/* 头部标题关闭 */}
+        <div className="flex items-center justify-between px-4 pt-4 ">
+          <h2 className="text-ly-16 leading-ly-24 font-medium text-black">
+            Sales Services
+          </h2>
+          <button
+            onClick={onClose}
+            className="w-8 h-8 flex items-center justify-center text-black/70 hover:text-black transition-colors"
+          >
+            <svg
+              width="24"
+              height="24"
+              viewBox="0 0 24 24"
+              fill="none"
+              stroke="currentColor"
+              strokeWidth="2"
+            >
+              <path d="M18 6L6 18M6 6l12 12" />
+            </svg>
+          </button>
+        </div>
+
+        {/* Tab横向滚动容器 */}
+        <div className="flex px-4 py-4 mt-1  gap-2 overflow-x-auto scrollbar-hide">
+          {tabList.map((tab) => (
+            <button
+              key={tab.key}
+              ref={(el) => {
+                tabRefMap.current.set(tab.key, el);
+                return undefined;
+              }} // 存入ref映射
+              onClick={() => handleTabClick(tab.key)}
+              className={`relative whitespace-nowrap px-2 pb-1.5 text-xs transition-colors ${
+                activeTab === tab.key
+                  ? "text-[#1E7049] font-bold"
+                  : "text-black"
+              }`}
+            >
+              {tab.label}
+              {activeTab === tab.key && (
+                <span className="absolute left-1/2 -translate-x-1/2 bottom-0 w-10 h-0.5 bg-[#1E7049] block"></span>
+              )}
+            </button>
+          ))}
+        </div>
+
+        {/* 内容滚动区域 */}
+        <div className=" px-4 pb-4">
+          <div className=" text-black text-ly-12 leading-ly-24  font-normal">
+            {tabContent[activeTab]}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 69 - 0
src/app/(public)/product/_components/popup/VipPlusModal.tsx

@@ -0,0 +1,69 @@
+"use client";
+import { BaseModalProps } from "@/types/modal";
+import { useEffect } from "react";
+
+export default function VipPlusModal({ isOpen, onClose }: BaseModalProps) {
+  // 弹窗打开时锁定页面滚动
+  useEffect(() => {
+    if (isOpen) document.body.style.overflow = "hidden";
+    else document.body.style.overflow = "";
+    return () => {
+      document.body.style.overflow = "";
+    };
+  }, [isOpen]);
+
+  if (!isOpen) return null;
+
+  return (
+    // 全局灰色遮罩层
+    <div className="fixed inset-0 z-1001 flex items-center justify-center ">
+      {/* 灰色半透明遮罩,点击空白关闭弹窗 */}
+      <div
+        onClick={onClose}
+        className="absolute inset-0 bg-black/50"
+      />
+
+      {/* 弹窗主体容器 */}
+      <div className="absolute bottom-0 w-full max-w-lg bg-white rounded-t-xl overflow-hidden z-10 mx-4">
+        {/* 弹窗头部:标题 + 关闭按钮 */}
+        <div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200">
+          <h2 className="text-ly-16 leading-ly-24 font-medium text-black">VIP Plus Exclusive</h2>
+          {/* 关闭按钮 X */}
+          <button
+            onClick={onClose}
+            className="w-8 h-8 flex items-center justify-center text-black "
+          >
+            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+              <path d="M18 6L6 18M6 6l12 12" />
+            </svg>
+          </button>
+        </div>
+        {/* 内容区域:固定高度、纵向滚动 */}
+        <div className=" p-4">
+          {/* 浅绿色背景内容块 */}
+          <div className="bg-[#F0FAF7] h-[296px] overflow-y-auto rounded-sm pt-3 px-3 text-ly-13 leading-ly-22 font-normal">
+            <p className="">Unlock extra savings every time you shop — stack more discounts, save more instantly.</p>
+
+            <h3 className="font-semibold text-black">Membership Price</h3>
+            <p className="">$29.99 → <span className="font-semibold text-black">$14.99 (Save $15 today)</span></p>
+
+            <h3 className="font-semibold ">What You Get</h3>
+            <ul className="">
+              <li>• Extra 5% OFF on every order (stackable with all promotions)</li>
+              <li>• $20 Gift Cards included (2 × $10, long-term valid)</li>
+              <li>• Valid for 120 days from purchase date</li>
+              <li>• Reference Support:</li>
+              <li>• Priority access to exclusive offers & events</li>
+              <li>• Faster support & member-first service</li>
+            </ul>
+
+            <h3 className="text-xl font-semibold text-black">Why Join VIP Plus</h3>
+            <p className="">Your membership instantly pays for itself — You receive $20 in gift cards + ongoing 5% savings, making VIP Plus effectively free to join.</p>
+             <h3 className="text-xl font-semibold text-black">How it Works</h3>
+            <p className="">Activate VIP Plus directly in your Cart or Checkout to unlock all benefits immediately.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 70 - 0
src/app/(public)/product/_components/question/AskQuestionModal.tsx

@@ -0,0 +1,70 @@
+import { useState } from "react";
+
+interface AskQuestionModalProps {
+  visible: boolean;
+  onClose: () => void;
+  onSubmit: (text: string) => void;
+}
+
+export default function AskQuestionModal({
+  visible,
+  onClose,
+  onSubmit,
+}: AskQuestionModalProps) {
+  // 输入框内容
+  const [inputText, setInputText] = useState("");
+  const maxLength = 300;
+
+  // 提交处理
+  const handleSubmit = () => {
+    if (!inputText.trim()) return;
+    onSubmit(inputText.trim());
+    setInputText(""); // 清空输入
+  };
+
+  if (!visible) return null;
+
+  return (
+    // 灰色遮罩层
+    <div className="fixed inset-0 bg-black/60 flex items-end justify-center z-50">
+      {/* 弹窗白色容器 */}
+      <div className="bg-white w-full max-w-[520px] rounded-t-xl p-4 relative">
+        {/* 标题 + 关闭叉号 */}
+        <div className="flex justify-between items-center mb-6">
+          <h3 className="text-xl font-bold text-center flex-1">
+            Ask Questions
+          </h3>
+          <button
+            onClick={onClose}
+            className="absolute top-6 right-6 text-2xl hover:opacity-60"
+          >
+            ×
+          </button>
+        </div>
+
+        {/* 输入框区域 */}
+        <div className="relative mb-4">
+          <textarea
+            value={inputText}
+            onChange={(e) => setInputText(e.target.value.slice(0, maxLength))}
+            placeholder="Enter your question?"
+            className="w-full min-h-40 border border-orange-200 rounded-lg p-4 outline-none focus:border-orange-400 resize-none"
+          />
+          {/* 字数统计右下角 */}
+          <span className="absolute bottom-3 right-3 text-gray-400 text-sm">
+            {inputText.length}/{maxLength}
+          </span>
+        </div>
+
+        {/* 底部黑色提交按钮 */}
+        <button
+          onClick={handleSubmit}
+          disabled={!inputText.trim()}
+          className="w-full bg-black text-white py-2 rounded-xl text-lg font-medium disabled:opacity-50"
+        >
+          Ask Questions
+        </button>
+      </div>
+    </div>
+  );
+}

Разлика између датотеке није приказан због своје велике величине
+ 157 - 0
src/app/(public)/product/_components/question/ComponentsQuestionModal.tsx


+ 166 - 0
src/app/(public)/product/_components/question/FaqList.tsx

@@ -0,0 +1,166 @@
+"use client";
+import { useState } from "react";
+import QAModal from "./QAModal"
+// 1. 定义FAQ数据类型
+export interface FaqItem {
+  id: string;
+  question: string;
+  answer?: string;
+  likeCount: number;
+}
+
+// 2. 单个FAQ折叠项子组件
+function FaqAccordionItem({
+  item,
+  isOpen,
+  onToggle,
+}: {
+  item: FaqItem;
+  isOpen: boolean;
+  onToggle: () => void;
+}) {
+  const ArrowIcon = (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="16"
+      height="16"
+      viewBox="0 0 16 16"
+      fill="none"
+       className={`transition-transform duration-300 ease-in-out ${
+        isOpen ? "rotate-180" : "rotate-0"
+      }`}
+    >
+      <path
+        d="M2.99834 11.4952C2.74906 11.4952 2.54509 11.6991 2.54509 11.9484L2.54509 13.0604C2.54509 13.3097 2.74906 13.5137 2.99834 13.5137L13.0604 13.5137C13.3097 13.5137 13.5137 13.3097 13.5137 13.0604L13.5137 11.9484C13.5137 11.6991 13.3097 11.4952 13.0604 11.4952L2.99834 11.4952Z"
+        fill="#E5E5E5"
+      ></path>
+      <path
+        d="M13.0596 9.87793C13.3089 9.87793 13.391 9.71437 13.2421 9.51441L8.29445 2.87045C8.14556 2.67051 7.90192 2.67051 7.75305 2.87045L2.80538 9.51441C2.65648 9.71437 2.73862 9.87793 2.98791 9.87793L13.0596 9.87793Z"
+        fill="#E5E5E5"
+      ></path>
+    </svg>
+  );
+  return (
+    <div className="border border-gray-100 rounded-lg bg-white p-4 mb-3">
+      {/* 标题行:Q图标 + 问题 + 上下箭头 */}
+      <button
+        type="button"
+        onClick={onToggle}
+        className="w-full flex items-start justify-between text-left gap-3"
+      >
+        {/* 绿色圆形Q图标 SVG */}
+        <div className="w-8 h-8 rounded-full bg-emerald-700 flex items-center justify-center shrink-0">
+          Q
+        </div>
+
+        {/* 问题文字 */}
+        <h3 className="text-base font-medium text-emerald-800 flex-1">
+          {item.question}
+        </h3>
+
+        {/* 上下箭头 SVG 切换 */}
+        <div className="shrink-0 mt-1">
+          {/* {isOpen ? (
+            <svg
+              xmlns="http://www.w3.org/2000/svg"
+              width="18"
+              height="18"
+              fill="#222"
+              viewBox="0 0 16 16"
+            >
+              <path
+                fillRule="evenodd"
+                d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6Z"
+              />
+            </svg>
+          ) : (
+            <svg
+              xmlns="http://www.w3.org/2000/svg"
+              width="18"
+              height="18"
+              fill="#222"
+              viewBox="0 0 16 16"
+            >
+              <path
+                fillRule="evenodd"
+                d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708Z"
+              />
+            </svg>
+          )} */}
+          {ArrowIcon}
+        </div>
+      </button>
+
+      {/* 展开时显示答案区域 */}
+      {isOpen && item.answer && (
+        <div className="mt-4 ">
+          {/* 单选圆圈A */}
+          <div className="flex gap-2 mb-3">
+            <div className="w-8 h-8 rounded-full bg-white-700 border border-black flex items-center justify-center shrink-0">
+              A
+            </div>
+            <p className="text-gray-800 text-sm leading-relaxed">
+              {item.answer}
+            </p>
+          </div>
+          {/* 点赞行 */}
+          <div className="flex items-center justify-end gap-2 text-emerald-700 text-sm">
+            <span>{item.likeCount}</span>
+            {/* 点赞SVG图标 */}
+            <svg
+              xmlns="http://www.w3.org/2000/svg"
+              width="16"
+              height="16"
+              fill="currentColor"
+              viewBox="0 0 16 16"
+            >
+              <path d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z" />
+            </svg>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}
+
+// 外层FAQ列表容器
+export default function FaqList({ faqList }: { faqList: FaqItem[] }) {
+  // 记录当前展开的FAQ id,仅支持单条展开
+  const [openId, setOpenId] = useState<string | null>(null);
+ const [isQaOpen, setIsQaOpen] = useState(false);
+
+  // 默认只展示前3条,点击Show More展示全部
+  const displayList =  faqList.slice(0, 3);
+
+  // 切换折叠状态
+  const toggleFaq = (id: string) => {
+    setOpenId(openId === id ? null : id);
+  };
+
+  return (
+    <div className="w-full max-w-2xl mx-auto">
+      {/* FAQ列表 */}
+      <div className="space-y-1 mt-4">
+        {displayList.map((item) => (
+          <FaqAccordionItem
+            key={item.id}
+            item={item}
+            isOpen={openId === item.id}
+            onToggle={() => toggleFaq(item.id)}
+          />
+        ))}
+      </div>
+
+      {/* 底部Show More按钮 */}
+      <button
+        type="button"
+        onClick={() => setIsQaOpen(true)}
+        className="mt-6 block mx-auto px-8 py-2 rounded-full border border-gray-400 text-gray-800 hover:bg-gray-50 transition-colors"
+      >
+        Show More
+      </button>
+      {/* 弹窗子组件,传入控制参数 */}
+      <QAModal open={isQaOpen} onClose={() => setIsQaOpen(false)} />
+    </div>
+  );
+}

Разлика између датотеке није приказан због своје велике величине
+ 390 - 0
src/app/(public)/product/_components/question/QAModal.tsx


+ 98 - 58
src/app/(public)/product/_components/review/AddProductReview.tsx

@@ -11,6 +11,10 @@ import Image from "next/image";
 import { CreateProductReviewInput } from "@/types/review";
 import { AddUploadImage } from "@components/common/icons/AddUploadImage";
 
+interface UploadImgItem {
+  fileName: string;
+  preview: string;
+}
 export default function AddProductReview({
   productId,
   onClose,
@@ -18,8 +22,10 @@ export default function AddProductReview({
   productId: string;
   onClose: () => void;
 }) {
-  const [imageFile, setImageFile] = useState<string | null>(null);
-  const [imagePreview, setImagePreview] = useState<string | null>(null);
+  // const [imageFile, setImageFile] = useState<UploadImgItem[]>([]);
+  const [imgList, setImgList] = useState<UploadImgItem[]>([]);
+  const MAX_UPLOAD = 3;
+  // const [imagePreview, setImagePreview] = useState<string | null>(null);
   const [errors, setErrors] = useState({
     title: "",
     comment: "",
@@ -36,17 +42,46 @@ export default function AddProductReview({
 
   const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
-    if (file) {
-      setImageFile(file.name);
-      setReviewInfo((prev) => ({ ...prev, attachments: file.name }));
-      const reader = new FileReader();
-      reader.onloadend = () => {
-        setImagePreview(reader.result as string);
-      };
-      reader.readAsDataURL(file);
+    if (!file) return;
+
+    // 判断是否达到3张上限
+    if (imgList.length >= MAX_UPLOAD) {
+      showToast(`You can only upload up to ${MAX_UPLOAD} images`, "warning");
+      e.target.value = ""; // 清空input,防止重复选
+      return;
     }
-  };
 
+    const reader = new FileReader();
+    reader.onloadend = () => {
+      const newItem: UploadImgItem = {
+        fileName: file.name,
+        preview: reader.result as string,
+      };
+      // 追加到图片数组
+      const newList = [...imgList, newItem];
+      setImgList(newList);
+      // 拼接所有文件名给表单附件
+      setReviewInfo((prev) => ({
+        ...prev,
+        attachments: newList.map((item) => item.fileName).join(","),
+      }));
+    };
+    reader.readAsDataURL(file);
+    // 清空input,允许重复选择同一张图片
+    e.target.value = "";
+  };
+  // 根据下标删除指定图片
+  const removeSingleImage = (targetIndex: number) => {
+    const newList = imgList.filter((_, idx) => idx !== targetIndex);
+    setImgList(newList);
+    // 重新拼接附件字符串
+    setReviewInfo((prev) => ({
+      ...prev,
+      attachments: newList.length
+        ? newList.map((item) => item.fileName).join(",")
+        : null,
+    }));
+  };
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
 
@@ -84,7 +119,8 @@ export default function AddProductReview({
         rating: reviewInfo.rating,
         name: "Guest User",
         email: "guest@mail.com",
-        attachments: "",
+        // 读取表单里拼接好的多图文件名,无图则为空字符串
+        attachments: reviewInfo.attachments ?? "",
       };
 
       await createProductReview(input);
@@ -95,8 +131,7 @@ export default function AddProductReview({
         rating: 0,
         attachments: null,
       });
-      setImageFile(null);
-      setImagePreview(null);
+      setImgList([]); // 新增:清空多张图片数组
       setErrors({
         title: "",
         comment: "",
@@ -139,58 +174,61 @@ export default function AddProductReview({
       </div>
       <div className="flex flex-col gap-4">
         <div className="space-y-4">
-          {imagePreview ? (
-            <div className="border-2 w-[120px] h-auto max-h-[120px] border-dashed border-gray-300  rounded-xl text-center transition-colors hover:border-primary-500">
-              <div className="space-y-3">
-                <div className="relative mx-auto">
-                  <Image
-                    src={imagePreview}
-                    alt="Preview"
-                    width={120}
-                    height={120}
-                    className="w-full h-full object-cover rounded-lg"
-                  />
-                  <button
-                    type="button"
-                    onClick={() => {
-                      setImageFile(null);
-                      setImagePreview(null);
-                    }}
-                    className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
+          {/* 多张图片预览 横向排列 */}
+          <div className="flex flex-wrap gap-3 items-start">
+            {imgList.map((imgItem, idx) => (
+              <div
+                key={idx}
+                className="relative w-[120px] h-[120px] border-2 border-dashed border-gray-300 rounded-xl"
+              >
+                <Image
+                  src={imgItem.preview}
+                  alt="preview"
+                  width={120}
+                  height={120}
+                  className="w-full h-full object-cover rounded-lg"
+                />
+                {/* 单张删除按钮 */}
+                <button
+                  type="button"
+                  onClick={() => removeSingleImage(idx)}
+                  className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
+                >
+                  <svg
+                    xmlns="http://www.w3.org/2000/svg"
+                    className="h-4 w-4"
+                    fill="none"
+                    viewBox="0 0 24 24"
+                    stroke="currentColor"
                   >
-                    <svg
-                      xmlns="http://www.w3.org/2000/svg"
-                      className="h-4 w-4"
-                      fill="none"
-                      viewBox="0 0 24 24"
-                      stroke="currentColor"
-                    >
-                      <path
-                        strokeLinecap="round"
-                        strokeLinejoin="round"
-                        strokeWidth={2}
-                        d="M6 18L18 6M6 6l12 12"
-                      />
-                    </svg>
-                  </button>
-                </div>
+                    <path
+                      strokeLinecap="round"
+                      strokeLinejoin="round"
+                      strokeWidth={2}
+                      d="M6 18L18 6M6 6l12 12"
+                    />
+                  </svg>
+                </button>
+                {/* 图片文件名 */}
+                <p className="text-xs text-gray-500 truncate px-1 mt-1">
+                  {imgItem.fileName}
+                </p>
               </div>
-            </div>
-          ) : (
-            <div className="border-2 border-dashed border-gray-300 w-32 h-32 rounded-xl p-6 text-center transition-colors hover:border-primary-500">
-              <div>
+            ))}
+
+            {/* 上传按钮:不足3张才显示 */}
+            {imgList.length < MAX_UPLOAD && (
+              <div className="border-2 border-dashed border-gray-300 w-32 h-32 rounded-xl p-6 text-center transition-colors hover:border-primary-500">
                 <label
                   htmlFor="file-upload"
-                  className="mx-auto flex cursor-pointer flex-col items-center justify-center gap-2"
+                  className="mx-auto flex cursor-pointer flex-col items-center justify-center gap-2 h-full"
                 >
                   <div className="mx-auto flex h-8 w-8 items-center justify-center rounded-full">
                     <AddUploadImage />
                   </div>
-
                   <div className="text-sm text-center">
                     <span className="font-medium">Add Image</span>
                   </div>
-
                   <input
                     id="file-upload"
                     name="file-upload"
@@ -201,9 +239,11 @@ export default function AddProductReview({
                   />
                 </label>
               </div>
-            </div>
-          )}
-          {imageFile && <p className="text-sm">{imageFile}</p>}
+            )}
+          </div>
+          {/* <p className="text-sm text-gray-400">
+            Max {MAX_UPLOAD} images allowed
+          </p> */}
         </div>
 
         <div className="flex flex-col gap-4">

+ 260 - 0
src/app/(public)/product/_components/review/AddProductReviewForm.tsx

@@ -0,0 +1,260 @@
+"use client";
+import { useState } from "react";
+import { Input, Textarea } from "@heroui/input";
+import { AddRatingStar } from "./AddRatingStar";
+import { Button } from "@components/common/button/Button";
+import ReviewImageUploader from "./ReviewImageUploader";
+import { useCustomToast } from "@utils/hooks/useToast";
+// import { useProductReview } from "@utils/hooks/useProductReview";
+import type {
+  UploadImgItem,
+  ReviewFormData,
+  ReviewFormErrors,
+} from "@/types/products/review";
+import { clientFetch } from "@/lib/restApiClient";
+const MAX_UPLOAD = 3;
+
+interface AddProductReviewFormProps {
+  productId: string;
+  onCloseForm: () => void; // 关闭表单回调(Cancel/提交成功都调用)
+}
+
+export default function AddProductReviewForm({
+  productId,
+  onCloseForm,
+}: AddProductReviewFormProps) {
+  const { showToast } = useCustomToast();
+  // const { createProductReview, isLoading } = useProductReview();
+
+  // 图片状态
+  const [imgList, setImgList] = useState<UploadImgItem[]>([]);
+
+  // 完整表单数据(匹配设计稿字段)
+  const [formData, setFormData] = useState<ReviewFormData>({
+    name: "",
+    hairLength: "",
+    qualityRating: 0,
+    shippingRating: 0,
+    serviceRating: 0,
+    comment: "",
+    attachments: [],
+  });
+
+  // 表单错误校验
+  const [errors, setErrors] = useState<ReviewFormErrors>({
+    name: "",
+    hairLength: "",
+    qualityRating: "",
+    comment: "",
+  });
+
+  // 更新表单通用方法
+  const updateForm = (
+    key: keyof ReviewFormData,
+    value: string | number | null | string[],
+  ) => {
+    setFormData((prev) => ({ ...prev, [key]: value }));
+    // 输入时清空对应报错
+    if (key in errors) setErrors((prev) => ({ ...prev, [key]: "" }));
+  };
+
+  // 表单提交
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    const newErrors: ReviewFormErrors = {
+      name: "",
+      hairLength: "",
+      qualityRating: "",
+      comment: "",
+    };
+
+    // 必填校验
+    if (!formData.name.trim()) newErrors.name = "Name is required";
+    if (!formData.hairLength.trim())
+      newErrors.hairLength = "Hair Length is required";
+    if (formData.qualityRating === 0)
+      newErrors.qualityRating = "Please rate quality";
+    if (!formData.comment.trim()) newErrors.comment = "Comment is required";
+
+    setErrors(newErrors);
+    const hasError = Object.values(newErrors).some((v) => v);
+    if (hasError) {
+      showToast("Please fill all required fields marked with *", "danger");
+      return;
+    }
+
+    try {
+      // 组装接口入参
+      const input = {
+        product_id: Number(productId.split("/").pop()),
+        name: formData.name,
+        title:formData.hairLength,
+        hairLength: formData.hairLength,
+        rating:formData.qualityRating,
+        quality: formData.qualityRating,
+        shipping: formData.shippingRating,
+        service: formData.serviceRating,
+        comment: formData.comment,
+        images: formData.attachments ?? "",
+      };
+      console.log('input----------------------------------------',input);
+      const res = await clientFetch("/api/shop/reviews", {
+            method: "POST",
+            headers: {
+              "Content-Type": "application/json",
+            },
+            // 传给接口的参数,对应 params = await req.json()
+            body: JSON.stringify(input),
+          });
+      // await createProductReview(input);
+      console.log("评论提交res-------------------------------------------------",res);
+      
+      showToast("Review submitted successfully!", "success");
+      // 重置表单 + 关闭
+      setFormData({
+        name: "",
+        hairLength: "",
+        qualityRating: 0,
+        shippingRating: 0,
+        serviceRating: 0,
+        comment: "",
+        attachments: [],
+      });
+      setImgList([]);
+      onCloseForm();
+    } catch (err) {
+      console.error(err);
+      showToast("Failed to submit review, try again", "danger");
+    }
+  };
+
+  // 字符计数
+  const commentMax = 300;
+  const commentCount = formData.comment.length;
+
+  return (
+    <form onSubmit={handleSubmit} className="w-full  bg-white mt-6">
+      {/* Name */}
+      <div className="mb-4">
+        <label className="text-ly-14 leading-ly-24 font-normal mb-1">
+          * Name
+        </label>
+        <Input
+          placeholder="Enter Your Name"
+          value={formData.name}
+          onChange={(e) => updateForm("name", e.target.value)}
+          variant="bordered"
+          isInvalid={!!errors.name}
+          errorMessage={errors.name}
+          classNames={{
+            input: "h-14 text-base px-4",
+            inputWrapper:
+              "border border-[#E5E5E5FF] rounded-none h-14 border-gray-400 px-0",
+          }}
+        />
+      </div>
+
+      {/* Hair Length */}
+      <div className="mb-6">
+        <label className="text-ly-14 leading-ly-24 font-normal mb-1">
+          * Hair Length
+        </label>
+        <Input
+          placeholder="Hair Length"
+          value={formData.hairLength}
+          onChange={(e) => updateForm("hairLength", e.target.value)}
+          variant="bordered"
+          isInvalid={!!errors.hairLength}
+          errorMessage={errors.hairLength}
+          classNames={{
+            input: "h-14 text-base px-4",
+            inputWrapper:
+              "border border-[#E5E5E5FF] rounded-none h-14 border-gray-400 px-0",
+          }}
+        />
+      </div>
+
+      {/* 三项星级评分 */}
+      <div className="space-y-3 mb-6">
+        <div className="flex items-center gap-3">
+          <span className="w-18 font-medium">Quality:</span>
+          <AddRatingStar
+            value={formData.qualityRating}
+            onChange={(v) => updateForm("qualityRating", v)}
+          />
+          <span>{formData.qualityRating}.0</span>
+        </div>
+        {errors.qualityRating && (
+          <p className="text-red-500 text-sm">{errors.qualityRating}</p>
+        )}
+
+        <div className="flex items-center gap-3">
+          <span className="w-18 font-medium">Shipping:</span>
+          <AddRatingStar
+            value={formData.shippingRating}
+            onChange={(v) => updateForm("shippingRating", v)}
+          />
+          <span>{formData.shippingRating}.0</span>
+        </div>
+
+        <div className="flex items-center gap-3">
+          <span className="w-18 font-medium">Service:</span>
+          <AddRatingStar
+            value={formData.serviceRating}
+            onChange={(v) => updateForm("serviceRating", v)}
+          />
+          <span>{formData.serviceRating}.0</span>
+        </div>
+      </div>
+
+      {/* 评论输入框 */}
+      <div className="mb-4">
+        <label className="block font-medium mb-2">Write a review</label>
+        <Textarea
+          placeholder="Write Your Comments Here..."
+          value={formData.comment}
+          onChange={(e) =>
+            updateForm("comment", e.target.value.slice(0, commentMax))
+          }
+          minRows={6}
+          variant="bordered"
+          isInvalid={!!errors.comment}
+          errorMessage={errors.comment}
+          classNames={{
+            input: "",
+            inputWrapper:
+              "border border-[#E5E5E5FF] rounded-none  border-gray-400 ",
+          }}
+        />
+        <p className="text-right text-sm text-gray-400 -mt-6 pr-3">
+          {commentCount}/{commentMax}
+        </p>
+      </div>
+
+      {/* 图片上传组件 */}
+      <ReviewImageUploader
+        imgList={imgList}
+        setImgList={setImgList}
+        setAttachments={(val) => updateForm("attachments", val)}
+        MAX_UPLOAD={MAX_UPLOAD}
+      />
+
+      {/* 底部按钮 */}
+      <div className="flex gap-3 mt-6">
+        <button
+          type="button"
+          onClick={onCloseForm}
+          className="relative  font-normal text-ly-14 leading-ly-24 text-[#1E3932FF] py-1 px-3.5  cursor-pointer w-full   rounded-full bg-[#E1E8E6FF] p-4 tracking-wide "
+        >
+          Cancel Review
+        </button>
+        <Button
+          title={isLoading ? "Submitting..." : "Submit Review"}
+          type="submit"
+          disabled={isLoading}
+          className="relative  font-normal text-ly-14 leading-ly-24 text-[#1E3932FF] py-1 px-3.5  cursor-pointer   rounded-full bg-[#E1E8E6FF] p-4 tracking-wide  "
+        />
+      </div>
+    </form>
+  );
+}

+ 53 - 0
src/app/(public)/product/_components/review/HairPhotoGallery.tsx

@@ -0,0 +1,53 @@
+"use client";
+import Image from "next/image";
+
+interface HairPhotoGalleryProps {
+  onOpenModal: () => void;
+  imageList: string[];
+}
+
+export default function HairPhotoGallery({
+  onOpenModal,
+  imageList,
+}: HairPhotoGalleryProps) {
+  const displayImages = imageList.slice(0, 4);
+  return (
+    <div className="w-full flex gap-2 overflow-hidden my-4">
+      {displayImages.map((src, idx) => (
+        <div
+          key={idx}
+          onClick={onOpenModal}
+          className="relative flex-1 aspect-3/4 cursor-pointer rounded overflow-hidden"
+        >
+          <Image
+            src={src}
+            alt={`hair ${idx}`}
+            width={87}
+            height={116}
+            className="w-full h-full object-cover"
+          />
+          {idx == 3 ? (
+            <div className="absolute inset-0 bg-black/25 flex flex-col items-center justify-center text-white">
+              <p className="text-sm font-normal leading-ly-22 underline">More</p>
+              <p className="text-sm font-normal leading-ly-22 underline">Photos</p>
+            </div>
+          ) : (
+            ""
+          )}
+        </div>
+      ))}
+
+      {/* More Photos 第四张卡片 */}
+      {/* <div
+        onClick={onOpenModal}
+        className="relative flex-1 aspect-4/3 cursor-pointer rounded overflow-hidden"
+      >
+        <Image
+          src="/img/hair4.jpg"
+          alt="more photos"
+          className="object-cover"
+        />
+      </div> */}
+    </div>
+  );
+}

+ 104 - 0
src/app/(public)/product/_components/review/QuestionInputModal.tsx

@@ -0,0 +1,104 @@
+"use client";
+import { useState } from "react";
+import { Textarea } from "@heroui/input";
+import { Button } from "@components/common/button/Button";
+
+interface QuestionInputModalProps {
+  productId: string;
+  onCloseForm: () => void; // 关闭表单回调(Cancel/提交成功都调用)
+}
+
+export default function QuestionInputModal({
+  // productId,
+  onCloseForm,
+}: QuestionInputModalProps) {
+  const MAX_LENGTH = 300;
+  const [content, setContent] = useState("");
+  const [loading, setLoading] = useState(false);
+  const [errorMsg, setErrorMsg] = useState("");
+
+  // 提交处理
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setErrorMsg("");
+    const trimText = content.trim();
+    console.log('cacacacac:------------------------');
+    
+    // 简单校验
+    if (!trimText) {
+      setErrorMsg("Please write your question");
+      return;
+    }
+
+    try {
+      setLoading(true);
+    //   await onSubmit(trimText);
+      // 提交成功清空+关闭
+      setContent("");
+      onCloseForm();
+    } catch (err) {
+      console.log('err-----------:',err);
+      setErrorMsg("Submit failed, please try again");
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 关闭重置表单
+  const handleCancel = () => {
+    setContent("");
+    setErrorMsg("");
+    onCloseForm();
+  };
+
+  const currentCount = content.length;
+  return (
+    <form onSubmit={handleSubmit} className="w-full pt-6 flex flex-col gap-1">
+      <h2 className=" font-normal text-ly-16 leading-ly-24 text-black">Ask a question</h2>
+
+      {/* 文本输入框 匹配设计稿样式 */}
+      <div className="relative">
+        <Textarea
+          placeholder="Write Your Comments Here..."
+          value={content}
+          onChange={(e) => {
+            // 限制最大300字符
+            const val = e.target.value.slice(0, MAX_LENGTH);
+            setContent(val);
+            if (errorMsg) setErrorMsg("");
+          }}
+          minRows={8}
+          variant="bordered"
+          isInvalid={!!errorMsg}
+          errorMessage={errorMsg}
+          classNames={{
+            base: "w-full",
+            inputWrapper: "border rounded-none border-[#E5E5E5FF] ",
+            input: "text-base  min-h-[200px]",
+          }}
+        />
+        {/* 右下角字符计数 */}
+        <span className="absolute right-3 bottom-3 text-sm text-gray-400">
+          {currentCount}/{MAX_LENGTH}
+        </span>
+      </div>
+
+      {/* 底部两个按钮 */}
+      <div className="flex gap-3 mt-2">
+        <button
+          type="button"
+          onClick={handleCancel}
+          className="relative  font-normal text-ly-14 leading-ly-24 text-[#1E3932FF] py-1 px-3.5  cursor-pointer w-full  rounded-full bg-[#E1E8E6FF] p-4 tracking-wide"
+        >
+          Cancel
+        </button>
+        <Button
+          title={loading ? "Submitting..." : "Submit Question"}
+          type="submit"
+          disabled={loading}
+          className="relative  font-normal text-ly-14 leading-ly-24 text-[#1E3932FF] py-1 px-3.5  cursor-pointer    rounded-full bg-[#E1E8E6FF] p-4 tracking-wide"
+        />
+      </div>
+    </form>
+  );
+}

+ 42 - 18
src/app/(public)/product/_components/review/ReviewAdd.tsx

@@ -1,23 +1,52 @@
 "use client";
 
 import { useState } from "react";
-import { Modal, ModalContent } from "@heroui/modal";
-import AddProductReview from "./AddProductReview";
+// import { Modal, ModalContent } from "@heroui/modal";
+// import AddProductReview from "./AddProductReview";
 import { ReviewButton } from "@components/common/button/ReviewButton";
+import AddProductReviewForm from "./AddProductReviewForm";
+import QuestionInputModal from "./QuestionInputModal";
 
-
-export default function ReviewAdd({
-  productId,
-}: {productId: string;}) {
+export default function ReviewAdd({ productId }: { productId: string }) {
   const [open, setOpen] = useState(false);
-
+  const [qusopen, setQueopen] = useState(false);
+  const handleCloseForm = () => setOpen(false);
+  const setShowForm = () => {
+    setOpen(() => true);
+    setQueopen(() => false);
+  };
   return (
     <>
-      <div>
-          <ReviewButton setShowForm={setOpen} />
+      <div className="flex items-center justify-start gap-3">
+        <ReviewButton setShowForm={setShowForm} />
+        <div
+          onClick={() => {
+            setOpen(() => false);
+            setQueopen(() => true);
+          }}
+          className="relative flex font-normal text-ly-14 leading-ly-24 text-[#1E3932FF] py-1 px-3.5  cursor-pointer h-fit items-center justify-center rounded-full bg-[#E1E8E6FF] p-4 tracking-wide mt-4"
+        >
+          Ask a question
+        </div>
       </div>
-
-      <Modal
+      {/* 页面内表单区块,非弹窗 */}
+      {open && (
+        <AddProductReviewForm
+          productId={productId}
+          // 传关闭回调函数,表单内部Cancel/提交成功都会调用
+          onCloseForm={handleCloseForm}
+        />
+      )}
+      {qusopen && (
+        <QuestionInputModal
+          productId={productId}
+          // 传关闭回调函数,表单内部Cancel/提交成功都会调用
+          onCloseForm={() => {
+            setQueopen(false);
+          }}
+        />
+      )}
+      {/* <Modal
         isOpen={open}
         onOpenChange={setOpen}
         backdrop="blur"
@@ -33,15 +62,10 @@ export default function ReviewAdd({
       >
         <ModalContent className="p-0 border-none">
           {(onClose) => (
-            <AddProductReview
-              productId={productId}
-              onClose={onClose}
-            />
+            <AddProductReview productId={productId} onClose={onClose} />
           )}
         </ModalContent>
-      </Modal>
+      </Modal> */}
     </>
   );
 }
-
-

Разлика између датотеке није приказан због своје велике величине
+ 156 - 62
src/app/(public)/product/_components/review/ReviewDetail.tsx


+ 97 - 0
src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx

@@ -0,0 +1,97 @@
+"use client";
+import { useRef, useState } from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+import { Navigation } from "swiper/modules";
+import Image from "next/image";
+import { GalleryItem } from "@/types/products/review";
+import "swiper/css";
+import "swiper/css/navigation";
+
+interface GalleryModalProps {
+  open: boolean;
+  onClose: () => void;
+  galleryList: GalleryItem[];
+  initialSlide: number;
+}
+
+export default function ReviewImageGalleryModal({
+  open,
+  onClose,
+  galleryList,
+  initialSlide,
+}: GalleryModalProps) {
+  const swiperRef = useRef<any>(null);
+  // 新增状态存储当前下标,初始值传入initialSlide
+  const [currentIndex, setCurrentIndex] = useState(initialSlide);
+
+  if (!open) return null;
+
+  return (
+    <div className="fixed inset-0 bg-black z-[100] flex flex-col">
+      {/* 顶部关闭按钮 */}
+      <div className="flex justify-between items-center p-4 text-white shrink-0">
+        <span className="text-lg">
+          {currentIndex + 1} / {galleryList.length}
+        </span>
+        <button onClick={onClose}>
+          <svg width="28" height="28" fill="none" stroke="#fff" strokeWidth="2">
+            <path d="M6 6L18 18M18 6L6 18" />
+          </svg>
+        </button>
+      </div>
+
+      {/* 轮播大图 */}
+      <div className="flex-1 flex items-center justify-center">
+        <Swiper
+          ref={swiperRef}
+          initialSlide={initialSlide}
+          modules={[Navigation]}
+          navigation
+          className="w-full h-full"
+          // 滑动切换时更新下标
+          onSlideChange={(swiper) => setCurrentIndex(swiper.activeIndex)}
+        >
+          {galleryList.map((item, idx) => (
+            <SwiperSlide
+              key={idx}
+              className="flex flex-col justify-between h-full py-4"
+            >
+              {/* 大图 */}
+              <div className="flex-1 flex items-center justify-center px-2">
+                <Image
+                  src={item.imageUrl}
+                  alt={`gallery-${idx}`}
+                  width={600}
+                  height={800}
+                  className="max-w-full max-h-[70vh] object-contain"
+                />
+              </div>
+              {/* 底部用户评论信息 */}
+              <div className="bg-black/80 text-white p-4 mt-4">
+                <div className="flex items-center gap-2 mb-2">
+                  <div className="w-10 h-10 rounded-full bg-gray-300"></div>
+                  <span className="font-medium">{item.review.name}</span>
+                  <span className="ml-auto">{item.review.likeCount} 👍</span>
+                </div>
+                <div className="flex gap-0.5 mb-2">
+                  {Array.from({ length: 5 }).map((_, i) => (
+                    <svg
+                      key={i}
+                      width="14"
+                      height="14"
+                      fill={i < item.review.rating ? "#fff" : "#666"}
+                      viewBox="0 0 24 24"
+                    >
+                      <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
+                    </svg>
+                  ))}
+                </div>
+                <p className="text-sm text-gray-200">{item.review.comment}</p>
+              </div>
+            </SwiperSlide>
+          ))}
+        </Swiper>
+      </div>
+    </div>
+  );
+}

+ 123 - 0
src/app/(public)/product/_components/review/ReviewImageUploader.tsx

@@ -0,0 +1,123 @@
+"use client";
+// import { useState } from "react";
+import Image from "next/image";
+import { AddUploadImage } from "@components/common/icons/AddUploadImage";
+import { useCustomToast } from "@utils/hooks/useToast";
+import type { UploadImgItem } from "@/types/products/review";
+
+interface ReviewImageUploaderProps {
+  imgList: UploadImgItem[];
+  setImgList: (list: UploadImgItem[]) => void;
+  setAttachments: (val: string | null) => void;
+  MAX_UPLOAD: number;
+}
+
+export default function ReviewImageUploader({
+  imgList,
+  setImgList,
+  setAttachments,
+  MAX_UPLOAD,
+}: ReviewImageUploaderProps) {
+  const { showToast } = useCustomToast();
+
+  // 上传图片
+  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0];
+    if (!file) return;
+
+    if (imgList.length >= MAX_UPLOAD) {
+      showToast(`You can only upload up to ${MAX_UPLOAD} images`, "warning");
+      e.target.value = "";
+      return;
+    }
+
+    const reader = new FileReader();
+    reader.onloadend = () => {
+      const base64Str = reader.result as string;
+
+      const newItem: UploadImgItem = {
+        fileName: file.name,
+        preview: base64Str,
+        base64: base64Str,
+      };
+      const newList = [...imgList, newItem];
+      setImgList(newList);
+      // attachments 直接赋值base64数组,不做逗号拼接
+      setAttachments(newList.map((item) => item.base64));
+    };
+    reader.readAsDataURL(file);
+    e.target.value = "";
+  };
+
+  // 删除单张图片
+  const removeSingleImage = (targetIndex: number) => {
+    const newList = imgList.filter((_, idx) => idx !== targetIndex);
+    setImgList(newList);
+    // 删除后同步更新base64数组
+    setAttachments(newList.map((item) => item.base64));
+  };
+
+  return (
+    <div className="space-y-3 mt-4">
+      <div className="flex flex-wrap gap-2 items-start">
+        {/* 已上传图片预览 */}
+        {imgList.map((imgItem, idx) => (
+          <div
+            key={idx}
+            className="relative w-[114px] h-[152px] border-2 border-dashed border-gray-300 "
+          >
+            <Image
+              src={imgItem.preview}
+              alt="preview"
+              width={120}
+              height={120}
+              className="w-full h-full object-cover"
+            />
+            <button
+              type="button"
+              onClick={() => removeSingleImage(idx)}
+              className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600"
+            >
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                className="h-4 w-4"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M6 18L18 6M6 6l12 12"
+                />
+              </svg>
+            </button>
+            {/* <p className="text-xs text-gray-500 truncate px-1 mt-1">{imgItem.fileName}</p> */}
+          </div>
+        ))}
+
+        {/* 上传框 不足3张显示 */}
+        {imgList.length < MAX_UPLOAD && (
+          <div className="border-2 border-dashed border-gray-300 w-[114px] h-[152px]  flex flex-col items-center justify-center gap-2">
+            <label
+              htmlFor="file-upload"
+              className="cursor-pointer flex flex-col items-center justify-center h-full w-full"
+            >
+              <AddUploadImage />
+              <span className="text-sm text-gray-500">Optional</span>
+              <input
+                id="file-upload"
+                type="file"
+                className="sr-only"
+                accept="image/*"
+                onChange={handleImageUpload}
+              />
+            </label>
+          </div>
+        )}
+      </div>
+      <p className="text-sm text-gray-400">Max {MAX_UPLOAD} images allowed</p>
+    </div>
+  );
+}

Разлика између датотеке није приказан због своје велике величине
+ 449 - 0
src/app/(public)/product/_components/review/ReviewModal.tsx


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

@@ -0,0 +1,275 @@
+"use client";
+import React from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+import { Navigation, Pagination } from "swiper/modules";
+import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+// 导入Swiper基础样式
+import "swiper/css";
+import "swiper/css/navigation";
+
+// 模拟产品数据(12个产品,分2块,每块6个)
+const productData = [
+  // 第一块(6个)
+  {
+    id: 1,
+    img: "https://picsum.photos/400/500?random=1",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 2,
+    img: "https://picsum.photos/400/500?random=2",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 3,
+    img: "https://picsum.photos/400/500?random=3",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 4,
+    img: "https://picsum.photos/400/500?random=4",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 5,
+    img: "https://picsum.photos/400/500?random=5",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 6,
+    img: "https://picsum.photos/400/500?random=6",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+
+  // 第二块(6个)
+  {
+    id: 7,
+    img: "https://picsum.photos/400/500?random=7",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 8,
+    img: "https://picsum.photos/400/500?random=8",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 9,
+    img: "https://picsum.photos/400/500?random=9",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 10,
+    img: "https://picsum.photos/400/500?random=10",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 11,
+    img: "https://picsum.photos/400/500?random=11",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+  {
+    id: 12,
+    img: "https://picsum.photos/400/500?random=12",
+    title: "14-36 In Water Wave Huma...",
+    rating: 4.5,
+    reviewCount: 342,
+    price: 143.35,
+    originalPrice: 143.35,
+  },
+];
+
+// 拆分数据为2个块(每块6个)
+const productBlocks = [
+  productData.slice(0, 6), // 第一块
+  productData.slice(6, 12), // 第二块
+];
+
+const Recommend = () => {
+  return (
+    <div className="w-full max-w-[1200px] mx-auto px-4 py-8">
+      {/* 标题 */}
+      <h2 className="text-2xl font-bold mb-6">You May Also Like </h2>
+
+      {/* Swiper容器 */}
+      <Swiper
+        slidesPerView={1}
+        spaceBetween={20}
+        modules={[Navigation, Pagination]}
+        pagination={{
+          el: ".custom-pagination",
+          clickable: true,
+          renderBullet: (index, className) => {
+            // 所有分页默认底色为 #E5E5E5
+            return `<span class="${className} w-6 h-[5px] rounded-full mx-1 bg-[#E5E5E5]"></span>`;
+          },
+          // 激活时强制覆盖底色,加 !important 提高权重
+          bulletActiveClass: "!bg-[#333]",
+        }}
+        className="relative"
+      >
+        {/* 渲染2个滑动块 */}
+        {productBlocks.map((block, blockIndex) => (
+          <SwiperSlide key={blockIndex} className="!px-0">
+            {/* 每个块:6个产品,2列布局 */}
+            <div className="grid grid-cols-2 gap-4">
+              {block.map((product) => (
+                <div key={product.id} className="flex flex-col gap-2">
+                  {/* 产品图片 */}
+                  <img
+                    src={product.img}
+                    alt={product.title}
+                    className="w-full h-auto object-cover rounded-sm"
+                  />
+                  {/* 产品标题 */}
+                  <p className="text-sm text-gray-800 truncate">
+                    {product.title}
+                  </p>
+                  {/* 评分 + 评论数 */}
+                  <div className="flex items-center gap-1">
+                    <div className="flex">
+                      {/* 星级图标(简化版,可替换为真实星星图标) */}
+                      {[1, 2, 3, 4, 5].map((star) => (
+                        <svg
+                          key={star}
+                          className="w-4 h-4 text-yellow-400"
+                          fill="currentColor"
+                          viewBox="0 0 20 20"
+                        >
+                          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
+                        </svg>
+                      ))}
+                    </div>
+                    <span className="text-xs text-gray-600">
+                      {product.reviewCount}
+                    </span>
+                  </div>
+                  {/* 价格 + 购物车图标 */}
+                  <div className="flex items-center justify-between">
+                    <div className="flex items-center gap-1">
+                      <span className="font-semibold">${product.price}</span>
+                      <span className="text-xs text-gray-500 line-through">
+                        ${product.originalPrice}
+                      </span>
+                    </div>
+                    {/* 购物车图标 */}
+                    <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>
+              ))}
+            </div>
+          </SwiperSlide>
+        ))}
+
+        {/* 自定义分页器容器(底部小按钮) */}
+        <div className="custom-pagination flex justify-center mt-4"></div>
+      </Swiper>
+    </div>
+  );
+};
+
+export default Recommend;

+ 62 - 0
src/app/(public)/search/_components/HotSearch.tsx

@@ -0,0 +1,62 @@
+'use client';
+
+import Link from 'next/link';
+// import { createUrl } from '@/utils/helper';
+
+const hotTags = [
+  {
+    name: 'HD Wig',
+    href: '/search?q=HD%20Wig'
+  },
+  {
+    name: 'Glueless Wig',
+    href: '/search?q=Glueless%20Wig'
+  },
+  {
+    name: 'New Arrival',
+    href: '/search?q=New%20Arrival'
+  },
+  {
+    name: 'Color Wig',
+    href: '/search?q=Color%20Wig'
+  },
+  {
+    name: 'Lace Front Wig',
+    href: '/search?q=Lace%20Front%20Wig'
+  },
+  {
+    name: 'Bundles',
+    href: '/search?q=Bundles'
+  },
+  {
+    name: 'M Hairline',
+    href: '/search?q=M%20Hairline'
+  },
+  {
+    name: '13x6',
+    href: '/search?q=13x6'
+  },
+];
+
+export default function HotSearch() {
+
+  return (
+    <div className="w-full max-w-screen-2xl mx-auto px-4 py-6">
+      <h2 className="text-base font-medium text-black mb-4">Hot Search</h2>
+      <div className="flex flex-wrap items-center justify-start gap-3">
+        {hotTags.map((tag) => {
+          // 构造带q参数的搜索url
+          return (
+            <Link
+              key={tag.name}
+              href={tag.href}
+              className="bg-[#F2F2F2FF] rounded-sm px-[15px] py-1.5 flex items-center justify-center text-xs text-black font-normal"
+            >
+              {tag.name}
+            </Link>
+          );
+        })}
+      </div>
+    </div>
+  );
+}

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

@@ -1,20 +1,30 @@
 import dynamicImport from "next/dynamic";
 import Grid from "@/components/theme/ui/grid/Grid";
-import NotFound from "@/components/theme/search/not-found";
+// import NotFound from "@/components/theme/search/not-found";
 import { isArray } from "@/utils/type-guards";
-import { GET_FILTER_PRODUCTS } from "@/graphql";
-import { GET_PRODUCTS, GET_PRODUCTS_PAGINATION } from "@/graphql";
-import { cachedGraphQLRequest, getFilterAttributes } from "@/utils/hooks/useCache";
+import { serverGraphqlFetch } from "@utils/bagisto/index";
 import {
-  generateMetadataForPage,
-  buildProductFilters,
-} from "@/utils/helper";
+  GET_PRODUCTS,
+  // GET_PRODUCTS_PAGINATION,
+  // GET_FILTER_PRODUCTS,
+  SEARCH_PRODUCTS,
+} from "@/graphql";
+import {
+  cachedGraphQLRequest,
+  // getFilterAttributes,
+} from "@/utils/hooks/useCache";
+import { generateMetadataForPage, buildProductFilters } from "@/utils/helper";
 import SortOrder from "@/components/theme/filters/SortOrder";
 import { SortByFields } from "@/utils/constants";
-import MobileFilter from "@/components/theme/filters/MobileFilter";
-import FilterList from "@/components/theme/filters/FilterList";
-import { ProductsResponse } from "@/components/catalog/type";
+// import MobileFilter from "@/components/theme/filters/MobileFilter";
+// import FilterList from "@/components/theme/filters/FilterList";
+import {
+  ProductsResponse,
+  SearchProductsResponse,
+  SearchProductsVariables,
+} from "@/components/catalog/type";
 import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
+import HotSearch from "./_components/HotSearch";
 const Pagination = dynamicImport(
   () => import("@/components/catalog/Pagination"),
 );
@@ -23,14 +33,15 @@ const ProductGridItems = dynamicImport(
 );
 
 export const dynamicParams = true;
-
+// 获取全站商品
 export async function generateStaticParams() {
   try {
-    const itemsPerPage = 12;
+    const itemsPerPage = 8;
     const commonSearches = [""];
     const params = [];
     for (const query of commonSearches) {
-      const {data} = await cachedGraphQLRequest<ProductsResponse>(
+      // 获取默认产品回头换成 you may like 的产品
+      const { data } = await cachedGraphQLRequest<ProductsResponse>(
         "search",
         GET_PRODUCTS,
         {
@@ -54,17 +65,18 @@ export async function generateStaticParams() {
         }
         params.push(pageParams);
         if (i < totalPages - 1) {
-          const {data: pageData} = await cachedGraphQLRequest<ProductsResponse>(
-            "search",
-            GET_PRODUCTS,
-            {
-              query: query,
-              first: itemsPerPage,
-              sortKey: "CREATED_AT",
-              reverse: true,
-              ...(cursor && { after: cursor }),
-            },
-          );
+          const { data: pageData } =
+            await cachedGraphQLRequest<ProductsResponse>(
+              "search",
+              GET_PRODUCTS,
+              {
+                query: query,
+                first: itemsPerPage,
+                sortKey: "CREATED_AT",
+                reverse: true,
+                ...(cursor && { after: cursor }),
+              },
+            );
           cursor = pageData?.products?.pageInfo?.endCursor;
         }
       }
@@ -76,7 +88,7 @@ export async function generateStaticParams() {
     return [];
   }
 }
-
+// SEO+分享
 export async function generateMetadata({
   searchParams,
 }: {
@@ -108,8 +120,7 @@ export default async function SearchPage({
   } = (params || {}) as {
     [key: string]: string;
   };
-
-  const itemsPerPage = 12;
+  const itemsPerPage = 8;
   const currentPage = page ? parseInt(page) - 1 : 0;
   const sortValue = params?.sort || "name-asc";
   const selectedSort =
@@ -121,82 +132,125 @@ export default async function SearchPage({
 
   let dataPromise;
   if (isFilterApplied) {
-    dataPromise = cachedGraphQLRequest<ProductsResponse>(
-      "search",
-      GET_FILTER_PRODUCTS,
-      {
+    dataPromise = await serverGraphqlFetch<
+      SearchProductsResponse,
+      SearchProductsVariables
+    >({
+      query: SEARCH_PRODUCTS,
+      variables: {
+        suggest: true,
         query: searchValue,
         filter: filterInput,
+        first: 8,
         ...(beforeCursor
           ? { last: itemsPerPage, before: beforeCursor }
           : { first: itemsPerPage, after: afterCursor }),
         sortKey: selectedSort.sortKey,
         reverse: selectedSort.reverse,
       },
-    );
+    });
+    //  dataPromise = cachedGraphQLRequest<ProductsResponse>(
+    //     "search",
+    //     GET_FILTER_PRODUCTS,
+    //     {
+    //       query: searchValue,
+    //       filter: filterInput,
+    //       ...(beforeCursor
+    //         ? { last: itemsPerPage, before: beforeCursor }
+    //         : { first: itemsPerPage, after: afterCursor }),
+    //       sortKey: selectedSort.sortKey,
+    //       reverse: selectedSort.reverse,
+    //     },
+    //   );
   } else {
     dataPromise = (async () => {
       let currentAfterCursor: string | undefined = afterCursor;
+      //  console.log("currentAfterCursoraaaaa00:",currentAfterCursor);
       if (currentPage > 0 && !afterCursor) {
-        const {data: cursorData} = await cachedGraphQLRequest<ProductsResponse>(
-          "search",
-          GET_PRODUCTS_PAGINATION,
-          {
+        // 兜底处理 如果用户直接通过url访问且没有在url上带cursor=  会调接口获取cursor值  
+        // 正常走底部分页按钮不会触发该接口调用;
+        const { data: cursorData } = await serverGraphqlFetch<
+          SearchProductsResponse,
+          SearchProductsVariables
+        >({
+          query: SEARCH_PRODUCTS,
+          variables: {
+            suggest: true,
             query: searchValue,
             first: currentPage * itemsPerPage,
             sortKey: selectedSort.sortKey,
             reverse: selectedSort.reverse,
           },
-        );
-        currentAfterCursor = cursorData?.products?.pageInfo?.endCursor;
+        });
+        // await cachedGraphQLRequest<ProductsResponse>(
+        //   "search",
+        //   GET_PRODUCTS_PAGINATION,
+        //   {
+        //     query: searchValue,
+        //     first: currentPage * itemsPerPage,
+        //     sortKey: selectedSort.sortKey,
+        //     reverse: selectedSort.reverse,
+        //   },
+        // );
+        currentAfterCursor = cursorData?.search?.pageInfo?.endCursor;
+        // console.log("currentAfterCursoraaaaa:",currentAfterCursor);
       }
-
-      return cachedGraphQLRequest<ProductsResponse>("search", GET_PRODUCTS, {
-        query: searchValue,
-        ...(beforeCursor
-          ? { last: itemsPerPage, before: beforeCursor }
-          : { first: itemsPerPage, after: currentAfterCursor }),
-        sortKey: selectedSort.sortKey,
-        reverse: selectedSort.reverse,
+      return serverGraphqlFetch<
+        SearchProductsResponse,
+        SearchProductsVariables
+      >({
+        query: SEARCH_PRODUCTS,
+        variables: {
+          suggest: true,
+          query: searchValue,
+          ...(beforeCursor
+            ? { last: itemsPerPage, before: beforeCursor }
+            : { first: itemsPerPage, after: currentAfterCursor }),
+          sortKey: selectedSort.sortKey,
+          reverse: selectedSort.reverse,
+        },
       });
+      //    cachedGraphQLRequest<ProductsResponse>("search", GET_PRODUCTS, {
+      //   query: searchValue,
+      //   ...(beforeCursor
+      //     ? { last: itemsPerPage, before: beforeCursor }
+      //     : { first: itemsPerPage, after: currentAfterCursor }),
+      //   sortKey: selectedSort.sortKey,
+      //   reverse: selectedSort.reverse,
+      // });
     })();
   }
 
-  const [{data}, filterAttributes] = await Promise.all([
-    dataPromise,
-    getFilterAttributes(),
-  ]);
-
-  const products = data?.products?.edges?.map((e) => e.node) || [];
-  const pageInfo = data?.products?.pageInfo;
-  const totalCount = data?.products?.totalCount || 0;
+  // const [{ data }, filterAttributes] = await Promise.all([
+  //   dataPromise,
+  //   getFilterAttributes(),
+  // ]);
+  const { data } = await dataPromise;
+  console.log("sssssssssssssssssssssssssss:", data);
 
+  const products = data?.search?.edges?.map((e) => e.node) || [];
+  const pageInfo = data?.search?.pageInfo;
+  const totalCount = data?.search?.totalCount || 0;
+  console.log("productssssssssssssssssssss---------------------",products);
+  
   return (
     <>
       <MobileSearchBar />
-      <h2 className="text-2xl sm:text-4xl font-semibold mx-auto mt-7.5 w-full max-w-screen-2xl my-3 mx-auto px-4 xss:px-7.5">
-        All Top Products
-      </h2>
-
-      <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full mx-auto max-w-screen-2xl px-4 xss:px-7.5">
-        <FilterList filterAttributes={filterAttributes} />
-
-        <SortOrder sortOrders={SortByFields} title="Sort by" />
-      </div>
-      <div className="flex items-center justify-between gap-4 py-8 md:hidden  mx-auto w-full max-w-screen-2xl px-4 xss:px-7.5">
-        <MobileFilter filterAttributes={filterAttributes} />
-
-        <SortOrder sortOrders={SortByFields} title="Sort by" />
-      </div>
-
+      <HotSearch />
+      {isArray(products) ? (
+        <div className="flex items-center justify-between gap-4 pt-0 pb-6 md:hidden  mx-auto w-full max-w-screen-2xl px-4 xss:px-7.5">
+          {/* <MobileFilter filterAttributes={filterAttributes} /> */}
+          <div>{totalCount||0} items</div>
+          <SortOrder sortOrders={SortByFields} title="Sort by" />
+        </div>
+      ) : null}
       {!isArray(products) && (
-        <NotFound
-          msg={`${
-            searchValue
-              ? `There are no products that match Showing : ${searchValue}`
-              : "There are no products that match Showing"
-          } `}
-        />
+        <div>
+          <h2>There are no products that match Showing : ${searchValue}</h2>
+          <h2 className="text-2xl sm:text-4xl font-semibold mx-auto mt-7.5 w-full max-w-screen-2xl my-3 mx-auto px-4 xss:px-7.5">
+            All Top Products
+          </h2>
+        </div>
       )}
       {isArray(products) ? (
         <Grid className="grid grid-flow-row grid-cols-2 gap-5 lg:gap-11.5 w-full max-w-screen-2xl mx-auto md:grid-cols-3 lg:grid-cols-4 px-4 xss:px-7.5">

+ 40 - 0
src/app/api/customer/growth-value/route.ts

@@ -0,0 +1,40 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function GET(req: NextRequest) {
+  try {
+    const guestToken = getAuthToken(req);
+    const response = await restApiFetch<any>({
+      api: "/customer/growth-value",
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 46 - 0
src/app/api/customer/orders/[id]/route.ts

@@ -0,0 +1,46 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+type Params = Promise<{ id: string }>;
+export async function GET(req: NextRequest, { params }: { params: Params }) {
+  try {
+     const { id } = await params; // 解包拿到订单ID
+    const guestToken = getAuthToken(req);
+
+   const apiUrl = `/customer/orders/${id}`;
+
+    console.log("最终请求API地址:", apiUrl);
+
+    const response = await restApiFetch<any>({
+      api: apiUrl,
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 61 - 0
src/app/api/customer/orders/route.ts

@@ -0,0 +1,61 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function GET(req: NextRequest) {
+  try {
+    const guestToken = getAuthToken(req);
+
+    // 👇 取出所有前端传过来的查询参数
+    const searchParams = req.nextUrl.searchParams;
+    const page = searchParams.get("page");
+    const limit = searchParams.get("limit");
+    const status = searchParams.get("status");
+
+    // 👇 构建查询参数数组(自动过滤空值)
+    const queryParts: string[] = [];
+    if (page) queryParts.push(`page=${page}`);
+    if (limit) queryParts.push(`limit=${limit}`);
+    if (status) queryParts.push(`status=${status}`);
+
+    // 👇 最终拼接 URL
+    let apiUrl = "/customer/orders"; 
+    if (queryParts.length > 0) {
+      apiUrl += "?" + queryParts.join("&");
+    }
+
+    console.log("最终请求API地址:", apiUrl);
+
+    const response = await restApiFetch<any>({
+      api: apiUrl,
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 59 - 0
src/app/api/customer/orders/tracking/route.ts

@@ -0,0 +1,59 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function GET(req: NextRequest) {
+  try {
+    const guestToken = getAuthToken(req);
+
+    // 👇 取出所有前端传过来的查询参数
+    const searchParams = req.nextUrl.searchParams;
+    const track_number = searchParams.get("track_number");
+   
+
+    // 👇 构建查询参数数组(自动过滤空值)
+    const queryParts: string[] = [];
+    if (track_number) queryParts.push(`track_number=${track_number}`);
+   
+
+    // 👇 最终拼接 URL
+    let apiUrl = "/customer/orders/tracking"; 
+    if (queryParts.length > 0) {
+      apiUrl += "?" + queryParts.join("&");
+    }
+
+    console.log("最终请求API地址:", apiUrl);
+
+    const response = await restApiFetch<any>({
+      api: apiUrl,
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 6 - 16
src/app/api/gift/add/route.ts

@@ -1,6 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
 
 export async function POST(req: NextRequest) {
@@ -21,28 +20,19 @@ export async function POST(req: NextRequest) {
         });
         // 打印后端原始返回结构
         console.log('接口原始response.body =', JSON.stringify(response.body, null, 2));
-        return NextResponse.json({
+        return NextResponse.json(response.body,{
             status: response.status,
-            data: response.body,
         });
+
     } catch (error) {
         console.log('/gift/add --- ', error); // 调试用
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                message: error instanceof Error ? error.message : "Network error",
+                success: false,
+                data: []
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

+ 5 - 16
src/app/api/gift/lists/route.ts

@@ -1,6 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
 import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
 export async function GET(req: NextRequest) {
@@ -12,27 +11,17 @@ export async function GET(req: NextRequest) {
             cache:'no-store',
             guestToken,
         });
-        return NextResponse.json({
+        return NextResponse.json(response.body,{
             status: response.status,
-            data: response.body,
         });
     } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                message: error instanceof Error ? error.message : "Network error",
+                success: false,
+                data: []
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

+ 20 - 19
src/app/api/gift/my-gift-cards/route.ts

@@ -1,38 +1,39 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
-import type { GiftCardRespBody,FetchResult  } from '@/types/api/gift/my-gift-cards';
+import type { GiftCardRespBody } from '@/types/api/gift/my-gift-cards';
 export async function GET(req: NextRequest) {
     try {
         const guestToken = getAuthToken(req);
-        const response = await restApiFetch<FetchResult<GiftCardRespBody>>({
+
+        const searchParams = req.nextUrl.searchParams;
+        const page = searchParams.get('page') || '1';
+        const per_page = searchParams.get('per_page') || '10';
+
+        const response = await restApiFetch<{
+            data: GiftCardRespBody,
+            variables: {page: string;per_page: string;}
+        }>({
+            variables: {
+                page,
+                per_page
+            },
             api: `/gift/my-gift-cards`,
             method:'GET',
             cache:'no-store',
             guestToken,
         });
-        return NextResponse.json({
+        return NextResponse.json(response.body,{
             status: response.status,
-            data: response.body,
         });
     } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
-        return NextResponse.json(
+         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                message: error instanceof Error ? error.message : "Network error",
+                success: false,
+                data: []
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

+ 12 - 66
src/app/api/graphql/route.ts

@@ -1,6 +1,5 @@
 import { NextRequest, NextResponse } from "next/server";
 import { bagistoFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
 import { getAuthToken } from "@/utils/helper";
 import {
     CREATE_ADD_PRODUCT_IN_CART,
@@ -62,55 +61,7 @@ function authorizationOperations(body: Record<string, any>,req:NextRequest): Fet
     const guestToken = getAuthToken(req);
     const query = ALLOWED_OPERATIONS[operationName];
     const finalVariables = variables;
-
-    /*if (operationName === 'CheckoutPaymentMethods' || operationName === 'CheckoutShippingRates') {
-        finalVariables = { ...variables };
-    }
-
-    if (operationName === 'CreateCheckoutPaymentMethod') {
-        finalVariables = {
-            ...variables,
-            successUrl: variables?.successUrl ?? `payment/success`,
-            failureUrl: variables?.failureUrl ?? `payment/failure`,
-            cancelUrl: variables?.cancelUrl ?? `payment/cancel`
-        };
-    }*/
-
-    /*if (operationName === 'createCheckoutAddress' && body.billingFirstName) {
-        finalVariables = {
-            billingFirstName: body.billingFirstName,
-            billingLastName: body.billingLastName,
-            billingEmail: body.billingEmail,
-            billingAddress: body.billingAddress,
-            billingCity: body.billingCity,
-            billingCountry: body.billingCountry,
-            billingState: body.billingState,
-            billingPostcode: body.billingPostcode,
-            billingPhoneNumber: body.billingPhoneNumber,
-            billingCompanyName: body.billingCompanyName,
-            useForShipping: body.useForShipping,
-            ...(!body.useForShipping && {
-                shippingFirstName: body.shippingFirstName,
-                shippingLastName: body.shippingLastName,
-                shippingEmail: body.billingEmail,
-                shippingAddress: body.shippingAddress,
-                shippingCity: body.shippingCity,
-                shippingCountry: body.shippingCountry,
-                shippingState: body.shippingState,
-                shippingPostcode: body.shippingPostcode,
-                shippingPhoneNumber: body.shippingPhoneNumber,
-                shippingCompanyName: body.shippingCompanyName,
-            })
-        };
-    }*/
-
-    /*if (operationName === 'createAddProductInCart' && body.productId) {
-        finalVariables = {
-            cartId: body.cartId ?? null,
-            productId: body.productId,
-            quantity: body.quantity,
-        };
-    }*/
+ 
     return {
         query,
         variables: finalVariables,
@@ -151,27 +102,22 @@ export async function POST(req: NextRequest) {
 
         
         const response = await bagistoFetch<any>(fetchOption);
-        // console.log('response ------ ', response);
-        return NextResponse.json({
-            data: response.body.data,
+        console.log('bagistoFetch response ------ ', response);
+        return NextResponse.json(response.body,{
+            status: response.status
         });
     } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
+        console.log('bagistoFetch ERROR ------ ', error);
         return NextResponse.json(
             {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
+                errors:[
+                    {
+                        message: error instanceof Error ? error.message : "Network error"
+                    }
+                ],
+                data:null
             },
-            { status: 500 }
+            { status:500 }
         );
     }
 }

+ 46 - 0
src/app/api/home/route.ts

@@ -0,0 +1,46 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+type Params = Promise<{ id: string }>;
+export async function GET(req: NextRequest, {  }: { params: Params }) {
+  try {
+    //  const { id } = await params; // 解包拿到订单ID
+    const guestToken = getAuthToken(req);
+
+   const apiUrl = `/home`;
+
+    console.log("最终请求API地址:", apiUrl);
+
+    const response = await restApiFetch<any>({
+      api: apiUrl,
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 48 - 0
src/app/api/shop/products/[productId]/reviews/route.ts

@@ -0,0 +1,48 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+type Params = Promise<{ productId: string }>;
+
+export async function GET(req: NextRequest, { params }: { params: Params }) {
+  try {
+    const { productId } = await params;
+    const searchParams = req.nextUrl.searchParams;
+    const guestToken = getAuthToken(req);
+    const response = await restApiFetch<any>({
+      api: `/shop/products/${productId}/reviews`,
+      method: "GET",
+      cache: "no-store",
+      variables: {
+        page: searchParams.get("page") ?? 1,
+        per_page: searchParams.get("per_page") ?? 10,
+        has_images:searchParams.get("has_images") ?? 0, 
+        sort:searchParams.get("sort") ?? 'all', 
+      },
+       guestToken,
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 }
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 38 - 0
src/app/api/shop/reviews/route.ts

@@ -0,0 +1,38 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { getAuthToken } from "@/utils/helper";
+
+export async function POST(req: NextRequest) {
+    try {
+        const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+        const params = await req.json();
+        const response = await restApiFetch<{
+            data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
+            variables: {
+                id: number; // 到时候你自己改看是number还是string
+            }
+        }>({
+            api: '/shop/reviews',
+            method:'POST',
+            cache:'no-store',
+            variables: params,
+            guestToken: authorizationToken,
+        });
+        // 打印后端原始返回结构
+        console.log('接口原始response.body =', JSON.stringify(response.body, null, 2));
+        return NextResponse.json(response.body,{
+            status: response.status,
+        });
+
+    } catch (error) {
+        console.log('/gift/add --- ', error); // 调试用
+        return NextResponse.json(
+            {
+                message: error instanceof Error ? error.message : "Network error",
+                success: false,
+                data: []
+            },
+            { status:500 }
+        );
+    }
+}

+ 13 - 5
src/app/layout.tsx

@@ -68,6 +68,9 @@ export default async function RootLayout({
   }
 
   const cookieStore = await cookies();
+  ////////eruda调试/////////
+  const loadEruda = cookieStore.get('9527_LOAD_ERUDA_3345678');
+  ////////eruda调试////////
   const currentCurrency = cookieStore.get(CURRENT_CURRENCY);
   const currentLocale = cookieStore.get(CURRENT_LOCAL);
   const currentChannel = cookieStore.get(CURRENT_CHANNEL);
@@ -85,10 +88,16 @@ export default async function RootLayout({
   const {data: countryResData} = await serverGraphqlFetch<{
       countries: Country[] 
   }>({
-    query: GET_COUNTRIES
+    query: GET_COUNTRIES,
+    cache: 'force-cache',
+    revalidate: 3600,
+    tags: ["config-country-list"]
   });
   const {data: currencyResData} = await serverGraphqlFetch<CurrenciesData>({
-    query: GET_CURRENCIES
+    query: GET_CURRENCIES,
+    cache: 'force-cache',
+    revalidate: 3600,
+    tags: ["config-currency-list"]
   });
  
   const countries = countryResData.countries;
@@ -106,8 +115,7 @@ export default async function RootLayout({
   };
 
   const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
-    query: GET_CART_ITEM,
-    operationName:'GetCartItem'
+    query: GET_CART_ITEM
   });
   
   const cartDetails = cartDetailsRes.createReadCart?.readCart ?? null;
@@ -115,7 +123,7 @@ export default async function RootLayout({
     <html lang="en" suppressHydrationWarning>
       <head>
         <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"></meta>
-        <Script src="https://cdn.jsdelivr.net/npm/eruda" strategy="beforeInteractive" />
+        {(loadEruda && loadEruda.value === '1') && <Script src="https://cdn.jsdelivr.net/npm/eruda" strategy="beforeInteractive" />}
       </head>
       <body className={clsx(
         "min-h-screen font-outfit text-foreground bg-background antialiased",

+ 3 - 3
src/components/cart/OpenCart.tsx

@@ -9,11 +9,11 @@ export default function OpenCart({
   quantity?: number | string;
 }) {
   return (
-    <div className="relative flex  items-center justify-center rounded-md border-0 lg:border border-solid border-neutral-200 dark:border-neutral-700 lg:h-11 lg:w-11">
+    <div className="relative flex  items-center justify-center rounded-md w-6 h-6 border-0 lg:border border-solid border-neutral-200 dark:border-neutral-700 lg:h-11 lg:w-11">
       <ShoppingCartIcon className={clsx("h-5 w-5", className)} />
-
+{/* -mr-2  */}
       {quantity ? (
-        <div className="absolute right-0 top-0 -mr-2 margin-t lg:-mt-2 h-4 w-4 rounded bg-blue-600 text-[11px] font-medium text-white">
+        <div className="absolute right-0 bottom-0 margin-t lg:-mt-2 w-3 h-3  rounded-full bg-[#FF4500FF]  text-[11px] text-center leading-3 font-medium text-white">
           {quantity}
         </div>
       ) : null}

+ 2 - 1
src/components/catalog/product/ProductCard.tsx

@@ -11,6 +11,7 @@ type ProductCardProps = {
   specialPrice?: string;
   imageUrl: string;
   product: {
+    _id: number;
     urlKey: string;
     name: string;
     id: string;
@@ -81,7 +82,7 @@ export const ProductCard: FC<ProductCardProps> = ({
               currencyCode={currency}
             />
           )}
-          <OpenAddToCartModalButton productUrlKey={product.urlKey} />
+          <OpenAddToCartModalButton productId={product._id} />
 
         </div>
       </div>

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

@@ -30,6 +30,7 @@ export function ProductsSection({ title, description, products }: ProductsSectio
               imageUrl={imageUrl || ""}
               price={String(ProductPrice)}
               product={{
+                _id: item._id,
                 urlKey: item.urlKey || item.sku,
                 name: item?.name || item.sku,
                 id: item.id,

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

@@ -195,6 +195,7 @@ export interface SearchProductsResponse {
 
 export interface ProductSectionNode {
   id: string;
+  _id: number;
   sku: string;
   type: string;
   urlKey?: string;

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

@@ -1,67 +1,64 @@
 "use client";
 import { FC, ReactNode } from "react";
 import { useAppDispatch } from "@/store/hooks";
-import { openAddToCartDialog } from '@/store/slices/addToCartDialogSlice';
-import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
-import { useLazyQuery } from '@apollo/client/react';
-import { SingleProductResponse } from "@/components/catalog/type";
+import { openAddToCartDialog } from "@/store/slices/addToCartDialogSlice";
+import { GET_PRODUCT_BY_ID } 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;
+  // productUrlKey?: string;
+  productId: number;
   icon?: ReactNode;
 }
 export const OpenAddToCartModalButton: FC<OpenAddToCartModalButtonProps> = ({
-  productUrlKey,
+  // productUrlKey,
+  productId,
   icon,
 }) => {
-    const dispatch = useAppDispatch();
-    // const [loading, setLoading] = useState(false);
-    const { showToast } = useCustomToast();
+  const dispatch = useAppDispatch();
+  // const [loading, setLoading] = useState(false);
+  const { showToast } = useCustomToast();
 
-    const [getContent, { loading }] = useLazyQuery<SingleProductResponse>(GET_PRODUCT_BY_URL_KEY,{
-        fetchPolicy: 'network-only'
-    });
-    const openDialog = async () => {
-        if( loading) return; 
-        try {
-            const result = await getContent({
-                variables: { urlKey: productUrlKey }
-            });
+  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 },
+      });
 
-            console.log('product ----- ', result);// 返回的数据中有__typename,如何过滤 @todo
+      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.log('error ----- ', error);
-            let errorMessage = "Network Error";
-            if (error instanceof Error) {
-                errorMessage = error.message;
-            } else if (typeof error === 'object' && error !== null && 'message' in error) {
-                errorMessage = String((error as any).message);
-            }
-            
-            showToast(errorMessage, "danger");
+      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.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={loading}>
+      {loading ? (
+        <LoadingSpinner colorClassName={"text-ly-green"} />
+      ) : (
+        <ShoppingCartIcon icon={icon} />
+      )}
+    </button>
+  );
 };

+ 4 - 3
src/components/common/button/ReviewButton.tsx

@@ -2,21 +2,22 @@ import { IS_GUEST } from "@utils/constants";
 import { getCookie } from "@utils/cookie-tools";
 import { useRouter } from "next/navigation";
 
-export const ReviewButton = ({ setShowForm, className }: { setShowForm: (show: boolean) => void, className?: string }) => {
+export const ReviewButton = ({ setShowForm, className }: { setShowForm: () => void, className?: string }) => {
     const IsGuest = getCookie(IS_GUEST);
     const router = useRouter();
     const handleAddReview = () => {
         if (IsGuest === "true" || IsGuest === null) {
+            //   setShowForm();
             router.push("/customer/login");
         } else {
-            setShowForm(true);
+            setShowForm();
         }
     };
 
     return (
         <button
             onClick={handleAddReview}
-            className={`relative flex w-full min-w-[18rem] max-w-[20rem] cursor-pointer h-fit items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white mt-6 ${className}`}
+            className={`relative flex font-normal text-ly-14 leading-ly-24 text-[#1E3932FF] py-1 px-3.5  cursor-pointer h-fit items-center justify-center rounded-full bg-[#E1E8E6FF] p-4 tracking-wide mt-4 ${className}`}
         > 
             Write a review
         </button>

+ 113 - 0
src/components/home/HomeImageBanner.tsx

@@ -3,7 +3,12 @@
 import { Swiper, SwiperSlide } from 'swiper/react';
 import 'swiper/css';
 import {CurrencySwitch} from "@/components/common/CurrencySwitch/CurrencySwitch";
+import makeClient from "@/lib/ApolloClientBrowser";
 
+import { GET_CART_ITEM,GET_FILTER_ATTRIBUTES,CREATE_APPLY_COUPON } from "@/graphql";
+import { useMutation, useLazyQuery } from "@apollo/client/react";
+import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
+import { useCustomToast } from "@/utils/hooks/useToast";
 // interface ImageCarouselProps {
 //     options: {
 //         images: {
@@ -14,8 +19,116 @@ import {CurrencySwitch} from "@/components/common/CurrencySwitch/CurrencySwitch"
 //     };
 // }
 export default function HomeImageBanner() {
+  const { showToast } = useCustomToast();
+  const apolloClient = makeClient();
+  // const res = useQuery(GET_COUNTRY_STATES, {
+  //         variables: {
+  //             countryId: 1000
+  //         }
+  //     });
+  //     console.log('query state --- res', res);
+  const ttt = async () => {
+    try {
+      const res = await apolloClient.mutate({
+        mutation: GET_CART_ITEM,
+        fetchPolicy: 'network-only', // 按需调整
+      });
+      console.log('----------',res);
+    } catch(error) {
+      console.log('ttt----------',error);
+      handleApolloBusinessError(error,(err) => {
+          showToast(err.message, "danger");
+      });
+    }  
+  }
+  // 76|nlq9qGztR7qmSS72Kal56NCjhRRVrLCeVOdZdLLYa2c769e9
+const [getContent] = useLazyQuery(GET_FILTER_ATTRIBUTES,{
+        fetchPolicy: 'network-only'
+    });
+
+  const testLazyQuery = async () => { 
+      try {
+            const result = await getContent();
+            console.log('lazy query res---', result);
+      } catch(err) {
+          console.log('lazy query ---- ', err);
+      }
+  }
+
+    // useMutation
+    
+
+
+    const [mutateAsync] = useMutation(
+        GET_CART_ITEM,
+        {
+          onCompleted: (res) => {
+            console.log('useMutation onCompleted res ----- 0',res);
+          },
+    
+          onError: (err) => {
+            console.log('useMutation onCompleted err ----- 0',err);
+          },
+        },
+      );
+
+
+    const testMutation = async () => { 
+        
+            const res = await mutateAsync();
+        console.log('testMutation ---',res)
+        
+    };
+    const testFilter= () => {
+            return apolloClient.query({
+                query: GET_FILTER_ATTRIBUTES,
+                fetchPolicy: "no-cache",
+                context: {
+                    fetchOptions: {
+                        cache: 'no-store',
+                    },
+            
+                },
+                // variables: {
+                //     orderId: Number(orderid)
+                // }
+            }).then((res) => {
+                
+                console.log('testFilter res ---- ',res);
+            
+            
+            }).catch((err) => {
+                console.error('testFilter error ---- ',err);
+                handleApolloBusinessError(err,(error) => {
+          showToast(error.message, "danger");
+      });
+            });
+        };
+    
+        const testApplyCoupon = async () => { 
+            try {
+                await apolloClient.mutate({
+                    mutation: CREATE_APPLY_COUPON,
+                    variables: {
+                        couponCode: 'BIRTHDAY20'
+                    },
+                    fetchPolicy: 'network-only', // 按需调整
+                });
+            } catch (err) {
+                console.log('testApplyCoupon error ---- ',err);
+            }
+        };
+  
   return (
     <>
+    <p>
+      <button onClick={ttt}>get cart detail</button>
+    </p>
+
+    <p><button onClick={testLazyQuery}>testLazyQuery</button></p>
+    <p><button onClick={testMutation}>testMutation</button></p>
+    <p><button onClick={testFilter}>testFilter</button></p>
+    <p><button onClick={testApplyCoupon}>testApplyCoupon</button></p>
     <Swiper
       spaceBetween={50}
       slidesPerView={3}

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

@@ -34,6 +34,7 @@ const Theme = ({ title, description, products }: ProductsSectionProps) => {
                 imageUrl={imageUrl || ""}
                 price={String(ProductPrice)}
                 product={{
+                  _id: item._id,
                   urlKey: item.urlKey || item.sku,
                   name: item?.name || item.sku,
                   id: item.id,

+ 2 - 2
src/components/layout/navbar/MobileMenu.tsx

@@ -59,11 +59,11 @@ export default function MobileMenu({ menu, isOpen, onClose }: MobileMenuProps) {
                       className="p-2 text-xl text-black dark:text-white"
                     >
                       <Link
-                        href={item.slug ? `/category/${item.slug}` : "/search"}
+                        href={item.slug ? `/category/${item.slug}/${item._id}` : "/search"}
                         aria-label={`${item?.name}`}
                         onClick={onClose}
                       >
-                        {item.name}
+                        {item.name}{item._id} 4444
                       </Link>
                     </li>
                   ))}

+ 0 - 0
src/components/layout/navbar/MobileSearch.tsx


Неке датотеке нису приказане због велике количине промена