瀏覽代碼

Merge branch 'master' of http://gogs.hnwmzp.cn/fogwind/nshop-front-pc into zzf-category

zhangzf 21 小時之前
父節點
當前提交
3ca529df85
共有 40 個文件被更改,包括 1439 次插入1012 次删除
  1. 1 0
      README.md
  2. 9 9
      src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx
  3. 5 2
      src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx
  4. 9 9
      src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx
  5. 3 3
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  6. 61 102
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  7. 7 10
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  8. 52 0
      src/app/(checkout)/checkout/_components/ContinueToPay/ProductsInPendingOrder.tsx
  9. 0 124
      src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx
  10. 1 0
      src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx
  11. 0 1
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  12. 19 16
      src/app/(checkout)/checkout/continuetopay/page.tsx
  13. 23 23
      src/app/(public)/paymentresult/_components/CancelOrFailure.tsx
  14. 23 23
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  15. 32 12
      src/app/(public)/product/[...urlProduct]/page.tsx
  16. 69 69
      src/app/(public)/product/_components/ProductAddToCart.tsx
  17. 2 2
      src/app/(public)/product/_components/ProductDescription.tsx
  18. 20 4
      src/app/(public)/product/_components/ProductDetail.tsx
  19. 6 6
      src/app/(public)/product/_components/ProductInformation.tsx
  20. 44 42
      src/app/(public)/product/_components/ProductMedia.tsx
  21. 42 7
      src/app/(public)/product/_components/ProductReviewSection.tsx
  22. 1 1
      src/app/(public)/product/_components/ProductShortDescription.tsx
  23. 144 0
      src/app/(public)/product/_components/ProductTwoColumnLayout.tsx
  24. 25 0
      src/app/(public)/product/_components/ProductdetailAndQuestion.tsx
  25. 1 1
      src/app/(public)/product/_components/ShopServicePanel.tsx
  26. 197 57
      src/app/(public)/product/_components/SwitchButton.tsx
  27. 1 1
      src/app/(public)/product/_components/popup/ShoppingSecurityModal.tsx
  28. 1 1
      src/app/(public)/product/_components/popup/VipPlusModal.tsx
  29. 130 122
      src/app/(public)/product/_components/review/AddProductReviewForm.tsx
  30. 46 31
      src/app/(public)/product/_components/review/HairPhotoGallery.tsx
  31. 11 11
      src/app/(public)/product/_components/review/ReviewAdd.tsx
  32. 16 13
      src/app/(public)/product/_components/review/ReviewDetail.tsx
  33. 101 95
      src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx
  34. 277 181
      src/app/(public)/product/_components/review/ReviewModal.tsx
  35. 52 29
      src/app/(public)/product/_components/youmaylike/Recommend.tsx
  36. 1 0
      src/app/globals.css
  37. 2 2
      src/components/common/button/ReviewButton.tsx
  38. 1 1
      src/components/theme/ui/kernel/confirm/host.tsx
  39. 3 2
      src/providers/SocialShareProvider.tsx
  40. 1 0
      src/types/products/review.ts

+ 1 - 0
README.md

@@ -361,6 +361,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 51. 关于分享组件:https://chatgpt.com/share/6aa351f3-5ed0-83ea-8320-43d34d66f10a
     -- 可以参考https://github.com/nygardk/react-share
     -- 接着SocialShare写
+    -- https://developer.mozilla.org/zh-CN/docs/Web/API/Clipboard/writeText
 
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054

+ 9 - 9
src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx

@@ -342,8 +342,8 @@ export default function BillingAddressCheckout ({
                     <label className="text-ly-16 block mb-1 font-semibold">
                         Name *
                     </label>
-                    <div className="w-full flex justify-between">
-                        <div className="w-80.5 flex-none">
+                    <div className="w-full flex justify-between gap-3">
+                        <div className="">
                             <InputText
                                 type="text"
                                 placeholder="First name"
@@ -357,7 +357,7 @@ export default function BillingAddressCheckout ({
                                 error={errors.billingFirstName && errors.billingFirstName.message as string}
                             />
                         </div> 
-                        <div className="w-80.5 flex-none">
+                        <div className="">
                             <InputText
                                 type="text"
                                 placeholder="Last name"
@@ -390,8 +390,8 @@ export default function BillingAddressCheckout ({
                 <div className="w-full mt-4">
                     <label className="text-ly-16 block mb-1 font-semibold">Country and State/Province  *</label>
                     <div className="w-full">
-                        <div className="w-full flex justify-between">
-                            <div className="w-80.5 flex-none">
+                        <div className="w-full flex justify-between gap-3">
+                            <div className="">
                                 <Select placeholder="Country/Region"
                                     {...register("billingCountry", {
                                         required: !billingSameAsShipping ? "Country/Region field is required" : false,
@@ -410,7 +410,7 @@ export default function BillingAddressCheckout ({
                                     error={errors.billingCountry && errors.billingCountry.message as string}
                                 /> 
                             </div>
-                            <div className="w-80.5 flex-none relative">
+                            <div className="relative">
                                 {statesLoading && <LoadingSpinner className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />}
                                 {statesOptions.length > 0 ?
                                 (
@@ -436,8 +436,8 @@ export default function BillingAddressCheckout ({
                             </div>
                             
                         </div>
-                        <div className="w-full flex justify-between mt-4">
-                            <div className="w-80.5 flex-none">
+                        <div className="w-full flex justify-between mt-4 gap-3">
+                            <div className="">
                                 <InputText
                                     type="text"
                                     placeholder="Zip/Postal Code"
@@ -451,7 +451,7 @@ export default function BillingAddressCheckout ({
                                     error={errors.billingPostcode&& errors.billingPostcode.message as string}
                                 />
                             </div>
-                            <div className="w-80.5 flex-none">
+                            <div className="">
                                 <InputText
                                     type="text"
                                     placeholder="City"

+ 5 - 2
src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx

@@ -3,7 +3,7 @@
 import {type Ref, useState, useEffect, useCallback, useImperativeHandle, useMemo } from "react";
 import clsx from "clsx";
 import { useRouter } from 'next/navigation';
-import { useForm, FormProvider } from "react-hook-form";
+import { useForm, FormProvider, useWatch } from "react-hook-form";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useCheckoutAddress, addressIsSame} from "@/utils/hooks/useCheckoutAddress"
 import {useLoginModal} from "@/providers/LoginModalProvider";
@@ -421,7 +421,10 @@ export function CheckoutAddress({
         
     };
 
-    const isBillSameAsShip = addressForm.watch("billingSameAsShipping");
+    const isBillSameAsShip = useWatch({
+        control: addressForm.control,
+        name: "billingSameAsShipping",
+    });
     const showSaveBtn = () => {
         let res = false;
         if(

+ 9 - 9
src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx

@@ -260,8 +260,8 @@ export default function ShippingAddressCheckout({
                 <label className="text-ly-16 block mb-1 font-semibold">
                     Name *
                 </label>
-                <div className="w-full flex justify-between">
-                    <div className="w-80.5 flex-none">
+                <div className="w-full flex justify-between gap-3">
+                    <div className="">
                         <InputText
                             type="text"
                             placeholder="First name"
@@ -275,7 +275,7 @@ export default function ShippingAddressCheckout({
                             error={errors.shippingFirstName && errors.shippingFirstName.message as string}
                         />
                     </div> 
-                    <div className="w-80.5 flex-none">
+                    <div className="">
                         <InputText
                             type="text"
                             placeholder="Last name"
@@ -336,8 +336,8 @@ export default function ShippingAddressCheckout({
             <div className="w-full mt-4">
                 <label className="text-ly-16 block mb-1 font-semibold">Country and State/Province  *</label>
                 <div className="w-full">
-                    <div className="w-full flex justify-between">
-                        <div className="w-80.5 flex-none">
+                    <div className="w-full flex justify-between gap-3">
+                        <div className="">
                             <Select placeholder="Country/Region"
                                 {...register("shippingCountry", {
                                     required: "Country/Region field is required",
@@ -357,7 +357,7 @@ export default function ShippingAddressCheckout({
                                 error={errors.shippingCountry && errors.shippingCountry.message as string}
                             /> 
                         </div>
-                        <div className="w-80.5 flex-none relative">
+                        <div className=" relative">
                             {statesLoading && <LoadingSpinner className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />}
                             {statesOptions.length > 0 ?
                             (
@@ -383,8 +383,8 @@ export default function ShippingAddressCheckout({
                         </div>
                         
                     </div>
-                    <div className="w-full flex justify-between mt-4">
-                        <div className="w-80.5 flex-none">
+                    <div className="w-full flex justify-between mt-4 gap-3">
+                        <div className="">
                             <InputText
                                 type="text"
                                 placeholder="Zip/Postal Code"
@@ -398,7 +398,7 @@ export default function ShippingAddressCheckout({
                                 error={errors.shippingPostcode&& errors.shippingPostcode.message as string}
                             />
                         </div>
-                        <div className="w-80.5 flex-none">
+                        <div className="">
                             <InputText
                                 type="text"
                                 placeholder="City"

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

@@ -323,8 +323,8 @@ export default function CheckoutWrapper({
             console.log('checkoutWrapper unmout==========================');
         }
     },[]);
-    return (<div className="w-full flex max-w-300 mx-auto items-start mt-6 mb-30">
-        <section className="flex-initial w-164 mr-16">
+    return (<div className="w-full flex justify-between max-w-312 mx-auto items-start mt-6 mb-30 px-6">
+        <section className="flex-initial w-164">
             <div className="pb-6 border-b-1">
                 <CheckoutAddress 
                     ref={checkoutAddressRef}
@@ -415,7 +415,7 @@ export default function CheckoutWrapper({
             </div>
 
         </section>
-        <section className="flex-none box-border w-120 border-1 p-4">
+        <section className="flex-initial box-border w-120 border-1 p-4 ml-4">
             <div className="box-border">
                 <CheckoutProducts 
                     cartData={cartData}

File diff suppressed because it is too large
+ 61 - 102
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx


+ 7 - 10
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -1,7 +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";
 import { useRouter } from 'next/navigation'
@@ -436,24 +435,22 @@ export function PaymentMethodContinueTpPay({
 
     return (
         <>
-            <form name="shipping method" className="w-full box-border px-4 mt-4">
-                <div className="mt-3 w-full">
+            <form name="shipping method" className="w-full box-border">
+                <div className="mt-4 w-full">
                     {paymentMethodsData.error && <p className="text-red-500 text-ly-12">{paymentMethodsData.error.message}</p>}
                     <ul className="w-full">
                     {
                         paymentMethods.map((item) => {
-                            return (<li className="w-full mt-3" key={item.method}>
+                            return (<li className="w-full mt-4" key={item.method}>
                                 <label htmlFor={"paymentmethod-" + item.method} 
                                     className="group box-border flex items-center relative bg-ly-lightgray  p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
                                 >
                                     <p className="pr-2 flex-none relative">
-                                        <Image className="block h-8 w-auto"
+                                        <img className="block h-8 w-auto"
                                             src={getPaymentMethodLogo(item.method)} alt={"Payment Method " + item.title}
-                                            width={32}
-                                            height={32}
                                         />
                                     </p>
-                                    <p className="text-ly-12 text-[#666666] ml-1">{genetePaymentTips(item.method)}</p>
+                                    <p className="text-ly-14 text-[#666666] ml-1">{genetePaymentTips(item.method)}</p>
 
                                     <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
                                         <svg className="check w-4 h-4 hidden group-has-[:checked]:block" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect  x="1" y="1" width="14" height="14" rx="1"    fill="#000000" ></rect><path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5"  strokeLinecap="square"  d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>
@@ -487,9 +484,9 @@ export function PaymentMethodContinueTpPay({
                 </div> 
             </form>
             
-            <div className="w-full box-border px-4 mt-3">
+            <div className="w-full box-border mt-4">
                 <p className="text-ly-12 text-[#666666] leading-4.75">
-                    By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.
+                    By providing your information, you agree to Asteria's Privacy Policyand Terms of Use.
                 </p>
 
                 <div className="mt-6 w-full">

+ 52 - 0
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsInPendingOrder.tsx

@@ -0,0 +1,52 @@
+"use client";
+
+import Image from "next/image";
+import type {ProductsInOrderDetails} from "@/types/customer/type";
+import {getProductAdditionalInfo} from "@/utils/orderDetailTools";
+
+
+export default function ProductsInPendingOrder({
+    products,
+    currencySymbol,
+}: {
+    products: ProductsInOrderDetails[],
+    currencySymbol: string;
+}) {
+
+    return (
+        <div className="w-full">
+            {products.map((product) => {
+                const baseImage = JSON.parse(product.baseImage);
+                return (
+                    <div key={product.id} 
+                        className="flex justify-between w-full box-border pb-4 border-b-1 border-[#f0f0f0] mt-4 last:border-0 last:pb-0 last:mt-0"
+                    >
+                        <div className="relative flex-initial w-22 h-30 mr-2">
+                            <Image className="object-cover" fill
+                                src={baseImage.medium_image_url}
+                                alt={product.name} 
+                            />
+                        </div>
+                        <div className="flex-1 flex flex-col justify-between ">
+                            <div>
+                                <p className="text-ly-14 leading-ly-20 line-clamp-2 text-ly-placeholder">
+                                    {product.name}
+                                </p>
+                                <p className="text-ly-14 leading-ly-20 mt-3">
+                                    {getProductAdditionalInfo(JSON.parse(product.additional))}
+                                </p>
+                            </div>
+                            <div className="flex justify-between items-center">
+                                <span className="text-ly-14 font-bold">
+                                    {currencySymbol + product.price}
+                                </span>
+                                <span className="text-ly-14">X {product.qtyOrdered}</span>
+                            </div>
+                        </div>
+                    </div>
+                )
+            })}
+            
+        </div>
+    );
+}

+ 0 - 124
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx

@@ -1,124 +0,0 @@
-"use client";
-
-import Image from "next/image";
-import CommonModal from "@/components/theme/ui/CommonModal";
-import type {ProductsInOrderDetails, ProductItemAdditional} from "@/types/customer/type";
-
-
-// 根据products 计算总价和总个数
-function caclculateTotal(products: ProductsInOrderDetails[]) {
-    let total = 0;
-    let totalItems = 0;
-
-    products.forEach((product) => {
-        total += product.total * 100;
-        totalItems += product.qtyOrdered;
-    });
-    let ss = String(total);
-    if(ss.length >= 3) {
-        ss = ss.slice(0, ss.length - 2) + "." + ss.slice(ss.length - 2);
-    } else if(ss.length === 2) {
-        ss = "0." + ss;
-    } else {
-        ss = "0.0" + ss;
-    }
-
-    return { 
-        subTotal: ss, 
-        totalItems: totalItems > 1 ? String(totalItems) + " Items" : String(totalItems) + " Item"
-    };
-}
-
-function getProductAdditionalInfo(productItem: ProductItemAdditional) { 
-    const attributeKeys = Object.keys(productItem.attributes);
-    let res = '';
-    attributeKeys.forEach(key => {
-      const attribute = productItem.attributes[key];
-      if (attribute) {
-        // option_label value_label
-
-        if(res) {
-            res = res + ', ' + attribute.option_label + ': ' + attribute.value_label;
-        } else {
-            res = attribute.option_label + ': ' + attribute.value_label;
-        }
-      }
-    });
-    return res;
-}
-
-export default function ProductsModalPendingOrder({
-    isOpen,
-    products,
-    currencySymbol,
-    onClose,
-}: {
-    isOpen: boolean;
-    products: ProductsInOrderDetails[],
-    currencySymbol: string;
-    onClose: () => void;
-}) {
-
-    const { subTotal, totalItems } = caclculateTotal(products);
-    
-
-    const closeModal = () => {
-        onClose();
-    };
-  return (
-    <CommonModal
-      isOpen={isOpen}
-      onClose={() => closeModal()}
-      header={
-        <div className="relative w-full box-border p-4">
-            <div className="flex justify-between items-center">
-                <span className="text-ly-16 flex-none">Order Summary</span>
-                <button className="flex-none w-6 h-6" onClick={() => closeModal()}>
-                    <svg className="block w-full h-full" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
-                </button>
-            </div>
-            
-            <div className="mt-6 flex justify-between items-center">
-                <span className="text-ly-12">{totalItems}</span>
-                <span className="text-ly-14 font-bold">{currencySymbol + subTotal}</span>
-            </div>
-        </div>
-      }
-      body={
-        <div className="w-full px-4 pb-6">
-            {products.map((product) => {
-                const baseImage = JSON.parse(product.baseImage);
-                return (
-                    <div key={product.id} className="flex justify-between w-full box-border pb-4 border-b-1 border-[#f0f0f0] mt-4 last:border-0 last:pb-0 last:mt-0">
-                        <div className="relative flex-none w-33 h-44 mr-2">
-                            <Image className="object-cover" fill
-                                src={baseImage.medium_image_url}
-                                alt={product.name} 
-                            />
-                        </div>
-                        <div className="flex-auto flex flex-col justify-between ">
-                            <div>
-                                <p className="text-ly-12 leading-ly-20 line-clamp-2 text-ly-placeholder">
-                                    {product.name}
-                                </p>
-                                <p className="text-ly-12 leading-ly-20 mt-3">
-                                    {getProductAdditionalInfo(JSON.parse(product.additional))}
-                                </p>
-                            </div>
-                            <div className="flex justify-between items-center">
-                                <span className="text-ly-14 font-bold">
-                                    {currencySymbol + product.price}
-                                </span>
-                                <span className="text-ly-12">X {product.qtyOrdered}</span>
-                            </div>
-                        </div>
-                    </div>
-                )
-            })}
-            
-        </div>
-      }
-      
-    />
-  )
-}

+ 1 - 0
src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx

@@ -54,6 +54,7 @@ export default function CheckoutGiftCardGate({
             currentPageRef.current = res.pagination.current_page;
         } else {
             showToast(res.message,"danger");
+            setHasMore(false);
         }
     },[showToast]);
 

+ 0 - 1
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -1,6 +1,5 @@
 "use client";
 
-import Image from "next/image";
 import { type Ref, useEffect, useImperativeHandle } from "react";
 import { useForm, get } from "react-hook-form";
 import {useConfig} from "@/utils/hooks/useConfig";

+ 19 - 16
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -10,7 +10,7 @@ import type { OrderDetailsData } from "@/types/customer/type";
 import type { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
 import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
 import LoadingPaymentMethod from "../_components/LoadingPaymentMethod";
-import TitleHeader from "@components/layout/navbar/TitleHeader";
+
 
 // 只能存在一个待支付订单
 export const dynamic = "force-dynamic";
@@ -64,9 +64,8 @@ export default async function ContinueToPay({searchParams}: {
     });
       
     return (<>
-        <TitleHeader title="Continue To Pay" />
         {orderDetailError ?
-            <div className="box-border px-4 w-full">
+            <div className="box-border max-w-312 mx-auto mb-30 mt-6 px-6">
             
                 
                 <div className="w-12 h-12 mx-auto mt-9">
@@ -77,27 +76,31 @@ export default async function ContinueToPay({searchParams}: {
                     <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">
+                <Link href="/" className="mt-6 mx-auto flex justify-center items-center w-30 h-12 border-1 rounded-3xl text-ly-16">
                     Continue Shopping
                 </Link>
             </div>
         :
-            <div className="w-full pb-8">
+            <div className="flex justify-between items-start box-border max-w-312 mx-auto mb-30 mt-6 px-6">
+                <div className="flex-initial w-164 mr-4">
+                    <ContinueToPayOrderInfo orderDetail={orderDetailData} />
+                </div>
             
-                <ContinueToPayOrderInfo orderDetail={orderDetailData} />
+                
             
-                <div className="mt-6 box-border px-4">
-                    <h3 className="text-ly-24 font-medium">Payment Method</h3>
+                <div className="flex-initial w-120 box-border">
+                    <h3 className="text-ly-20 font-medium">Payment Method</h3>
+                    <Suspense fallback={<LoadingPaymentMethod/>}> 
+                        <PaymentMethodContinueTpPay 
+                            orderId={orderDetailData._id}
+                            orderDetail={orderDetailData}
+                            paymentMethodsDataPromise={paymentMethodsResponse}
+                            defaultPaymentMethod={orderDetailData.payment}
+                        />
+                    </Suspense>
 
                 </div>
-                <Suspense fallback={<LoadingPaymentMethod/>}> 
-                    <PaymentMethodContinueTpPay 
-                        orderId={orderDetailData._id}
-                        orderDetail={orderDetailData}
-                        paymentMethodsDataPromise={paymentMethodsResponse}
-                        defaultPaymentMethod={orderDetailData.payment}
-                    />
-                </Suspense>
+                
                 
             </div>
         }

+ 23 - 23
src/app/(public)/paymentresult/_components/CancelOrFailure.tsx

@@ -64,8 +64,8 @@ export default function CancelOrFailure({
 
 
 
-    return (<div className="flex max-w-300 mx-auto items-start">
-        <div className="flex-initial w-164 mr-16">
+    return (<div className="flex items-start justify-between max-w-312 mx-auto px-6">
+        <div className="flex-initial w-164 mr-4">
             <div className="box-border w-full">
                 {paymentStatus === 'failure' &&
                     <>
@@ -115,9 +115,9 @@ export default function CancelOrFailure({
             </div>
             <div className="box-border w-full mt-6">
                 <h4 className="text-ly-20 font-medium">Order Information</h4>
-                <div className="mt-6 flex justify-between">
-                    <div className="text-ly-14 flex-none">Ship to:</div>
-                    <div className="w-117.5 text-ly-14 leading-ly-20 text-[#666666] flex-none">
+                <div className="mt-6 flex justify-between gap-6">
+                    <div className="text-ly-14 flex-none w-35.5">Ship to:</div>
+                    <div className="text-ly-14 leading-ly-20 text-[#666666] flex-1">
                         {
                             shippingAddress.firstName 
                             + ' ' + shippingAddress.lastName 
@@ -130,33 +130,33 @@ export default function CancelOrFailure({
                         }
                     </div>
                 </div>
-                <div className="mt-4 flex justify-between">
-                    <div className="text-ly-14 flex-none">Order number:</div>
-                    <div className="w-117.5 text-ly-14 leading-ly-20 text-[#666666] flex-none">
+                <div className="mt-4 flex justify-between gap-6">
+                    <div className="text-ly-14 flex-none w-35.5">Order number:</div>
+                    <div className="text-ly-14 leading-ly-20 text-[#666666] flex-1">
                         {orderDetailRes?._id}
                     </div>
                 </div>
-                <div className="mt-4 flex justify-between">
-                    <div className="text-ly-14 flex-none">Status:</div>
-                    <div className="w-117.5 text-ly-14 leading-ly-20 text-[#666666] flex-none capitalize">
+                <div className="mt-4 flex justify-between gap-6">
+                    <div className="text-ly-14 flex-none w-35.5">Status:</div>
+                    <div className="text-ly-14 leading-ly-20 text-[#666666] flex-1 capitalize">
                         {orderDetailRes?.status}
                     </div>
                 </div>
-                <div className="mt-4 flex justify-between">
-                    <div className="text-ly-14 flex-none">Order date:</div>
-                    <div className="w-117.5 text-ly-14 leading-ly-20 text-[#666666] flex-none">
+                <div className="mt-4 flex justify-between gap-6">
+                    <div className="text-ly-14 flex-none w-35.5">Order date:</div>
+                    <div className="text-ly-14 leading-ly-20 text-[#666666] flex-1">
                         {orderCreatedAt}
                     </div>
                 </div>
-                <div className="mt-4 flex justify-between">
-                    <div className="text-ly-14 flex-none">Shipping method:</div>
-                    <div className="w-117.5 text-ly-14 leading-ly-20 text-[#666666] flex-none">
+                <div className="mt-4 flex justify-between gap-6">
+                    <div className="text-ly-14 flex-none w-35.5">Shipping method:</div>
+                    <div className="text-ly-14 leading-ly-20 text-[#666666] flex-1">
                         {orderDetailRes?.shippingTitle}
                     </div>
                 </div>
-                <div className="mt-4 flex justify-between">
-                    <div className="text-ly-14 flex-none">Payment method:</div>
-                    <div className="w-117.5 text-ly-14 leading-ly-20 text-[#666666] flex-none">
+                <div className="mt-4 flex justify-between gap-6">
+                    <div className="text-ly-14 flex-none w-35.5">Payment method:</div>
+                    <div className="text-ly-14 leading-ly-20 text-[#666666] flex-1">
                         {orderDetailRes?.payment?.methodTitle}
                     </div>
                 </div>
@@ -164,20 +164,20 @@ export default function CancelOrFailure({
             </div>
         </div>
 
-        <div className="flex-none box-border w-120 border-1 p-4">
+        <div className="flex-initial box-border w-120 border-1 p-4">
             <h3 className="text-ly-20">My Bag({orderDetailRes.totalQtyOrdered})</h3>
             <div className="box-border w-full max-h-130 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200">
                 {productItems.map((product) => {
                     const baseImage = JSON.parse(product.baseImage);
                     return (
                         <div key={product.id} className="w-full flex gap-2 mt-4 pt-4 border-t-1 border-[#f0f0f0]">
-                            <div className="relative w-33 h-44 flex-none">
+                            <div className="relative w-33 h-44 flex-initial">
                                 <Image className="object-cover" fill
                                     src={baseImage.medium_image_url}
                                     alt={product.name} 
                                 />
                             </div>
-                            <div className="flex-initial w-77 flex flex-col justify-between">
+                            <div className="flex-1 flex flex-col justify-between">
                                 <div>
                                     <p className="text-ly-14 leading-ly-20 line-clamp-2">
                                         {product.name}

File diff suppressed because it is too large
+ 23 - 23
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx


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

@@ -1,7 +1,7 @@
 import { notFound } from "next/navigation";
 import { Suspense } from "react";
 import {
-  ProductDetailSkeleton,
+  // ProductDetailSkeleton,
   RelatedProductSkeleton,
 } from "@/components/common/skeleton/ProductSkeleton";
 import { BASE_SCHEMA_URL, PRODUCT_TYPE } from "@/utils/constants";
@@ -13,15 +13,17 @@ import type {
   ProductMediaType,
 } 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 { ProductInformation } from "@/app/(public)/product/_components/ProductInformation";
-import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
-import ProductDescription from "../_components/ProductDescription";
-import SwitchButton from "../_components/SwitchButton";
-import ShopServicePanel from "../_components/ShopServicePanel";
+// import { HeroCarouselShimmer } from "@components/common/slider";
+// import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
+// import { ProductInformation } from "@/app/(public)/product/_components/ProductInformation";
+// import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
+// import ProductDescription from "../_components/ProductDescription";
+// import SwitchButton from "../_components/SwitchButton";
+// import ShopServicePanel from "../_components/ShopServicePanel";
 import Recommend from "../_components/youmaylike/Recommend"
 import { formatFlexibleVariants } from "@/utils/variantTools";
+import ProductTwoColumnLayout from "../_components/ProductTwoColumnLayout";
+import ProductdetailAndQuestion from "../_components/ProductdetailAndQuestion";
 
 // export const dynamic = 'auto'
 // // 'auto' | 'force-dynamic' | 'error' | 'force-static'
@@ -36,6 +38,7 @@ export default async function ProductPage({
 }) {
   const { urlProduct } = await params;
   const fullPath = urlProduct.join("/");
+  console.log('fullPath-----:',fullPath);
   
   const resProductData = await serverGraphqlFetch<SingleProductResponse,{urlKey: string;}>({
       // urlKey, // 产品名称
@@ -48,7 +51,7 @@ export default async function ProductPage({
       
   });
   console.log("productCustomerFeatures----------------------",resProductData);
-  const product = resProductData.data?.product;
+  const product =  resProductData.data?.product;
   const fetchProductError = resProductData.error;
   if (fetchProductError && fetchProductError.extensions.status === 404) return notFound();
   if(!product) throw new Error(fetchProductError?.message ?? 'Get product data failed');
@@ -87,7 +90,7 @@ export default async function ProductPage({
         }}
         type="application/ld+json"
       />
-      <div className="w-full">
+      {/* <div className="w-full">
         <Suspense fallback={<HeroCarouselShimmer />}>
           <ProductMedia mediaData={mediaImgs} />
         </Suspense>
@@ -117,8 +120,25 @@ export default async function ProductPage({
           
         </Suspense>
         <Recommend/>
-      </div>
-        
+      </div> */}
+        {/* 左右两栏滚动跟随布局(客户端组件) */}
+      <ProductTwoColumnLayout
+        mediaImgs={mediaImgs}
+        productId={product._id}
+        productName={product.name || ""}
+        productOptions={productOptions}
+        flexibleVariants={flexibleVariants}
+        isSaleable={product.isSaleable}
+        shortDescription={product.shortDescription || ""}
+        featureHtml={product.feature || ""}
+        descriptionHtml={product.description || ""}
+       
+      />
+      <ProductdetailAndQuestion  
+        productId={product._id}
+        questionTotal={questionTotal}
+        questionList={["aaa"]} />
+      <Recommend/>
       <Suspense fallback={<RelatedProductSkeleton />}>
         <RelatedProductsSection fullPath={fullPath} />
       </Suspense>

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

@@ -5,65 +5,65 @@ import { useState, useEffect, useCallback } from "react";
 import PriceModal from "./popup/PriceModal";
 
 // 价格组件复用你项目自带的Price
-import { Price } from "@components/theme/ui/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 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>
-);
+// 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 = {
@@ -87,7 +87,7 @@ export function ProductAddToCart({
   currencyCode:string;
 }) {
   const btnClass =
-    "flex items-center justify-center rounded-full text-ly-16 font-bold text-white h-[46px]";
+    "flex items-center justify-center rounded-full text-ly-16 font-bold text-white h-[46px] w-full";
   // 滚动控制显示
   const [isShowBar, setIsShowBar] = useState(false);
   const SCROLL_THRESHOLD = 200;
@@ -103,20 +103,20 @@ export function ProductAddToCart({
   }, [handleScroll]);
 
   // 解构价格数据
-  const { totalNowPrice, totalLinePrice, save } = priceInfo;
+  // const { totalNowPrice, totalLinePrice, save } = priceInfo;
   // 是否有折扣(原价≠现价)
-  const hasDiscount = totalLinePrice !== totalNowPrice;
+  // 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",
+        " w-full  bg-white box-border mt-8   transition-all duration-300 ease-in-out",  //fixed z-[1000] left-0 border-t border-solid border-ly-middlegray pt-3 px-4 pb-6
         isShowBar ? "bottom-0" : "-bottom-[160px]",
       )}
     >
       {/* 顶部价格区域 */}
-      <div className="flex items-center justify-between mb-4 w-full">
-        {/* 划线原价(左侧) */}
+      {/* <div className="flex items-center justify-between mb-4 w-full">
+        划线原价(左侧)
         {hasDiscount && (
           <Price
             className="text-ly-gray  text-base line-through "
@@ -126,7 +126,7 @@ export function ProductAddToCart({
         )}
 
         <div className="flex items-center justify-end ">
-          {/* 优惠标签黄色块 */}
+          优惠标签黄色块
           {hasDiscount && (
             <Price
               className="bg-ly-gold px-2  leading-ly-22  text-sm mr-2  font-bold"
@@ -134,29 +134,29 @@ export function ProductAddToCart({
               currencyCode={currencyCode}
             />
           )}
-          {/* 现价 */}
+          现价
           <Price
             className="  text-base font-bold mr-1"
             amount={String(totalNowPrice)}
             currencyCode={currencyCode}
           />
-          {/* 右侧下拉箭头SVG */}
+          右侧下拉箭头SVG
           <span onClick={() => setCouponModalOpen(true)}>
             <ArrowDownSvg />
           </span>
         </div>
-      </div>
+      </div> */}
 
       {/* 底部按钮行:左侧对话图标 + 两个圆角按钮 */}
       {isAvailable ? (
-        <div className="flex items-center gap-3">
+        <div className="">
           {/* 左侧对话图标 */}
-          <ChatSvg />
+          {/* <ChatSvg /> */}
           {/* Buy Now 深色绿按钮 */}
           <button
             onClick={onBuyNow}
             type="button"
-            className={clsx(btnClass, "flex-1 bg-ly-deepgreen")}
+            className={clsx(btnClass, "mb-4 flex-1 bg-ly-deepgreen")}
           >
             Buy Now
           </button>

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

@@ -41,13 +41,13 @@ export default function ProductDescription({ htmlString }: ProductSpecsProps) {
   const needViewMore = specList.length > 4 && !showAll;
 
   return (
-    <div className="w-full px-4">
+    <div className="w-full ">
       {/* 头部标题 + 下拉箭头,可整体收展 */}
       <div
         className="flex justify-between items-center cursor-pointer pb-2"
         onClick={() => setPanelCollapsed(!panelCollapsed)}
       >
-        <h2 className="text-[clamp(1.5rem,3vw,2.25rem)] font-bold">Description</h2>
+        <h2 className="text-ly-18 font-medium leading-ly-26">Description</h2>
         {/* 箭头svg 旋转180代表展开 */}
         <svg
           className={`w-7 h-7 transition-transform duration-200 ${panelCollapsed ? "rotate-180" : ""}`}

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

@@ -1,15 +1,31 @@
 "use client";
-
+// import { useState } from 'react';
 export function ProductDetail({
     detail, 
+    isExpanded,
+    setIsExpanded,
 } : {
     detail:string;
+    isExpanded: boolean;
+    setIsExpanded: (v: boolean) => void;
 }) { 
+    // const [isExpanded, setIsExpanded] = useState(false);
     return (
         <>
-            <div className="box-border w-full pr-4 pl-4">
-                <h3>Product Details</h3>
-                <div dangerouslySetInnerHTML={{ __html: detail }}></div>
+            <div className="box-border w-full ">
+                <h3 className="text-ly-18 font-medium leading-ly-26">Product Details</h3>
+                <div 
+                    className={`transition-all duration-300 overflow-hidden ${
+                    isExpanded ? 'max-h-none' : 'max-h-48'
+                    }`}
+                    dangerouslySetInnerHTML={{ __html: detail }}></div>
+                {/* 展开/收起按钮 */}
+                <button 
+                    onClick={() => setIsExpanded(!isExpanded)}
+                    className="mt-2 w-22.5 block h-8 opacity-100 rounded-2xl mx-auto text-xs text-center leading-8 bg-white border border-black"
+                >
+                    {isExpanded ? 'View Less' : 'View More'}
+                </button>
             </div>
             
         </>

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

@@ -191,7 +191,7 @@ export function ProductInformation({
   }, [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";
+    "text-ly-16 leading-ly-24 border border-solid rounded-lg px-6 py-3 bg-white";
 
   const handlerProductQty = (action: string) => {
     if (action === "increase") {
@@ -251,8 +251,8 @@ export function ProductInformation({
 
   return (
     <>
-      <div className="box-border w-full pr-4 pl-4">
-        <h3 className="text-ly-14 text-black mt-4 leading-ly-22">
+      <div className="box-border w-full ">
+        <h3 className="text-ly-14 text-black  leading-ly-22">
           {name} {isSaleable}
         </h3>
         <ProductText ProductText={shortDescription} />
@@ -333,7 +333,7 @@ export function ProductInformation({
         <PromotionDiscountCardProps productId="product-1001" />
       </div>
       <Guide open={guideModalOpen} onClose={() => setGuideModalOpen(false)} />
-      <div className="box-border w-full pr-4 pl-4 mt-4 relative">
+      <div className="box-border w-full  mt-4 relative">
         <span
           onClick={() => setGuideModalOpen(true)}
           className="absolute text-sm font-medium leading-ly-22 underline text-black top-0 right-4"
@@ -342,7 +342,7 @@ export function ProductInformation({
         </span>
         {productOptions.map((option) => (
           <div key={option.id}>
-            <div className="text-ly-12 font-medium ">{option.label}</div>
+            <div className="text-ly-16 leading-6 font-medium ">{option.label}</div>
 
             <div className="flex flex-wrap gap-3 mt-3">
               {option.values.map((value) => {
@@ -372,7 +372,7 @@ export function ProductInformation({
         ))}
 
         <div className="w-full flex justify-between items-center mt-3">
-          <div className="text-ly-14 text-black  font-medium ">Quantity*</div>
+          <div className="text-ly-16 leading-6 text-black  font-medium ">Quantity*</div>
           <div className="flex border border-solid rounded-lg">
             <button
               onClick={() => handlerProductQty("decrease")}

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

@@ -36,50 +36,19 @@ export function ProductMedia({
   };
 
   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">
-        {/* 下方缩略图轮播 */}
+    <div className="flex flex-row gap-4 w-full items-start">
+      {/* ========== 左侧:垂直缩略图轮播 ========== */}
+      <div className="w-18 shrink-0">
         <Swiper
           onSwiper={(swiper) => (thumbSwiperRef.current = swiper)}
-          slidesPerView={6} // 固定6个缩略图
-          spaceBetween={8}
+          direction="vertical" // 垂直方向滚动
+          slidesPerView={7.2} // 垂直可见几张缩略图,按需调
+          spaceBetween={12}
           watchSlidesProgress
-          className="thumb-swiper w-full"
+          className="thumb-swiper h-[704px]" // 和主图高度对齐,与Image height={520}匹配
         >
           {mediaData.map((img, idx) => (
-            <SwiperSlide key={img.id} className="w-[100px] flex-shrink-0">
+            <SwiperSlide key={img.id} className="h-18">
               <div
                 className={`cursor-pointer border-2 rounded overflow-hidden transition-all ${
                   activeIndex === idx
@@ -87,14 +56,13 @@ export function ProductMedia({
                     : "border-transparent opacity-70 hover:opacity-100"
                 }`}
                 onClick={() => {
-                  // 点击缩略图,主图跳转对应下标
                   mainSwiperRef.current?.slideTo(idx);
                 }}
               >
                 <Image
                   src={getImageUrl(img.publicPath, baseUrl)}
-                  width={100}
-                  height={130}
+                  width={72}
+                  height={96}
                   loading="eager"
                   alt={`Thumbnail ${idx + 1}`}
                   className="w-full h-full object-cover"
@@ -104,6 +72,40 @@ export function ProductMedia({
           ))}
         </Swiper>
       </div>
+
+      {/* ========== 右侧:主图轮播 ========== */}
+      <div className="flex-1 w-[528px] h-[704px]">
+        <Swiper
+          pagination={pagination}
+          modules={[Pagination]}
+          className="product-media-swiper relative w-full h-full"
+          onSlideChange={(swiper) => {
+            setActiveIndex(swiper.activeIndex);
+            thumbSwiperRef.current?.slideTo(swiper.activeIndex);
+          }}
+          onSwiper={(swiper) => {
+            mainSwiperRef.current = swiper;
+          }}
+        >
+          {mediaData.map((img) => (
+            <SwiperSlide className="relative w-full h-full" key={img.id}>
+              <Image
+                className="block w-full h-full object-cover"
+                src={getImageUrl(img.publicPath, baseUrl)}
+                // width={528}
+                // height={704}
+                fill
+                loading="eager"
+                alt="Product wig image"
+              />
+            </SwiperSlide>
+          ))}
+          {/* 自定义分页数字 */}
+          <div slot="container-end">
+            <div className="product-media-swiper-pagination z-1 w-13 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>
     </div>
   ) : (
     <></>

+ 42 - 7
src/app/(public)/product/_components/ProductReviewSection.tsx

@@ -1,27 +1,49 @@
-import ReviewDetail from "@/app/(public)/product/_components/review/ReviewDetail";
+// import ReviewDetail from "@/app/(public)/product/_components/review/ReviewDetail";
+import ReviewModal from "@/app/(public)/product/_components/review/ReviewModal";
 import { NoReview } from "@/app/(public)/product/_components/review/NoReview";
 // import { getProductReviews } from "@utils/hooks/getProductReviews";
-import type { ProductReviewList } from "@/types/products/productDetail";
+import type { ProductReviewList,ProductReviewNode } from "@/types/products/review";
+import type {
+  GalleryItem,
+} from "@/types/products/review";
 import React from "react";
 // 组件入参TS规范
 interface ProductReviewSectionProps {
   productId: string;
+  reviewTotal:number
   reviews: ProductReviewList;
-  modalOpen: boolean;
+  // modalOpen: boolean;
   closeModal: () => void;
   onOpenBaseModal: () => void;
   activeTab: string;
   onChangeTab: (tabKey: string) => void;
+  galleryOpen: boolean;
+  galleryData: {
+    galleryList: GalleryItem[];
+    initialSlide: number;
+  };
+  galleryClose: () => void;
+  // 接收父传进来的打开弹窗函数
+  onOpenGallery: (
+    clickReviewIndex: number,
+    clickImgIndex: number,
+    reviewList: ProductReviewNode[]
+  ) => void;
 }
 
 function ProductReviewSection({
   productId,
   reviews,
-  modalOpen,
+  reviewTotal,
+  // modalOpen,
   closeModal,
   activeTab,
-  onOpenBaseModal,
+  // onOpenBaseModal,
   onChangeTab,
+  galleryOpen, 
+  galleryData, 
+  galleryClose,
+  onOpenGallery
 }: ProductReviewSectionProps) {
   // const getAllreviews = await getProductReviews(productId)
   console.log("productId-------------:", productId, reviews);
@@ -30,15 +52,28 @@ function ProductReviewSection({
       <div className="box-border w-full">
         {reviews.length > 0 ? (
           <>
-            <ReviewDetail
+            {/* <ReviewDetail
+              reviewTotal ={reviewTotal}
               reviewDetails={reviews}
               totalReview={reviews.length}
               productId={productId}
-              modalOpen={modalOpen}
+              // modalOpen={modalOpen}
               onOpenBaseModal={onOpenBaseModal}
               onClose={closeModal}
               activeTab={activeTab}
               onChangeTab={onChangeTab} // 关键:把切换tab的函数传给弹窗
+            /> */}
+            <ReviewModal
+              // open={modalOpen}
+              onClose={closeModal}
+              productId={productId}
+              onChangeTab={onChangeTab}
+              activeTab={activeTab}
+              reviewTotal={reviewTotal}
+              galleryOpen={galleryOpen}
+              galleryData={galleryData}
+              galleryClose={galleryClose}
+              onOpenGallery={onOpenGallery}
             />
           </>
         ) : (

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

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

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

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

+ 25 - 0
src/app/(public)/product/_components/ProductdetailAndQuestion.tsx

@@ -0,0 +1,25 @@
+"use client";
+import SwitchButton from "./SwitchButton";
+
+// props 把服务组件拿到的数据全部透传进来
+type Props = {
+  productId: number;
+  questionTotal: number;
+  questionList: string[];
+};
+
+export default function ProductdetailAndQuestion({
+  productId,
+  questionTotal,
+  questionList,
+}: Props) {
+  return (
+    <>
+      <SwitchButton
+        productId={String(productId)}
+        questionTotal={questionTotal}
+        questionList={questionList}
+      />
+    </>
+  );
+}

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

@@ -14,7 +14,7 @@ export default function ShopServicePanel() {
   const closeVipModal = () => setVipModalOpen(false);
 
   return (
-    <div className="w-full px-4 my-4 ">
+    <div className="w-full  my-4 ">
       {/* 第一项:Shopping Security */}
       <div
         onClick={() => setSecurityModalOpen(true)}

File diff suppressed because it is too large
+ 197 - 57
src/app/(public)/product/_components/SwitchButton.tsx


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

@@ -114,7 +114,7 @@ export default function ShoppingSecurityModal({
     <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="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 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">

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

@@ -26,7 +26,7 @@ export default function VipPlusModal({ isOpen, onClose }: BaseModalProps) {
       />
 
       {/* 弹窗主体容器 */}
-      <div className="absolute bottom-0 w-full max-w-lg bg-white rounded-t-xl overflow-hidden z-10 mx-4">
+      <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 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>

+ 130 - 122
src/app/(public)/product/_components/review/AddProductReviewForm.tsx

@@ -135,128 +135,136 @@ export default function AddProductReviewForm({
   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)}
+    <div className=" fixed inset-0 z-1001 flex items-center justify-center "> 
+        <div
+           onClick={onCloseForm}
+           className="absolute inset-0 bg-black/50"
+         />
+        <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl bg-white rounded-t-xl overflow-hidden z-10 mx-4">
+        <form onSubmit={handleSubmit} className="w-full p-4 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}
           />
-          <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>
+    
+          {/* 底部按钮 */}
+          <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>
+     </div>
+    </div>
   );
 }

+ 46 - 31
src/app/(public)/product/_components/review/HairPhotoGallery.tsx

@@ -1,53 +1,68 @@
-"use client";
 import Image from "next/image";
-
+import type {ProductReviewNode} from "@/types/products/review";
 interface HairPhotoGalleryProps {
-  onOpenModal: () => void;
-  imageList: string[];
+  fullReviewList: ProductReviewNode[];
+  onOpenModal: (
+    clickReviewIndex: number,
+    clickImgIndex: number,
+    fullReviewList: ProductReviewNode[]
+  ) => void;
 }
-
 export default function HairPhotoGallery({
+  fullReviewList,
   onOpenModal,
-  imageList,
 }: HairPhotoGalleryProps) {
-  const displayImages = imageList.slice(0, 4);
+  // 1. 遍历所有评论,把全部图片打平成一维数组,同时保存所属评论下标、图片下标
+  const allImageItems: {
+    reviewIndex: number;
+    imgIndex: number;
+    src: string;
+  }[] = [];
+
+  fullReviewList.forEach((review, reviewIndex) => {
+    // 这里假设图片数组是 review.attachments,按你实际字段替换
+    const imgs = review.attachments || [];
+    imgs.forEach((imgSrc:string, imgIndex:number) => {
+      allImageItems.push({
+        reviewIndex,
+        imgIndex,
+        src: imgSrc,
+      });
+    });
+  });
+
+  // 只展示前4张
+  const previewItems = allImageItems.slice(0, 4);
+  const totalCount = allImageItems.length;
+
+  const handleImageClick = (item: typeof previewItems[number]) => {
+    onOpenModal(item.reviewIndex, item.imgIndex, fullReviewList);
+  };
+
   return (
     <div className="w-full flex gap-2 overflow-hidden my-4">
-      {displayImages.map((src, idx) => (
+      {previewItems.map((item, displayIdx) => (
         <div
-          key={idx}
-          onClick={onOpenModal}
+          key={`${item.reviewIndex}-${item.imgIndex}`}
+          onClick={() => handleImageClick(item)}
           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"
+            src={item.src}
+            alt={`hair ${displayIdx}`}
+            width={144}
+            height={192}
+            className="w-36 object-cover"
           />
-          {idx == 3 ? (
+          {/* 第4张(displayIdx===3)并且总图片大于4,显示遮罩 */}
+          {displayIdx === 3 && totalCount > 4 ? (
             <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>
-          ) : (
-            ""
-          )}
+          ) : null}
         </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>
   );
 }

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

@@ -3,22 +3,22 @@
 import { useState } from "react";
 // import { Modal, ModalContent } from "@heroui/modal";
 // import AddProductReview from "./AddProductReview";
-import { ReviewButton } from "@components/common/button/ReviewButton";
-import AddProductReviewForm from "./AddProductReviewForm";
+// import { ReviewButton } from "@components/common/button/ReviewButton";
+// import AddProductReviewForm from "./AddProductReviewForm";
 import QuestionInputModal from "./QuestionInputModal";
 
 export default function ReviewAdd({ productId }: { productId: string }) {
-  const [open, setOpen] = useState(false);
+  const [, setOpen] = useState(false);
   const [qusopen, setQueopen] = useState(false);
-  const handleCloseForm = () => setOpen(false);
-  const setShowForm = () => {
-    setOpen(() => true);
-    setQueopen(() => false);
-  };
+  // const handleCloseForm = () => setOpen(false);
+  // const setShowForm = () => {
+  //   setOpen(() => true);
+  //   setQueopen(() => false);
+  // };
   return (
     <>
       <div className="flex items-center justify-start gap-3">
-        <ReviewButton setShowForm={setShowForm} />
+        {/* <ReviewButton setShowForm={setShowForm} /> */}
         <div
           onClick={() => {
             setOpen(() => false);
@@ -30,13 +30,13 @@ export default function ReviewAdd({ productId }: { productId: string }) {
         </div>
       </div>
       {/* 页面内表单区块,非弹窗 */}
-      {open && (
+      {/* {open && (
         <AddProductReviewForm
           productId={productId}
           // 传关闭回调函数,表单内部Cancel/提交成功都会调用
           onCloseForm={handleCloseForm}
         />
-      )}
+      )} */}
       {qusopen && (
         <QuestionInputModal
           productId={productId}

+ 16 - 13
src/app/(public)/product/_components/review/ReviewDetail.tsx

@@ -12,7 +12,7 @@ import { Avatar } from "@heroui/avatar";
 import clsx from "clsx";
 import React, { type FC, useState } from "react";
 import type { ProductReviewNode } from "@/components/catalog/type";
-import ReviewModal from "./ReviewModal";
+// import ReviewModal from "./ReviewModal";
 // import { ReviewTabType } from "@/types/products/review";
 import { useCustomToast } from "@utils/hooks/useToast";
 import { clientFetch } from "@/lib/restApiClient";
@@ -26,7 +26,8 @@ interface ReviewDetailProps {
   reviewDetails: ProductReviewEdge[];
   totalReview: number;
   productId: string;
-  modalOpen: boolean;
+  reviewTotal:number,
+  // modalOpen: boolean;
   onClose: () => void;
   activeTab: string;
   onChangeTab: (tabKey: string) => void;
@@ -35,13 +36,14 @@ interface ReviewDetailProps {
 
 const ReviewDetail: FC<ReviewDetailProps> = ({
   reviewDetails,
+  // reviewTotal,
   // totalReview,
   productId,
-  modalOpen,
-  onClose,
-  activeTab,
-  onChangeTab,
-  onOpenBaseModal,
+  // modalOpen,
+  // onClose,
+  // activeTab,
+  // onChangeTab,
+  // onOpenBaseModal,
 }) => {
  
   const [visibleCount, setVisibleCount] = useState(5);
@@ -182,7 +184,7 @@ const ReviewDetail: FC<ReviewDetailProps> = ({
            "
             //   max-h-[380px] scrollbar-thin scrollbar-track-transparent  scrollbar-thumb-neutral-400 dark:scrollbar-thumb-neutral-600
           >
-            {visibleReviews.map((item: any, index: number) => {
+            {false && visibleReviews.map((item: any, index: number) => {
               const { liked, like_count } = getCurrentLike(item);
               return (
                 <div
@@ -346,25 +348,26 @@ const ReviewDetail: FC<ReviewDetailProps> = ({
                 </button>
               </div>
             )}
-            <div className="mt-6 mb-8 flex justify-center">
+            {/* <div className="mt-6 mb-8 flex justify-center">
               <button
                 onClick={onOpenBaseModal}
                 className="bg-white-700 text-black px-9 border border-black py-1.5 text-xs font-normal leading-5 rounded-full"
               >
                 Open Reviews
               </button>
-            </div>
+            </div> */}
           </div>
         </div>
       </div>
       {/* 仅传递3个控制参数,无评论数据 */}
-      <ReviewModal
-        open={modalOpen}
+      {/* <ReviewModal
+        // open={modalOpen}
         onClose={onClose}
         productId={productId}
         onChangeTab={onChangeTab}
         activeTab={activeTab}
-      />
+        reviewTotal={reviewTotal}
+      /> */}
     </>
   );
 };

+ 101 - 95
src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx

@@ -36,104 +36,110 @@ export default function ReviewImageGalleryModal({
   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="fixed inset-0  z-[100]  flex flex-col">
+        <div
+           onClick={onClose}
+           className="absolute inset-0 bg-black/50"
+        />
+        {/* 顶部关闭按钮 */}
+        <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl bg-white rounded-t-xl overflow-hidden z-10 ">
+            <div className="flex bg-black 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) => {
-            const likeInfo = getCurrentLike(item.review);
-            return (
-              <SwiperSlide
-                key={idx}
-                className="flex flex-col justify-between h-full py-4"
+            {/* 轮播大图 */}
+            <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)}
               >
-                {/* 大图 */}
-                <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>
-                    <div
-                      className="ml-auto flex items-center gap-1 cursor-pointer"
-                      onClick={() => onLike(item.review)}
+                {galleryList.map((item, idx) => {
+                  const likeInfo = getCurrentLike(item.review);
+                  return (
+                    <SwiperSlide
+                      key={idx}
+                      className="flex flex-col justify-between h-full py-4"
                     >
-                      <span
-                        className={likeInfo.liked ? "text-[#036141FF]" : ""}
-                      >
-                        {likeInfo.like_count}
-                      </span>
-                      {likeInfo.liked ? (
-                        <svg
-                          xmlns="http://www.w3.org/2000/svg"
-                          width="16"
-                          height="16"
-                          viewBox="0 0 16 16"
-                          fill="#036141FF"
-                        >
-                          <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>
-                      ) : (
-                        <svg
-                          xmlns="http://www.w3.org/2000/svg"
-                          width="16"
-                          height="16"
-                          viewBox="0 0 16 16"
-                          fill="white"
-                        >
-                          <path d="M8 2.748l-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.981 6.186 6.362 3.352-2.38 5.265-4.547 6.186-6.362.955-1.884.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01L8 2.748zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143c.06.055.119.112.176.171a3.12 3.12 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15z" />
-                        </svg>
-                      )}
-                    </div>
-                    {/* <span className="ml-auto">{item.review.like_count} 👍</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 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 ">
+                        <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>
+                          <div
+                            className="ml-auto flex items-center gap-1 cursor-pointer"
+                            onClick={() => onLike(item.review)}
+                          >
+                            <span
+                              className={likeInfo.liked ? "text-[#036141FF]" : ""}
+                            >
+                              {likeInfo.like_count}
+                            </span>
+                            {likeInfo.liked ? (
+                              <svg
+                                xmlns="http://www.w3.org/2000/svg"
+                                width="16"
+                                height="16"
+                                viewBox="0 0 16 16"
+                                fill="#036141FF"
+                              >
+                                <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>
+                            ) : (
+                              <svg
+                                xmlns="http://www.w3.org/2000/svg"
+                                width="16"
+                                height="16"
+                                viewBox="0 0 16 16"
+                                fill="white"
+                              >
+                                <path d="M8 2.748l-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.981 6.186 6.362 3.352-2.38 5.265-4.547 6.186-6.362.955-1.884.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01L8 2.748zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143c.06.055.119.112.176.171a3.12 3.12 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15z" />
+                              </svg>
+                            )}
+                          </div>
+                          {/* <span className="ml-auto">{item.review.like_count} 👍</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>
     </div>
   );

+ 277 - 181
src/app/(public)/product/_components/review/ReviewModal.tsx

@@ -2,7 +2,8 @@
 import React, { useState, useRef, useEffect, useCallback } from "react";
 import type {
   ReviewTabType,
-  ProductReviewList,
+  // ProductReviewList,
+  ProductReviewNode,
   GalleryItem,
 } from "@/types/products/review";
 import ReviewImageGalleryModal from "./ReviewImageGalleryModal";
@@ -11,15 +12,28 @@ import { clientFetch } from "@/lib/restApiClient";
 import { useCustomToast } from "@utils/hooks/useToast";
 import { getUuId } from "@/utils/helper";
 // 分页常量
-const PAGE_SIZE = 10;
+const PAGE_SIZE = 5;
 
 interface ReviewModalProps {
-  open: boolean;
+  // open: boolean;
+  reviewTotal:number,
   onClose: () => void;
   onChangeTab: (tabKey: string) => void;
   productId: string;
   // 父组件传入默认Tab all / newest / photos
   activeTab: ReviewTabType;
+  galleryOpen: boolean;
+  galleryData: {
+    galleryList: GalleryItem[];
+    initialSlide: number;
+  };
+  galleryClose: () => void;
+  // 接收父传进来的打开弹窗函数
+  onOpenGallery: (
+    clickReviewIndex: number,
+    clickImgIndex: number,
+    reviewList: ProductReviewNode[]
+  ) => void;
 }
 // 模拟后端请求函数,替换为你的真实接口
 async function fetchReviewApi(
@@ -27,93 +41,27 @@ async function fetchReviewApi(
   tab: ReviewTabType,
   pageNum: number,
   guestId: string | null,
-): Promise<{ list: ProductReviewList; total: number }> {
-  // 模拟网络延迟,模拟真实请求loading
-  // await new Promise((resolve) => setTimeout(resolve, 700));
-  //     return {
-  //     list: [],
-  //     total: 100,
-  //   };
-  // 全局固定100条模拟评论总数据
-  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  signal?: AbortSignal
+): Promise<{ list: ProductReviewNode[]; total: number }> {
+  
   let has_images = 0;
   if (tab == "photos") {
     has_images = 1;
   }
   const res = await clientFetch(
     `/api/shop/products/${pid}/reviews?page=${pageNum}&per_page=${PAGE_SIZE}&has_images=${has_images}&sort=${tab}&client_id=${guestId}`,
+    {
+      signal
+    }
   );
   console.log(
     "弹窗内res-=---------------------------------aa:",
     res?.data,
   );
-
-  // const mockTotal = 100;
-  // const fullMockData: ProductReviewList = [];
-
-  // for (let i = 1; i <= mockTotal; i++) {
-  //   // 30条带图片评论,70条无图
-  //   const hasImage = i <= 30;
-  //   // 带图评论随机1~3张图片
-  //   const imgCount = hasImage ? Math.floor(Math.random() * 3) + 1 : 0;
-  //   const attachments: string[] = [];
-  //   for (let j = 1; j <= imgCount; j++) {
-  //     attachments.push(
-  //       `https://cdn.alipearlhair.com/media/reviewimages/cache/6/image/150x/040ec09b1e35df139433887a97daa66f/reviewimages/7O_A6W92AW3D_87N5PR8CO.png`,
-  //     );
-  //   }
-
-  //   // 随机星级1-5
-  //   const randomRating = Math.floor(Math.random() * 5) + 1;
-  //   // 随机标签
-  //   const tagArr: ("influencer" | "verified" | "")[] = [
-  //     "",
-  //     "verified",
-  //     "influencer",
-  //   ];
-  //   const randomTag = tagArr[Math.floor(Math.random() * tagArr.length)];
-  //   // 随机时间,用于Newest排序
-  //   const randomDayOffset = Math.floor(Math.random() * 90);
-  //   const createDate = new Date();
-  //   createDate.setDate(createDate.getDate() - randomDayOffset);
-
-  //   fullMockData.push({
-  //     __typename: "ProductReviewEdge",
-  //     node: {
-  //       name: `D****${i}`,
-  //       title: `${16 + Math.floor(Math.random() * 3)}`,
-  //       rating: randomRating,
-  //       comment:
-  //         "This lady is so sweetly gorgeous. I love the curl pattern, the side part makes it worth every penny spent. Hair is soft and natural, I bleached it the day she arrived.",
-  //       createdAt: createDate.toISOString(),
-  //       attachments,
-  //       isVip: true,
-  //       tag: randomTag,
-  //       likeCount: 1000 + Math.floor(Math.random() * 500),
-  //     },
-  //   });
-  // }
   // 根据tab筛选数据
   const filteredList = [...res?.data];
   const totalReview = res?.pagination?.total;
-  // if (tab === "newest") {
-  //   // 最新:创建时间倒序
-  //   filteredList.sort(
-  //     (a, b) =>
-  //       new Date(b.node.createdAt).getTime() -
-  //       new Date(a.node.createdAt).getTime(),
-  //   );
-  // } else if (tab === "photos") {
-  //   // 仅保留带图片评论
-  //   filteredList = filteredList.filter(
-  //     (item) => item.node.attachments.length > 0,
-  //   );
-  // }
-
-  // 分页切片
-  // const startIndex = (pageNum - 1) * PAGE_SIZE;
-  // const pageData = filteredList.slice(startIndex, startIndex + PAGE_SIZE);
-
+  
   return {
     list: filteredList,
     total: totalReview,
@@ -122,14 +70,19 @@ async function fetchReviewApi(
 
 const ReviewModal: React.FC<ReviewModalProps> = React.memo(
   function ReviewModal({
-    open,
-    onClose,
+    // open,
+    reviewTotal,
+    // onClose,
     productId,
     onChangeTab,
     activeTab,
+    galleryOpen, 
+    galleryData, 
+    galleryClose,
+    onOpenGallery
   }) {
     // 全部原始评论数据(接口返回完整数据缓存)
-    const [fullReviewList, setFullReviewList] = useState<ProductReviewList>([]);
+    const [fullReviewList, setFullReviewList] = useState<ProductReviewNode[]>([]);
     // 分页状态
     const [page, setPage] = useState(1);
     const [loading, setLoading] = useState(false);
@@ -137,17 +90,21 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
     const scrollContainerRef = useRef<HTMLDivElement>(null);
     const { showToast } = useCustomToast();
     // 图库弹窗状态
-    const [galleryOpen, setGalleryOpen] = useState(false);
-    const [galleryData, setGalleryData] = useState<{
-      galleryList: GalleryItem[];
-      initialSlide: number;
-    }>({ galleryList: [], initialSlide: 0 });
+    // const [galleryOpen, setGalleryOpen] = useState(false);
+    const totalPage = Math.ceil(reviewTotal / PAGE_SIZE);
+    const [pageInputVal, setPageInputVal] = useState<string>(String(page));
+    // const [galleryData, setGalleryData] = useState<{
+    //   galleryList: GalleryItem[];
+    //   initialSlide: number;
+    // }>({ galleryList: [], initialSlide: 0 });
     const [localLikeMap, setLocalLikeMap] = useState<
       Record<string, { liked: boolean; like_count: number }>
     >({});
     const handleTabClick = useCallback(
       (tabKey: ReviewTabType) => {
         onChangeTab(tabKey);
+        setIsOpen(false);
+        setPageInputVal(String(1));
       },
       [onChangeTab],
     );
@@ -155,108 +112,125 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
       console.log("activeTab 变更:", activeTab);
     }, [activeTab]);
 
-    // ========== 弹窗打开 / Tab切换 / 分页 触发接口请求 ==========
+   
     const loadReviewData = useCallback(
-      async (tab: ReviewTabType, pageNum: number, reset = false) => {
-        setLoading(true);
-        try {
-          const uuid = getUuId();
-          const res = await fetchReviewApi(productId, tab, pageNum, uuid);
-          if (reset) {
-            // 切换Tab/首次打开,重置列表
+        async (tab: ReviewTabType, pageNum: number) => {
+          const controller = new AbortController();
+          const signal = controller.signal;
+          setLoading(true);
+          try {
+            const uuid = getUuId();
+            // debugger
+            const res = await fetchReviewApi(productId, tab, pageNum, uuid, signal);
+            if (signal.aborted) return;
+            // 直接覆盖,不再追加
             setFullReviewList(res.list);
-          } else {
-            // 触底分页,追加数据
-            setFullReviewList((prev) => [...prev, ...res.list]);
+            setHasMore(pageNum * PAGE_SIZE < res.total);
+          } catch (err: any) {
+            if (err.name !== "AbortError") {
+              console.error("加载评论失败", err);
+            }
+          } finally {
+            if (!signal.aborted) {
+              setLoading(false);
+            }
           }
-          // 判断是否还有下一页
-          setHasMore(pageNum * PAGE_SIZE < res.total);
-        } catch (err) {
-          console.error("加载评论失败", err);
-        } finally {
-          setLoading(false);
-        }
-      },
-      [productId],
+          return () => controller.abort();
+        },
+        [productId],
     );
     // 切换Tab,重置分页、重新请求第一页
     useEffect(() => {
-      if (open) {
+      // if (open) {
         setPage(1);
         setHasMore(true);
-        loadReviewData(activeTab, 1, true);
+        loadReviewData(activeTab, 1);
         if (scrollContainerRef.current)
           scrollContainerRef.current.scrollTop = 0;
-      }
-    }, [activeTab, open, loadReviewData]);
+      // }
+    }, [activeTab,  loadReviewData]);
 
     // 筛选当前Tab展示的评论
     const filterReviewList = useCallback(() => {
       const list = [...fullReviewList];
-      // if (activeTab === "newest") {
-      //   list.sort(
-      //     (a, b) =>
-      //       new Date(b.node.createdAt).getTime() -
-      //       new Date(a.node.createdAt).getTime(),
-      //   );
-      // } else if (activeTab === "photos") {
-      //   list = list.filter((item) => item.node.attachments.length > 0);
-      // }
-      // console.log("list----------------------------",list);
       return list;
     }, [activeTab, fullReviewList]);
 
     // 当前分页渲染数据
-    const displayList = filterReviewList(); //.slice(0, page * PAGE_SIZE)
-
+    const displayList = filterReviewList(); 
+    // debugger;
     // 生成图库一维图片数组
-    const buildGalleryList = useCallback((): GalleryItem[] => {
-      const allGallery: GalleryItem[] = [];
-      filterReviewList().forEach((edge: any, reviewIdx) => {
-        edge.attachments.forEach((imgUrl: any) => {
-          allGallery.push({
-            reviewIndex: reviewIdx,
-            imageUrl: imgUrl,
-            review: edge,
-          });
-        });
-      });
-      return allGallery;
-    }, [filterReviewList]);
-
-    // 触底加载更多
-    const handleScroll = useCallback(() => {
-      if (!scrollContainerRef.current || loading || !hasMore) return;
-      const el = scrollContainerRef.current;
-      const isBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 100;
-      if (isBottom) {
-        const nextPage = page + 1;
-        setPage(nextPage);
-        loadReviewData(activeTab, nextPage, false);
+  //  const buildGalleryList = useCallback((reviewList: any[]): GalleryItem[] => {
+  //    const allGallery: GalleryItem[] = [];
+  //    reviewList.forEach((edge: any, reviewIdx) => {
+  //      edge.attachments.forEach((imgUrl: any) => {
+  //        allGallery.push({
+  //          reviewIndex: reviewIdx,
+  //          imageUrl: imgUrl,
+  //          review: edge,
+  //        });
+  //      });
+  //    });
+  //    return allGallery;
+  //  }, []);
+    // 页码切换
+      const handlePageChange = (newPage: number) => {
+        // 边界校验
+        if (newPage < 1 || newPage > totalPage) return;
+        setPage(newPage);
+        setPageInputVal(String(newPage));
+        // 重置滚动到顶部
+        scrollContainerRef.current?.scrollTo({ top: 0, behavior: "smooth" });
+        // 触发接口请求,加载对应页评论(你原有loadReviews逻辑)
+        // fetchReviews(newPage);
+        loadReviewData(activeTab, newPage);
+      };
+      
+      // 输入框回车/失焦跳转
+      const handlePageInputSubmit = () => {
+        const num = Number(pageInputVal);
+        if (Number.isInteger(num) && num >= 1 && num <= totalPage) {
+          handlePageChange(num);
+        } else {
+          // 非法值回退当前页
+          setPageInputVal(String(page));
+        }
+      };
+      
+      // 渲染中间页码(只展示当前附近页码,和截图一致最多5个页码按钮)
+      const renderPageNumbers = () => {
+        const pages: number[] = [];
+        let start = Math.max(1, page - 2);
+        const end = Math.min(totalPage, start + 4);
+        // 修正,保证最多5个
+        if(end - start <4) start = Math.max(1, end -4);
+        for(let i=start; i<=end; i++) pages.push(i);
+        return pages;
       }
-    }, [loading, hasMore, page, activeTab, loadReviewData]);
 
     // 打开图片预览弹窗,定位点击图片下标
-    const openGalleryModal = (
-      clickReviewIndex: number,
-      clickImgIndex: number,
-    ) => {
-      console.log(
-        "clickReviewIndex:",
-        clickReviewIndex,
-        "clickImgIndex:",
-        clickImgIndex,
-      );
+    // const openGalleryModal = (
+    //   clickReviewIndex: number,
+    //   clickImgIndex: number,
+    //   reviewList: ProductReviewList
+    // ) => {
+    //   console.log(
+    //     "clickReviewIndex:",
+    //     clickReviewIndex,
+    //     "clickImgIndex:",
+    //     clickImgIndex,
+    //   );
 
-      const galleryAll = buildGalleryList();
-      const targetReview: any = filterReviewList()[clickReviewIndex];
-      const targetImg = targetReview.attachments[clickImgIndex];
-      const targetSlide = galleryAll.findIndex(
-        (g) => g.reviewIndex === clickReviewIndex && g.imageUrl === targetImg,
-      );
-      setGalleryData({ galleryList: galleryAll, initialSlide: targetSlide });
-      setGalleryOpen(true);
-    };
+    //   const galleryAll = buildGalleryList(reviewList);
+    //   const targetReview: any = reviewList[clickReviewIndex];
+    //   const targetImg = targetReview.attachments[clickImgIndex];
+
+    //   const targetSlide = galleryAll.findIndex(
+    //     (g) => g.reviewIndex === clickReviewIndex && g.imageUrl === targetImg
+    //   );
+    //   setGalleryData({ galleryList: galleryAll, initialSlide: targetSlide });
+    //   setGalleryOpen(true);
+    // };
     const getCurrentLike = (item: any) => {
       const cache = localLikeMap[item.id];
       if (cache) return cache;
@@ -324,16 +298,34 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
         showToast("点赞失败,请稍后重试");
       }
     };
-
-    if (!open) return null;
-
+// 映射显示文本
+const tabLabelMap: Record<ReviewTabType, string> = {
+    all: "ALL",
+    newest: "Newest",
+    photos: "Photos",
+};
+    // if (!open) return null;
+    const [isOpen, setIsOpen] = useState(false);
+    const dropdownRef = useRef<HTMLDivElement>(null);
+    // 点击外部关闭下拉
+    useEffect(() => {
+        const handleClickOutside = (event: MouseEvent) => {
+            if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+                setIsOpen(false);
+            }
+        };
+        document.addEventListener("mousedown", handleClickOutside);
+        return () => document.removeEventListener("mousedown", handleClickOutside);
+    }, []);
+ 
     return (
       <>
         {/* 遮罩层 */}
-        <div className="fixed inset-0 bg-black/60 z-[90] flex items-center justify-center ">
-          <div className="w-full max-w-3xl h-full bg-white flex flex-col overflow-hidden">
+        <div className="w-full flex items-center justify-center ">
+          {/* //fixed inset-0 bg-black/60 z-[90]    */}
+          <div className="w-full  h-full bg-white flex flex-col overflow-hidden">
             {/* 顶部固定头部:返回 + Reviews标题 */}
-            <div className="flex items-center justify-between p-4 border-b border-gray-200 shrink-0">
+            {/* <div className="flex items-center justify-between p-4 border-b border-gray-200 shrink-0">
               <button onClick={onClose} className="shrink-0">
                 <svg
                   width="24"
@@ -349,10 +341,10 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
               </button>
               <h2 className="text-2xl font-semibold">Reviews</h2>
               <div className="w-6 shrink-0"></div>
-            </div>
+            </div> */}
 
             {/* Tab切换栏 固定 */}
-            <div className="flex gap-3 px-4 py-3 border-b border-gray-200 shrink-0">
+            {/* <div className="flex gap-3 px-4 py-3 border-b border-gray-200 shrink-0">
               {(["all", "newest", "photos"] as ReviewTabType[]).map((tab) => (
                 <button
                   key={tab}
@@ -370,13 +362,53 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
                       : "Photos"}
                 </button>
               ))}
+            </div> */}
+            <div className="flex items-center justify-between  py-3 border-b border-gray-200 shrink-0">
+            {/* 左侧 Reviews (数量) */}
+            <div className="text-xl font-medium">Reviews ({reviewTotal})</div>
+
+            {/* 右侧下拉选择器 */}
+            <div ref={dropdownRef} className="relative">
+                {/* 触发器:文字+箭头 */}
+                <button
+                    onClick={() => setIsOpen(!isOpen)}
+                    className="flex items-center gap-2 text-lg cursor-pointer"
+                >
+                    {tabLabelMap[activeTab]}
+                    <svg
+                        className={`w-4 h-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
+                        fill="none"
+                        viewBox="0 0 24 24"
+                        stroke="currentColor"
+                    >
+                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
+                    </svg>
+                </button>
+
+                {/* 下拉菜单 */}
+                {isOpen && (
+                    <div className="absolute right-0 mt-2 w-36 bg-white shadow-lg rounded-md z-20 border border-gray-200">
+                        {(["all", "newest", "photos"] as ReviewTabType[]).map((tab) => (
+                            <button
+                                key={tab}
+                                onClick={() => handleTabClick(tab)}
+                                className={`w-full text-left px-4 py-2 hover:bg-gray-100 ${
+                                    activeTab === tab ? "font-semibold" : ""
+                                }`}
+                            >
+                                {tabLabelMap[tab]}
+                            </button>
+                        ))}
+                    </div>
+                )}
             </div>
+        </div>
 
             {/* 可滚动评论列表区域 */}
             <div
               ref={scrollContainerRef}
-              onScroll={handleScroll}
-              className="flex-1 overflow-y-auto px-4 py-4 space-y-6"
+              // onScroll={handleScroll}
+              className="flex-1 overflow-y-auto  py-4 space-y-6"
             >
               {loading && page === 1 ? (
                 <p className="text-center py-10 text-gray-400">
@@ -385,7 +417,9 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
               ) : displayList.length === 0 ? (
                 <p className="text-center text-gray-500 py-10">No reviews</p>
               ) : (
-                displayList.map((item: any, reviewIdx) => {
+                displayList.map((edge: ProductReviewNode, reviewIdx) => {
+                  const item = edge;
+                  // debugger;
                   const { liked, like_count } = getCurrentLike(item);
                   // const item = edge.node;
                   return (
@@ -508,9 +542,7 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
                           {item.attachments.map((img: any, imgIdx: any) => (
                             <button
                               key={imgIdx}
-                              onClick={() =>
-                                openGalleryModal(reviewIdx, imgIdx)
-                              }
+                              onClick={() => onOpenGallery(reviewIdx, imgIdx, displayList)}
                               className="w-28 h-28 rounded-lg overflow-hidden"
                             >
                               <Image
@@ -530,7 +562,7 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
               )}
               {loading && page > 1 && (
                 <p className="text-center py-4 text-gray-400">
-                  Loading more...
+                  Loading ...
                 </p>
               )}
               {!hasMore && displayList.length > 0 && (
@@ -541,17 +573,81 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
             </div>
           </div>
         </div>
-
         {/* 图片预览弹窗 完全不变 */}
         <ReviewImageGalleryModal
           open={galleryOpen}
-          onClose={() => setGalleryOpen(false)}
+          onClose={galleryClose}
           galleryList={galleryData.galleryList}
           initialSlide={galleryData.initialSlide}
-          //直接绑定父原有点赞函数,不需要任何修改
           onLike={handleLikeClick}
           getCurrentLike={getCurrentLike}
         />
+        <div className="flex items-center justify-center gap-3 py-6  shrink-0">
+            <button
+              onClick={()=>handlePageChange(page -1)}
+              disabled={page <= 1}
+              className={`px-1 py-1 ${page <=1 ? "opacity-40 cursor-not-allowed" : "cursor-pointer"}`}
+            >
+              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+                <polyline points="15 18 9 12 15 6"></polyline>
+              </svg>
+            </button>
+            {renderPageNumbers().map(p=> (
+              <button
+                key={p}
+                onClick={()=>handlePageChange(p)}
+                className={`w-9 h-9 rounded-full text-base transition-colors
+                  ${page === p 
+                    ? "bg-[#0F6636] text-white" 
+                    : "hover:bg-gray-100 text-gray-700"}
+                `}
+              >
+                {p}
+              </button>
+            ))}
+
+            {/* 下一页 */}
+            <button
+              onClick={()=>handlePageChange(page +1)}
+              disabled={page >= totalPage}
+              className={`px-1 py-1 ${page >= totalPage ? "opacity-40 cursor-not-allowed" : "cursor-pointer"}`}
+            >
+              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+                <polyline points="9 18 15 12 9 6"></polyline>
+              </svg>
+            </button>
+
+           
+            <div className="flex items-center ml-4">
+              <button
+                onClick={()=>handlePageChange(page -1)}
+                disabled={page <=1}
+                className={`px-1 ${page <=1 ? "opacity-40" : ""}`}
+              >
+                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+                  <polyline points="15 18 9 12 15 6"></polyline>
+                </svg>
+              </button>
+              <input
+                value={pageInputVal}
+                onChange={(e)=>setPageInputVal(e.target.value)}
+                onKeyDown={(e)=> e.key === 'Enter' && handlePageInputSubmit()}
+                onBlur={handlePageInputSubmit}
+                className="w-14 h-9 border border-gray-300 rounded-md text-center outline-none focus:border-[#0F6636]"
+                type="text"
+              />
+              <span className="mx-1">/ {totalPage}</span>
+              <button
+                onClick={()=>handlePageChange(page +1)}
+                disabled={page >= totalPage}
+                className={`px-1 ${page >= totalPage ? "opacity-40" : ""}`}
+              >
+                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+                  <polyline points="9 18 15 12 9 6"></polyline>
+                </svg>
+              </button>
+            </div>
+        </div>
       </>
     );
   },

File diff suppressed because it is too large
+ 52 - 29
src/app/(public)/product/_components/youmaylike/Recommend.tsx


+ 1 - 0
src/app/globals.css

@@ -116,6 +116,7 @@ html {
 body {
   background: var(--background);
   color: var(--foreground);
+  min-width: 768px;
 }
 
 @layer components {

+ 2 - 2
src/components/common/button/ReviewButton.tsx

@@ -8,7 +8,7 @@ export const ReviewButton = ({ setShowForm, className }: { setShowForm: (show: b
     const router = useRouter();
     const handleAddReview = async () => {
         const session = await getSession()
-        if (!session) {
+        if (false&&!session) {
             router.push("/customer/login");
         } else {
             setShowForm(true);
@@ -18,7 +18,7 @@ export const ReviewButton = ({ setShowForm, className }: { setShowForm: (show: b
     return (
         <button
             onClick={handleAddReview}
-            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}`}
+            className={`relative flex font-normal text-ly-14 leading-ly-24 text-white  py-1 px-3.5  cursor-pointer h-fit items-center justify-center rounded-full bg-black p-4 tracking-wide ${className}`}
         > 
             Write a review
         </button>

+ 1 - 1
src/components/theme/ui/kernel/confirm/host.tsx

@@ -33,7 +33,7 @@ export function ConfirmHost() {
                 <div className="w-80 p-3 bg-white rounded-xl">
                     <h3 className="text-center text-ly-18 font-bold mb-6">{current.title}</h3>
                     <p className="text-ly-16 leading-5 text-center">{current.content}</p>
-                    <div className="w-full flex justify-between gap-3 mt-6">
+                    <div className="w-full flex justify-center gap-3 mt-6">
                         {!current.noOk && <button className="w-35.5 h-12 flex justify-center items-center rounded-3xl border-1 text-ly-16 font-medium" onClick={() => close(true)}>Yes</button>}
                         {!current.noCancel && <button className="w-35.5 h-12 flex justify-center items-center rounded-3xl bg-ly-green text-white text-ly-16 font-medium" onClick={() => close(false)}>Cancel</button>}
                     </div>

+ 3 - 2
src/providers/SocialShareProvider.tsx

@@ -1,5 +1,5 @@
 "use client";
-
+/*
 import Script from "next/script";
 import {
   createContext,
@@ -121,4 +121,5 @@ export const useSocialShareContext = () => {
     }
 
     return context;
-};
+};
+*/

+ 1 - 0
src/types/products/review.ts

@@ -12,6 +12,7 @@ export interface ProductReviewNode {
   isVip: boolean;
   tag: "influencer" | "verified" | "";
   like_count: number;
+  liked:boolean;
 }
 
 export interface ProductReviewEdge {