Ver código fonte

产品详情页调整

zhangzf 3 dias atrás
pai
commit
61ae3614c5
23 arquivos alterados com 1071 adições e 564 exclusões
  1. 24 4
      src/app/(public)/product/[...urlProduct]/page.tsx
  2. 11 11
      src/app/(public)/product/_components/ProductAddToCart.tsx
  3. 2 2
      src/app/(public)/product/_components/ProductDescription.tsx
  4. 20 4
      src/app/(public)/product/_components/ProductDetail.tsx
  5. 6 6
      src/app/(public)/product/_components/ProductInformation.tsx
  6. 44 42
      src/app/(public)/product/_components/ProductMedia.tsx
  7. 41 6
      src/app/(public)/product/_components/ProductReviewSection.tsx
  8. 1 1
      src/app/(public)/product/_components/ProductShortDescription.tsx
  9. 144 0
      src/app/(public)/product/_components/ProductTwoColumnLayout.tsx
  10. 25 0
      src/app/(public)/product/_components/ProductdetailAndQuestion.tsx
  11. 1 1
      src/app/(public)/product/_components/ShopServicePanel.tsx
  12. 181 51
      src/app/(public)/product/_components/SwitchButton.tsx
  13. 1 1
      src/app/(public)/product/_components/popup/ShoppingSecurityModal.tsx
  14. 1 1
      src/app/(public)/product/_components/popup/VipPlusModal.tsx
  15. 130 122
      src/app/(public)/product/_components/review/AddProductReviewForm.tsx
  16. 2 2
      src/app/(public)/product/_components/review/HairPhotoGallery.tsx
  17. 6 6
      src/app/(public)/product/_components/review/ReviewAdd.tsx
  18. 9 6
      src/app/(public)/product/_components/review/ReviewDetail.tsx
  19. 101 95
      src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx
  20. 271 177
      src/app/(public)/product/_components/review/ReviewModal.tsx
  21. 47 24
      src/app/(public)/product/_components/youmaylike/Recommend.tsx
  22. 2 2
      src/components/common/button/ReviewButton.tsx
  23. 1 0
      src/types/products/review.ts

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

@@ -22,6 +22,8 @@ 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>

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

@@ -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;
@@ -110,13 +110,13 @@ export function ProductAddToCart({
   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>
   ) : (
     <></>

+ 41 - 6
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 } 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: ProductReviewList
+  ) => void;
 }
 
 function ProductReviewSection({
   productId,
   reviews,
-  modalOpen,
+  reviewTotal,
+  // modalOpen,
   closeModal,
   activeTab,
   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)}

Diferenças do arquivo suprimidas por serem muito extensas
+ 181 - 51
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>
   );
 }

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

@@ -22,8 +22,8 @@ export default function HairPhotoGallery({
           <Image
             src={src}
             alt={`hair ${idx}`}
-            width={87}
-            height={116}
+            width={144}
+            height={192}
             className="w-full h-full object-cover"
           />
           {idx == 3 ? (

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

@@ -3,14 +3,14 @@
 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 [qusopen, setQueopen] = useState(false);
-  const handleCloseForm = () => setOpen(false);
+  // const handleCloseForm = () => setOpen(false);
   const setShowForm = () => {
     setOpen(() => true);
     setQueopen(() => false);
@@ -18,7 +18,7 @@ export default function ReviewAdd({ productId }: { productId: string }) {
   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}

+ 9 - 6
src/app/(public)/product/_components/review/ReviewDetail.tsx

@@ -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,9 +36,10 @@ interface ReviewDetailProps {
 
 const ReviewDetail: FC<ReviewDetailProps> = ({
   reviewDetails,
+  reviewTotal,
   // totalReview,
   productId,
-  modalOpen,
+  // modalOpen,
   onClose,
   activeTab,
   onChangeTab,
@@ -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,24 +348,25 @@ 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}
+        // 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>
   );

+ 271 - 177
src/app/(public)/product/_components/review/ReviewModal.tsx

@@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect, useCallback } from "react";
 import type {
   ReviewTabType,
   ProductReviewList,
+  ProductReviewEdge,
   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: ProductReviewList
+  ) => void;
 }
 // 模拟后端请求函数,替换为你的真实接口
 async function fetchReviewApi(
@@ -27,93 +41,27 @@ async function fetchReviewApi(
   tab: ReviewTabType,
   pageNum: number,
   guestId: string | null,
+  signal?: AbortSignal
 ): Promise<{ list: ProductReviewList; total: number }> {
-  // 模拟网络延迟,模拟真实请求loading
-  // await new Promise((resolve) => setTimeout(resolve, 700));
-  //     return {
-  //     list: [],
-  //     total: 100,
-  //   };
-  // 全局固定100条模拟评论总数据
-  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  
   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,11 +70,16 @@ async function fetchReviewApi(
 
 const ReviewModal: React.FC<ReviewModalProps> = React.memo(
   function ReviewModal({
-    open,
+    // open,
+    reviewTotal,
     onClose,
     productId,
     onChangeTab,
     activeTab,
+    galleryOpen, 
+    galleryData, 
+    galleryClose,
+    onOpenGallery
   }) {
     // 全部原始评论数据(接口返回完整数据缓存)
     const [fullReviewList, setFullReviewList] = useState<ProductReviewList>([]);
@@ -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,124 @@ 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();
+            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(); 
+    
     // 生成图库一维图片数组
-    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);
+        let 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 +297,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 +340,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 +361,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 +416,8 @@ 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: ProductReviewEdge, reviewIdx) => {
+                  const item = edge.node;
                   const { liked, like_count } = getCurrentLike(item);
                   // const item = edge.node;
                   return (
@@ -508,9 +540,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 +560,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 +571,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>
       </>
     );
   },

Diferenças do arquivo suprimidas por serem muito extensas
+ 47 - 24
src/app/(public)/product/_components/youmaylike/Recommend.tsx


+ 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 - 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 {