Kaynağa Gözat

合并zzf-account

zgl 6 gün önce
ebeveyn
işleme
316d22e7d8
100 değiştirilmiş dosya ile 12572 ekleme ve 1423 silme
  1. BIN
      public/image/products_details/afterpay.png
  2. BIN
      public/image/products_details/guide.png
  3. BIN
      public/image/products_details/installment_description.png
  4. BIN
      public/image/products_details/klarna.png
  5. BIN
      public/image/products_details/paypal-expressm.png
  6. BIN
      public/image/products_details/question.png
  7. BIN
      public/image/products_details/reviews.png
  8. 48 0
      src/app/(public)/_components/CategoryTabSwiper .tsx
  9. 166 0
      src/app/(public)/_components/HomeBestSellers.tsx
  10. 156 0
      src/app/(public)/_components/HomeContent.tsx
  11. 69 0
      src/app/(public)/_components/HomeMiddleBanner.tsx
  12. 163 0
      src/app/(public)/_components/HomeNewArrival.tsx
  13. 145 0
      src/app/(public)/_components/JinGangSwiper.tsx
  14. 168 0
      src/app/(public)/_components/MiddleImageSwiper.tsx
  15. 268 0
      src/app/(public)/_components/RecommendedProducts.tsx
  16. 54 0
      src/app/(public)/_components/TopImageSwiper.tsx
  17. 324 0
      src/app/(public)/_components/Trending.tsx
  18. 5 0
      src/app/(public)/_components/css/swiper-custom.css
  19. 0 177
      src/app/(public)/category/[collection]/page.tsx
  20. 71 0
      src/app/(public)/category/[slug]/[id]/_components/CategoryDesc.tsx
  21. 129 0
      src/app/(public)/category/[slug]/[id]/_components/FaqList.tsx
  22. 212 0
      src/app/(public)/category/[slug]/[id]/_components/ProductListing.tsx
  23. 301 0
      src/app/(public)/category/[slug]/[id]/page.tsx
  24. 74 0
      src/app/(public)/customer/account/_components/AccountBottomHrefSwiper.tsx
  25. 158 0
      src/app/(public)/customer/account/_components/AccountHeader.tsx
  26. 341 0
      src/app/(public)/customer/account/_components/AccountVipPopup.tsx
  27. 112 0
      src/app/(public)/customer/account/_components/HelpModal.tsx
  28. 183 0
      src/app/(public)/customer/account/mypoints/_components/GetPoints.tsx
  29. 28 0
      src/app/(public)/customer/account/mypoints/_components/PointsRule.tsx
  30. 16 71
      src/app/(public)/customer/account/mypoints/page.tsx
  31. 14 129
      src/app/(public)/customer/account/page.tsx
  32. 104 221
      src/app/(public)/customer/account/pointlist/page.tsx
  33. 58 0
      src/app/(public)/customer/address/_components/DefaultAddress.tsx
  34. 306 0
      src/app/(public)/customer/address/eidt/[id]/page.tsx
  35. 99 0
      src/app/(public)/customer/address/new/_components/CustomerAddressPhone.tsx
  36. 221 0
      src/app/(public)/customer/address/new/_components/NewAddressDefault.tsx
  37. 1286 169
      src/app/(public)/customer/address/new/page.tsx
  38. 13 8
      src/app/(public)/customer/address/page.tsx
  39. 388 0
      src/app/(public)/customer/order/history/_components/OrderHistory.tsx
  40. 11 0
      src/app/(public)/customer/order/history/page.tsx
  41. 142 0
      src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx
  42. 59 0
      src/app/(public)/customer/order/track/order_id/[id]/_components/TrackModal.tsx
  43. 74 0
      src/app/(public)/customer/order/track/order_id/[id]/page.tsx
  44. 30 0
      src/app/(public)/customer/order/view/order_id/[id]/_components/OrderHeader.tsx
  45. 51 0
      src/app/(public)/customer/order/view/order_id/[id]/_components/OrderInfoBlock.tsx
  46. 71 0
      src/app/(public)/customer/order/view/order_id/[id]/_components/PriceSummary.tsx
  47. 29 0
      src/app/(public)/customer/order/view/order_id/[id]/_components/ProductList.tsx
  48. 57 0
      src/app/(public)/customer/order/view/order_id/[id]/page.tsx
  49. 207 0
      src/app/(public)/hair-care-tips/page.tsx
  50. 688 0
      src/app/(public)/help-faqs/_components/Faqs.tsx
  51. 10 0
      src/app/(public)/help-faqs/page.tsx
  52. 31 14
      src/app/(public)/page.tsx
  53. 80 58
      src/app/(public)/product/[...urlProduct]/page.tsx
  54. 63 0
      src/app/(public)/product/_components/NewUserBanner.tsx
  55. 181 24
      src/app/(public)/product/_components/ProductAddToCart.tsx
  56. 392 255
      src/app/(public)/product/_components/ProductInformation.tsx
  57. 108 53
      src/app/(public)/product/_components/ProductMedia.tsx
  58. 48 29
      src/app/(public)/product/_components/ProductReviewSection.tsx
  59. 1 1
      src/app/(public)/product/_components/ProductShortDescription.tsx
  60. 48 0
      src/app/(public)/product/_components/ProductText.tsx
  61. 158 0
      src/app/(public)/product/_components/PromotionDiscountCardProps .tsx
  62. 148 0
      src/app/(public)/product/_components/ShopServicePanel.tsx
  63. 170 0
      src/app/(public)/product/_components/SwitchButton.tsx
  64. 81 0
      src/app/(public)/product/_components/popup/Guide.tsx
  65. 81 0
      src/app/(public)/product/_components/popup/InstallmentPopup.tsx
  66. 102 0
      src/app/(public)/product/_components/popup/PriceModal.tsx
  67. 173 0
      src/app/(public)/product/_components/popup/ShoppingSecurityModal.tsx
  68. 71 0
      src/app/(public)/product/_components/popup/VipPlusModal.tsx
  69. 70 0
      src/app/(public)/product/_components/question/AskQuestionModal.tsx
  70. 157 0
      src/app/(public)/product/_components/question/ComponentsQuestionModal.tsx
  71. 166 0
      src/app/(public)/product/_components/question/FaqList.tsx
  72. 390 0
      src/app/(public)/product/_components/question/QAModal.tsx
  73. 98 58
      src/app/(public)/product/_components/review/AddProductReview.tsx
  74. 262 0
      src/app/(public)/product/_components/review/AddProductReviewForm.tsx
  75. 53 0
      src/app/(public)/product/_components/review/HairPhotoGallery.tsx
  76. 104 0
      src/app/(public)/product/_components/review/QuestionInputModal.tsx
  77. 42 18
      src/app/(public)/product/_components/review/ReviewAdd.tsx
  78. 156 62
      src/app/(public)/product/_components/review/ReviewDetail.tsx
  79. 97 0
      src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx
  80. 123 0
      src/app/(public)/product/_components/review/ReviewImageUploader.tsx
  81. 449 0
      src/app/(public)/product/_components/review/ReviewModal.tsx
  82. 275 0
      src/app/(public)/product/_components/youmaylike/Recommend.tsx
  83. 62 0
      src/app/(public)/search/_components/HotSearch.tsx
  84. 130 76
      src/app/(public)/search/page.tsx
  85. 76 0
      src/app/(public)/track_order/_components/TrackFaqs.tsx
  86. 32 0
      src/app/(public)/track_order/_components/TrackOrder.tsx
  87. 47 0
      src/app/(public)/track_order/page.tsx
  88. 40 0
      src/app/api/customer/growth-value/route.ts
  89. 46 0
      src/app/api/customer/orders/[id]/route.ts
  90. 61 0
      src/app/api/customer/orders/route.ts
  91. 59 0
      src/app/api/customer/orders/tracking/route.ts
  92. 50 0
      src/app/api/customer/reward-points/browse-product/route.ts
  93. 40 0
      src/app/api/customer/reward-points/follow-status/route.ts
  94. 62 0
      src/app/api/customer/reward-points/follow/route.ts
  95. 61 0
      src/app/api/customer/reward-points/history/route.ts
  96. 193 0
      src/app/api/customer/token/address/route.ts
  97. 45 0
      src/app/api/home/route.ts
  98. 40 0
      src/app/api/shop/customer-address-gets/route.ts
  99. 41 0
      src/app/api/shop/customer-addresses/route.ts
  100. 0 0
      src/app/api/shop/products/[productId]/reviews/route.ts

BIN
public/image/products_details/afterpay.png


BIN
public/image/products_details/guide.png


BIN
public/image/products_details/installment_description.png


BIN
public/image/products_details/klarna.png


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


BIN
public/image/products_details/question.png


BIN
public/image/products_details/reviews.png


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

@@ -0,0 +1,48 @@
+"use client";
+import React from "react";
+import Link from "next/link";
+import { Swiper, SwiperSlide } from "swiper/react";
+import { FreeMode } from "swiper/modules";
+import "swiper/css";
+
+// 标签数据,可自由增删修改文字、跳转路由
+const tabList = [
+  { label: "HD Lace Wigs", href: "/category/hd-lace-wigs" },
+  { label: "New Arrival", href: "/category/new-arrival" },
+  { label: "Wigs", href: "/category/wigs" },
+  { label: "Colored Wigs", href: "/category/colored-wigs" },
+  { label: "Body Wave", href: "/category/body-wave" },
+  { label: "Curly Hair", href: "/category/curly-hair" },
+];
+
+const CategoryTabSwiper = () => {
+  return (
+    <div className="py-4">
+      <Swiper
+        modules={[FreeMode]}
+        freeMode={true}
+        slidesOffsetBefore={16}
+        slidesOffsetAfter={0}
+        spaceBetween={8}
+        slidesPerView="auto"
+        pagination={false}
+        navigation={false}
+       
+        className="w-full overflow-hidden"
+      >
+        {tabList.map((tab, idx) => (
+          <SwiperSlide key={idx} className="!w-auto">
+            <Link
+              href={tab.href}
+              className="inline-block px-2 border-2 border-black leading-6.5 rounded-sm bg-[#ecfdf5] text-xs text-center font-normal "
+            >
+              {tab.label}
+            </Link>
+          </SwiperSlide>
+        ))}
+      </Swiper>
+    </div>
+  );
+};
+
+export default CategoryTabSwiper;

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

@@ -0,0 +1,166 @@
+import React from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+import { Navigation, Pagination } from "swiper/modules";
+import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+// 导入Swiper基础样式
+import "swiper/css";
+import "swiper/css/navigation";
+import { HomeProduct } from "@/types/home/type";
+import Link from "next/link";
+interface HomeBestSellersProps {
+  bestSellers: HomeProduct[];
+}
+const HomeBestSellers = ({ bestSellers }: HomeBestSellersProps) => {
+  if (!bestSellers || bestSellers.length === 0) return null;
+  // 4. 真实数据按每6个拆分一组,替换原来的模拟拆分逻辑
+  const chunkSize = 6;
+  const productBlocks: HomeProduct[][] = [];
+  for (let i = 0; i < bestSellers.length; i += chunkSize) {
+    productBlocks.push(bestSellers.slice(i, i + chunkSize));
+  }
+  return (
+    <div className="w-full max-w-[1200px] mx-auto px-4 py-8">
+      {/* 标题 */}
+      <h2 className="text-2xl font-bold mb-6">Best Sellers</h2>
+
+      {/* Swiper容器 */}
+      <Swiper
+        slidesPerView={1}
+        spaceBetween={20}
+        modules={[Navigation, Pagination]}
+        pagination={{
+          el: ".custom-pagination",
+          clickable: true,
+          renderBullet: (index, className) => {
+            // 所有分页默认底色为 #E5E5E5
+            return `<span class="${className} w-6 h-[5px] rounded-full mx-1 bg-[#E5E5E5]"></span>`;
+          },
+          // 激活时强制覆盖底色,加 !important 提高权重
+          bulletActiveClass: "!bg-[#333]",
+        }}
+        className="relative"
+      >
+        {/* 渲染2个滑动块 */}
+        {productBlocks.map((block, blockIndex) => (
+          <SwiperSlide key={blockIndex} className="!px-0">
+            {/* 每个块:6个产品,2列布局 */}
+            <div className="grid grid-cols-2 gap-4">
+              {block.map((product) => (
+                <div key={product.id} className="flex flex-col gap-2">
+                  {/* 产品图片 */}
+                  <img
+                    src={product.image_url}
+                    alt={product.name}
+                    className="w-full h-auto object-cover rounded-sm"
+                  />
+                  {/* 产品标题 */}
+                  <Link href={"/wiggins-ready-go-250-density-popping-curly-hd-lace-front-wigs-medium-length-human-hair-wigs.html"} className="text-sm text-gray-800 truncate">
+                    {product.name}
+                  </Link>
+                  {/* 评分 + 评论数 */}
+                  <div className="flex items-center gap-1">
+                    <div className="flex">
+                      {/* 星级图标(简化版,可替换为真实星星图标) */}
+                      {[1, 2, 3, 4, 5].map((star) => (
+                        <svg
+                          key={star}
+                          className="w-4 h-4 text-yellow-400"
+                          fill="currentColor"
+                          viewBox="0 0 20 20"
+                        >
+                          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
+                        </svg>
+                      ))}
+                    </div>
+                    <span className="text-xs text-gray-600">
+                      {product.review_count}
+                    </span>
+                  </div>
+                  {/* 价格 + 购物车图标 */}
+                  <div className="flex items-center justify-between">
+                    <div className="flex items-center gap-1">
+                      <span className="font-semibold">
+                        ${product.min_price}
+                      </span>
+                      <span className="text-xs text-gray-500 line-through">
+                        ${product.price}
+                      </span>
+                    </div>
+                    {/* 购物车图标 */}
+                    <OpenAddToCartModalButton
+                      productId={product.id}
+                      icon={
+                        <svg
+                          xmlns="http://www.w3.org/2000/svg"
+                          width="32"
+                          height="32"
+                          viewBox="0 0 32 32"
+                          fill="none"
+                        >
+                          <rect
+                            x="0"
+                            y="0"
+                            width="32"
+                            height="32"
+                            rx="8"
+                            fill="#036141"
+                          ></rect>
+                          <path
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                            d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
+                          ></path>
+                          <ellipse
+                            cx="11.066040515899658"
+                            cy="23.199928283691406"
+                            rx="1.4000248908996582"
+                            ry="1.4000015258789062"
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                          ></ellipse>
+                          <ellipse
+                            cx="23.1998291015625"
+                            cy="23.199928283691406"
+                            rx="1.4000244140625"
+                            ry="1.4000015258789062"
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                          ></ellipse>
+                          <path
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                            d="M15.2656 15.2665L18.999 15.2665"
+                          ></path>
+                          <path
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                            d="M17.1348 17.1332L17.1348 13.3999"
+                          ></path>
+                        </svg>
+                      }
+                    />
+                  </div>
+                </div>
+              ))}
+            </div>
+          </SwiperSlide>
+        ))}
+
+        {/* 自定义分页器容器(底部小按钮) */}
+        <div className="custom-pagination flex justify-center mt-4"></div>
+      </Swiper>
+    </div>
+  );
+};
+
+export default HomeBestSellers;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 156 - 0
src/app/(public)/_components/HomeContent.tsx


+ 69 - 0
src/app/(public)/_components/HomeMiddleBanner.tsx

@@ -0,0 +1,69 @@
+"use client";
+import { useState } from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+import { Navigation } from "swiper/modules";
+import "swiper/css";
+import "swiper/css/navigation";
+import Image from "next/image";
+import { HomeSectionImage } from "@/types/home/type";
+interface middleBannerProps {
+  middleBanner: HomeSectionImage[];
+}
+export default function HomeMiddleBanner({ middleBanner }: middleBannerProps) {
+  const [activeIndex, setActiveIndex] = useState(0);
+
+  const [swiperRef] = useState(null);
+  if (!middleBanner || middleBanner.length === 0) return null;
+  const middleBannerImages = middleBanner;
+
+  const handleSlideChange = (swiper: any) => {
+    setActiveIndex(swiper.activeIndex);
+  };
+
+  const handleIndicatorClick = (index: any) => {
+    if (swiperRef) {
+      setActiveIndex(index);
+    }
+  };
+
+  return (
+    <div className="max-w-screen-md mx-auto p-4">
+      <Swiper
+        modules={[Navigation]}
+        spaceBetween={10}
+        slidesPerView={1}
+        onSlideChange={handleSlideChange}
+        className="mb-6"
+      >
+        {middleBannerImages.map((item, idx) => (
+          <SwiperSlide key={idx}>
+            <div className="relative w-full h-42">
+              <Image
+                src={item.image}
+                alt={item.title}
+                fill
+                className="w-full h-auto object-cover rounded-md"
+              />
+            </div>
+          </SwiperSlide>
+        ))}
+      </Swiper>
+      <div className="flex justify-center gap-2">
+        <button
+          onClick={() => handleIndicatorClick(0)}
+          className={`w-6 h-[5px] rounded-full  opacity-100 ${
+            activeIndex === 0 ? "bg-[#333]" : "bg-[#E5E5E5]"
+          }`}
+          aria-label="切换到第一块"
+        />
+        <button
+          onClick={() => handleIndicatorClick(1)}
+          className={`w-6 h-[5px] rounded-full  opacity-100 ${
+            activeIndex === 1 ? "bg-[#333]" : "bg-[#E5E5E5]"
+          }`}
+          aria-label="切换到第二块"
+        />
+      </div>
+    </div>
+  );
+}

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

@@ -0,0 +1,163 @@
+import React from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+import { Navigation, Pagination } from "swiper/modules";
+import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+// 导入Swiper基础样式
+import "swiper/css";
+import "swiper/css/navigation";
+import {HomeProduct} from "@/types/home/type"
+interface NewArrivalProps {
+  newArrival: HomeProduct[];
+}
+const HomeNewArrival = ({ newArrival }: NewArrivalProps) => {
+     if (!newArrival || newArrival.length === 0) return null;
+        // 4. 真实数据按每6个拆分一组,替换原来的模拟拆分逻辑
+    const chunkSize = 6;
+    const productBlocks:  HomeProduct[][] = [];
+    for (let i = 0; i < newArrival.length; i += chunkSize) {
+      productBlocks.push(newArrival.slice(i, i + chunkSize));
+    }
+  return (
+    <div className="w-full max-w-[1200px] mx-auto px-4 py-8">
+      {/* 标题 */}
+      <h2 className="text-2xl font-bold mb-6">New Arrival</h2>
+
+      {/* Swiper容器 */}
+      <Swiper
+        slidesPerView={1}
+        spaceBetween={20}
+        modules={[Navigation, Pagination]}
+        pagination={{
+          el: ".custom-pagination",
+          clickable: true,
+          renderBullet: (index, className) => {
+            // 所有分页默认底色为 #E5E5E5
+            return `<span class="${className} w-6 h-[5px] rounded-full mx-1 bg-[#E5E5E5]"></span>`;
+          },
+          // 激活时强制覆盖底色,加 !important 提高权重
+          bulletActiveClass: "!bg-[#333]",
+        }}
+        className="relative"
+      >
+        {/* 渲染2个滑动块 */}
+        {productBlocks.map((block, blockIndex) => (
+          <SwiperSlide key={blockIndex} className="!px-0">
+            {/* 每个块:6个产品,2列布局 */}
+            <div className="grid grid-cols-2 gap-4">
+              {block.map((product) => (
+                <div key={product.id} className="flex flex-col gap-2">
+                  {/* 产品图片 */}
+                  <img
+                    src={product.image_url}
+                    alt={product.name}
+                    className="w-full h-auto object-cover rounded-sm"
+                  />
+                  {/* 产品标题 */}
+                  <p className="text-sm text-gray-800 truncate">
+                    {product.name}
+                  </p>
+                  {/* 评分 + 评论数 */}
+                  <div className="flex items-center gap-1">
+                    <div className="flex">
+                      {/* 星级图标(简化版,可替换为真实星星图标) */}
+                      {[1, 2, 3, 4, 5].map((star) => (
+                        <svg
+                          key={star}
+                          className="w-4 h-4 text-yellow-400"
+                          fill="currentColor"
+                          viewBox="0 0 20 20"
+                        >
+                          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
+                        </svg>
+                      ))}
+                    </div>
+                    <span className="text-xs text-gray-600">
+                      {product.review_count}
+                    </span>
+                  </div>
+                  {/* 价格 + 购物车图标 */}
+                  <div className="flex items-center justify-between">
+                    <div className="flex items-center gap-1">
+                      <span className="font-semibold">${product.min_price}</span>
+                      <span className="text-xs text-gray-500 line-through">
+                        ${product.price}
+                      </span>
+                    </div>
+                    {/* 购物车图标 */}
+                    <OpenAddToCartModalButton
+                        productId={product.id}
+                      icon={
+                        <svg
+                          xmlns="http://www.w3.org/2000/svg"
+                          width="32"
+                          height="32"
+                          viewBox="0 0 32 32"
+                          fill="none"
+                        >
+                          <rect
+                            x="0"
+                            y="0"
+                            width="32"
+                            height="32"
+                            rx="8"
+                            fill="#036141"
+                          ></rect>
+                          <path
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                            d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
+                          ></path>
+                          <ellipse
+                            cx="11.066040515899658"
+                            cy="23.199928283691406"
+                            rx="1.4000248908996582"
+                            ry="1.4000015258789062"
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                          ></ellipse>
+                          <ellipse
+                            cx="23.1998291015625"
+                            cy="23.199928283691406"
+                            rx="1.4000244140625"
+                            ry="1.4000015258789062"
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                          ></ellipse>
+                          <path
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                            d="M15.2656 15.2665L18.999 15.2665"
+                          ></path>
+                          <path
+                            stroke="rgba(255, 255, 255, 1)"
+                            stroke-width="1.3333333333333333"
+                            stroke-linejoin="round"
+                            stroke-linecap="round"
+                            d="M17.1348 17.1332L17.1348 13.3999"
+                          ></path>
+                        </svg>
+                      }
+                    />
+                  </div>
+                </div>
+              ))}
+            </div>
+          </SwiperSlide>
+        ))}
+
+        {/* 自定义分页器容器(底部小按钮) */}
+        <div className="custom-pagination flex justify-center mt-4"></div>
+      </Swiper>
+    </div>
+  );
+};
+
+export default HomeNewArrival;

+ 145 - 0
src/app/(public)/_components/JinGangSwiper.tsx

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

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

@@ -0,0 +1,168 @@
+import React from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+import {  Pagination  } from "swiper/modules";
+import "swiper/css";
+import "swiper/css/pagination";
+import "swiper/css/navigation";
+import "./css/swiper-custom.css";
+import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+import {HomeProduct,HomeSectionImage} from "@/types/home/type"
+interface zeroWearProps {
+  zeroWear: HomeProduct[];
+  zeroWearImage:HomeSectionImage[]
+}
+// const productList = [
+//   {
+//     id: 1,
+//     img: "https://picsum.photos/400/500?random=1",
+//     title: "14-36 In Water Wave Hum...",
+//     price: 143.35,
+//   },
+//   {
+//     id: 2,
+//     img: "https://picsum.photos/400/500?random=2",
+//     title: "14-36 In Water Wave Hum.",
+//     price: 143.35,
+//   },
+//   {
+//     id: 3,
+//     img: "https://picsum.photos/400/500?random=3",
+//     title: "14-36 In Water Wave Hum...",
+//     price: 143.35,
+//   },
+//   {
+//     id: 4,
+//     img: "https://picsum.photos/400/500?random=4",
+//     title: "14-36 In Water Wave Hum.",
+//     price: 143.35,
+//   },
+// ];
+
+const MiddleImageSwiper = ({ zeroWear,zeroWearImage }: zeroWearProps) => {
+  if (!zeroWear || zeroWear.length === 0) return null;
+  const  productList = zeroWear
+  const WearImage = zeroWearImage;
+  return (
+    <div className="w-full pr-4 pl-4 mx-auto  rounded-lg  ">
+      <div className="w-full">
+        <img
+          src={WearImage[0].image || 'https://cdn.asteriahair.com/uploads/202602/11/20260211095330_7ec9031a4ab36e44.gif'}
+          alt={WearImage[0].title || 'ZEROWEAR hair banner'}
+          className="w-full h-auto object-cover block"
+        />
+      </div>
+      <div className="py-4 px-4 pb-6 relative min-h-83 bg-[#f0f5f3] pb-10">
+        <div className="w-full relative">
+          <Swiper
+            modules={[Pagination]}
+            spaceBetween={16}
+            slidesPerView={2}
+            pagination={{
+              type: "progressbar",
+              el: ".my-progress-bar",
+            }}
+            className="mySwiper"
+            breakpoints={{
+              480: { slidesPerView: 1 },
+              768: { slidesPerView: 2 },
+            }}
+          >
+            {productList.map((item) => (
+              <SwiperSlide key={item.id}>
+                <div className="bg-white rounded overflow-hidden  flex flex-col">
+                  <img
+                    src={item.image_url}
+                    alt={item.name}
+                    className="w-full aspect-[3/4] object-cover"
+                  />
+
+                  <div className="p-2 flex flex-col gap-2">
+                    <p className="text-sm text-gray-800 truncate">
+                      {item.name}
+                    </p>
+                    {/* 价格 + 购物车图标 */}
+                    <div className="flex items-center justify-between">
+                      <div className="flex items-center gap-1">
+                        <span className="font-semibold">${item.min_price}</span>
+                        <span className="text-xs text-gray-500 line-through">
+                          ${item.price}
+                        </span>
+                      </div>
+                      {/* 购物车图标 */}
+                      <OpenAddToCartModalButton
+                         productId={item.id}
+                        icon={
+                          <svg
+                            xmlns="http://www.w3.org/2000/svg"
+                            width="32"
+                            height="32"
+                            viewBox="0 0 32 32"
+                            fill="none"
+                          >
+                            <rect
+                              x="0"
+                              y="0"
+                              width="32"
+                              height="32"
+                              rx="8"
+                              fill="#036141"
+                            ></rect>
+                            <path
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                              d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
+                            ></path>
+                            <ellipse
+                              cx="11.066040515899658"
+                              cy="23.199928283691406"
+                              rx="1.4000248908996582"
+                              ry="1.4000015258789062"
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                            ></ellipse>
+                            <ellipse
+                              cx="23.1998291015625"
+                              cy="23.199928283691406"
+                              rx="1.4000244140625"
+                              ry="1.4000015258789062"
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                            ></ellipse>
+                            <path
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                              d="M15.2656 15.2665L18.999 15.2665"
+                            ></path>
+                            <path
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                              d="M17.1348 17.1332L17.1348 13.3999"
+                            ></path>
+                          </svg>
+                        }
+                      />
+                    </div>
+                
+                  </div>
+                </div>
+              </SwiperSlide>
+            ))}
+          </Swiper>
+          <div className="my-progress-bar mt-5 w-full h-1 bg-gray-200 rounded-full bottom-0 [top:unset]"></div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default MiddleImageSwiper;

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

@@ -0,0 +1,268 @@
+"use client";
+import React, { useState, useEffect, useCallback } from "react";
+import Image from "next/image";
+import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+import {HomeProduct} from "@/types/home/type"
+interface recommendedProps {
+  recommended: HomeProduct[];
+}
+// 商品类型定义
+interface ProductItem {
+  id: number;
+  img: string;
+  title: string;
+  price: number;
+  originPrice: number;
+  star: number;
+  reviewCount: number;
+}
+
+// ========== 模拟后端分页接口(真实场景替换为axios/fetch请求) ==========
+/**
+ * @param page 当前页码
+ * @param pageSize 每页条数
+ * @returns Promise<{ list: ProductItem[], total: number }>
+ */
+const mockGetProductApi = async (page: number, pageSize: number) => {
+  // 模拟总数据22条,后端固定总数
+  const TOTAL = 22;
+  // 模拟全部源数据
+  const allMockData: ProductItem[] = Array.from({ length: TOTAL }).map(
+    (_, idx) => ({
+      id: idx + 1,
+      img: `/img/hair${(idx % 6) + 1}.jpg`,
+      title: "14-36 In Water Wave Human Hair Wig",
+      price: 143.35,
+      originPrice: 143.35,
+      star: 5,
+      reviewCount: 342,
+    }),
+  );
+
+  // 分页切片
+  const start = (page - 1) * pageSize;
+  const end = page * pageSize;
+  const pageList = allMockData.slice(start, end);
+
+  // 模拟网络延迟
+  await new Promise((resolve) => setTimeout(resolve, 600));
+  return {
+    list: pageList,
+    total: TOTAL,
+  };
+};
+
+const PAGE_SIZE = 6;
+
+export default function RecommendedProducts({ recommended }: recommendedProps) {
+  
+  // 已渲染的全部商品
+  const [productList, _setProductList] = useState<HomeProduct[]>(recommended);
+  // 当前页码
+  const [page, setPage] = useState(1);
+  // 数据总条数
+  const [total, setTotal] = useState(0);
+  // 加载状态
+  const [loading, setLoading] = useState(false);
+
+  // 是否还有下一页:已渲染数量 < 总条数
+  const hasMore = productList.length < total;
+
+  // 请求分页数据
+  const fetchPageData = useCallback(async (currentPage: number) => {
+    setLoading(true);
+    try {
+      const res = await mockGetProductApi(currentPage, PAGE_SIZE);
+      setTotal(res.total);
+      // 函数式更新,稳定依赖
+      // setProductList((prev) =>
+      //   currentPage === 1 ? res.list : [...prev, ...res.list],
+      // );
+    } catch (err) {
+      // 屏蔽 no-console 警告
+      // eslint-disable-next-line no-console
+      console.error("商品分页加载失败", err);
+    } finally {
+      setLoading(false);
+    }
+  }, []);
+
+  // 页面初始化挂载只加载一次第一页
+  useEffect(() => {
+    // 封装内部异步函数,规范写法
+    const initLoad = async () => {
+      await fetchPageData(1);
+    };
+    initLoad();
+  }, [fetchPageData]); // 补齐依赖 fetchPageData
+  // 点击View More 加载下一页
+  const handleViewMore = () => {
+    const nextPage = page + 1;
+    setPage(nextPage);
+    fetchPageData(nextPage);
+  };
+  if (!recommended || recommended.length === 0) return null;
+  // 五星星星渲染
+  const renderStars = (count: number) => {
+    return Array.from({ length: count }).map((_, idx) => (
+      <svg
+        key={idx}
+        className="w-4 h-4 text-green-600 inline-block"
+        viewBox="0 0 24 24"
+        fill="currentColor"
+      >
+        <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>
+    ));
+  };
+
+  return (
+    <section className="py-6 px-4 pt-8">
+      <h2 className="text-[clamp(1.5rem,4vw,2.2rem)] font-semibold mb-6">
+        Recommended For You
+      </h2>
+
+      {/* 商品2列网格 */}
+      <div className="grid grid-cols-2 gap-4">
+        {productList.map((item) => (
+          <div key={item.id} className="flex flex-col gap-3">
+            {/* Next.Image 商品图容器 */}
+            <div className="relative w-full aspect-[3/4] overflow-hidden rounded-md">
+              <Image
+                src={item.image_url}
+                alt={item.name}
+                fill
+                sizes="(max-width:768px) 50vw, 33vw"
+                className="object-cover"
+              />
+            </div>
+
+            {/* 标题 */}
+            <p className="text-sm text-gray-800 truncate">{item.name}</p>
+
+            {/* 星级 + 评论数 */}
+            <div className="flex items-center gap-1">
+              {renderStars(item.rating)}
+              <span className="text-sm text-gray-600 ml-1 underline">
+                {item.review_count}
+              </span>
+            </div>
+
+            {/* 价格 + 购物车按钮 */}
+            {/* <div className="flex items-center justify-between">
+              <div className="flex items-center gap-2">
+                <span className="text-xl font-bold text-black">
+                  ${item.price}
+                </span>
+                <span className="text-sm text-gray-400 line-through">
+                  ${item.originPrice}
+                </span>
+              </div>
+              <button className="w-10 h-10 bg-emerald-600 rounded-lg flex items-center justify-center text-white">
+                <svg
+                  width="18"
+                  height="18"
+                  viewBox="0 0 24 24"
+                  fill="none"
+                  stroke="currentColor"
+                  strokeWidth="2"
+                >
+                  <path d="M6 3h12M3 6h18l-2 11H5L3 6z" />
+                  <circle cx="9" cy="20" r="1" />
+                  <circle cx="15" cy="20" r="1" />
+                </svg>
+              </button>
+            </div> */}
+            {/* 价格 + 购物车图标 */}
+            <div className="flex items-center justify-between">
+              <div className="flex items-center gap-1">
+                <span className="font-semibold">${item.min_price}</span>
+                <span className="text-xs text-gray-500 line-through">
+                  ${item.price}
+                </span>
+              </div>
+              {/* 购物车图标 */}
+              <OpenAddToCartModalButton
+                productId={
+                  item.id
+                }
+                icon={
+                  <svg
+                    xmlns="http://www.w3.org/2000/svg"
+                    width="32"
+                    height="32"
+                    viewBox="0 0 32 32"
+                    fill="none"
+                  >
+                    <rect
+                      x="0"
+                      y="0"
+                      width="32"
+                      height="32"
+                      rx="8"
+                      fill="#036141"
+                    ></rect>
+                    <path
+                      stroke="rgba(255, 255, 255, 1)"
+                      stroke-width="1.3333333333333333"
+                      stroke-linejoin="round"
+                      stroke-linecap="round"
+                      d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
+                    ></path>
+                    <ellipse
+                      cx="11.066040515899658"
+                      cy="23.199928283691406"
+                      rx="1.4000248908996582"
+                      ry="1.4000015258789062"
+                      stroke="rgba(255, 255, 255, 1)"
+                      stroke-width="1.3333333333333333"
+                      stroke-linejoin="round"
+                      stroke-linecap="round"
+                    ></ellipse>
+                    <ellipse
+                      cx="23.1998291015625"
+                      cy="23.199928283691406"
+                      rx="1.4000244140625"
+                      ry="1.4000015258789062"
+                      stroke="rgba(255, 255, 255, 1)"
+                      stroke-width="1.3333333333333333"
+                      stroke-linejoin="round"
+                      stroke-linecap="round"
+                    ></ellipse>
+                    <path
+                      stroke="rgba(255, 255, 255, 1)"
+                      stroke-width="1.3333333333333333"
+                      stroke-linejoin="round"
+                      stroke-linecap="round"
+                      d="M15.2656 15.2665L18.999 15.2665"
+                    ></path>
+                    <path
+                      stroke="rgba(255, 255, 255, 1)"
+                      stroke-width="1.3333333333333333"
+                      stroke-linejoin="round"
+                      stroke-linecap="round"
+                      d="M17.1348 17.1332L17.1348 13.3999"
+                    ></path>
+                  </svg>
+                }
+              />
+            </div>
+          </div>
+        ))}
+      </div>
+
+      {/* View More 按钮:有下一页且非加载中显示 */}
+      {hasMore && (
+        <div className="flex justify-center mt-8">
+          <button
+            onClick={handleViewMore}
+            disabled={loading}
+            className="px-8 py-2 border border-black rounded-full text-base hover:bg-gray-100 transition disabled:opacity-50 disabled:cursor-not-allowed"
+          >
+            {loading ? "Loading..." : "View More"}
+          </button>
+        </div>
+      )}
+    </section>
+  );
+}

+ 54 - 0
src/app/(public)/_components/TopImageSwiper.tsx

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

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

@@ -0,0 +1,324 @@
+import React, { useState, useRef, useEffect } from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+import { Scrollbar, Navigation } from "swiper/modules";
+import "swiper/css";
+import "swiper/css/scrollbar";
+import "swiper/css/navigation";
+import Image from "next/image";
+import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
+import {HomeProduct} from "@/types/home/type"
+interface trendingProps {
+  trending: HomeProduct[];
+}
+// // 商品数据,每个卡片携带videoUrl视频地址
+// type ProductItem = {
+//   id: number;
+//   img: string;
+//   title: string;
+//   price: number;
+//   videoUrl: string;
+// };
+
+// const productList: ProductItem[] = [
+//   {
+//     id: 1,
+//     img: "/img/hair-video-1.jpg",
+//     title: "14-36 In Water Wave Human Hair HD Inv...",
+//     price: 146.1,
+//     videoUrl: "/video/hair-demo-1.mp4",
+//   },
+//   {
+//     id: 2,
+//     img: "/img/hair-video-2.jpg",
+//     title: "14-36 In Water Wave Human Hair HD Inv...",
+//     price: 146.1,
+//     videoUrl: "/video/hair-demo-2.mp4",
+//   },
+//   {
+//     id: 3,
+//     img: "/img/hair-video-3.jpg",
+//     title: "14-36 In Water Wave Human Hair HD Inv...",
+//     price: 146.1,
+//     videoUrl: "/video/hair-demo-3.mp4",
+//   },
+//   {
+//     id: 4,
+//     img: "/img/hair-video-4.jpg",
+//     title: "14-36 In Water Wave Human Hair HD Inv...",
+//     price: 146.1,
+//     videoUrl: "/video/hair-demo-4.mp4",
+//   },
+// ];
+
+const Trending = ({ trending }: trendingProps) => {
+   // 视频弹窗状态
+  const [openVideoModal, setOpenVideoModal] = useState(false);
+  const [currentVideoSrc, setCurrentVideoSrc] = useState("");
+   const videoRef = useRef<HTMLVideoElement>(null);
+   // 弹窗打开自动播放
+  useEffect(() => {
+    if (openVideoModal && videoRef.current) {
+      videoRef.current.play().catch((err) => console.log("播放拦截", err));
+    }
+  }, [openVideoModal]);
+  
+  if (!trending || trending.length === 0) return null;
+  const  productList = trending
+ 
+ 
+
+  // 打开弹窗并赋值视频地址
+  const handlePlayClick = (videoUrl: string) => {
+    setCurrentVideoSrc(videoUrl);
+    setOpenVideoModal(true);
+  };
+
+  // 关闭弹窗,暂停视频
+  const closeModal = () => {
+    setOpenVideoModal(false);
+    if (videoRef.current) {
+      videoRef.current.pause();
+      videoRef.current.currentTime = 0;
+    }
+  };
+
+ 
+
+  return (
+    <div className="w-full max-w-[960px] mx-auto">
+      <div className="w-full max-w-[960px] mx-auto px-4 py-6">
+        {/* 顶部标题 */}
+        <h2 className="text-[clamp(1.5rem,4vw,2.2rem)] font-bold mb-6 text-black">
+          Real looks,Trending Now
+        </h2>
+
+        {/* Swiper外层容器:进度条深层Tailwind样式 */}
+        <div
+          className="
+          [&_.custom-scrollbar]:h-[3px]
+          [&_.custom-scrollbar]:bg-gray-200
+          [&_.custom-scrollbar]:rounded-full
+          [&_.swiper-scrollbar-drag]:h-full
+          [&_.swiper-scrollbar-drag]:bg-black
+          [&_.swiper-scrollbar-drag]:rounded-full
+          [&_.swiper-scrollbar-drag]:shadow-none
+          [&_.swiper-scrollbar]:bg-transparent
+          /* 自定义左右箭头样式 匹配截图灰色圆形按钮 */
+          [&_.swiper-button-next]:w-11
+          [&_.swiper-button-next]:h-11
+          [&_.swiper-button-next]:rounded-full
+          [&_.swiper-button-next]:bg-black/40
+          [&_.swiper-button-next]:text-white
+          [&_.swiper-button-next]:after:text-base
+          [&_.swiper-button-next]:after:font-bold
+          [&_.swiper-button-prev]:w-11
+          [&_.swiper-button-prev]:h-11
+          [&_.swiper-button-prev]:rounded-full
+          [&_.swiper-button-prev]:bg-black/40
+          [&_.swiper-button-prev]:text-white
+          [&_.swiper-button-prev]:after:text-base
+          [&_.swiper-button-prev]:after:font-bold
+          [&_.swiper-button-next]:right-3
+          [&_.swiper-button-prev]:left-3
+        "
+        >
+          <Swiper
+            modules={[Scrollbar, Navigation]}
+            spaceBetween={16}
+            slidesPerView={1.2} // 露出右侧下一张,和截图一致
+            navigation={{
+              prevEl: ".btn-prev", // 左箭头class
+              nextEl: ".btn-next", // 右箭头class
+              disabledClass: "opacity-20", // 滑到首尾自动加这个类,Tailwind直接用
+            }}
+            breakpoints={{
+              640: { slidesPerView: 2.2 },
+              1024: { slidesPerView: 2.5 },
+            }}
+          >
+            {productList.map((item) => (
+              <SwiperSlide key={item.id} className="!h-auto">
+                {/* 单个商品卡片 */}
+                <div className=" overflow-hidden  flex flex-col">
+                  {/* 图片区域 + 中间播放按钮 */}
+                  <div className="relative rounded-xl w-full aspect-[4/5] overflow-hidden rounded-xl">
+                    <img
+                      src={item.image_url}
+                      alt={item.name}
+                      className="w-full h-full object-cover"
+                    />
+                    {/* 居中圆形播放按钮 */}
+                    <button
+                      onClick={() => handlePlayClick(item.videoUrl||"/video/hair-demo-1.mp4" )}
+                      className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
+                      w-24 h-24 rounded-full bg-white/60 backdrop-blur-sm
+                      flex items-center justify-center transition hover:bg-white/80"
+                    >
+                      <svg
+                        width="36"
+                        height="36"
+                        viewBox="0 0 24 24"
+                        fill="none"
+                        stroke="#222"
+                        strokeWidth="2"
+                      >
+                        <polygon points="5 3 19 12 5 21 5 3" />
+                      </svg>
+                    </button>
+                  </div>
+
+                  {/* 底部文字价格区域 */}
+                  <div className="p-3 flex flex-col rounded-xl mt-3 bg-[#f0f8f5] gap-3">
+                    <p className="text-base text-gray-900 truncate">
+                      {item.name}
+                    </p>
+                    {/* 价格 + 购物车图标 */}
+                    <div className="flex items-center justify-between">
+                      <div className="flex items-center gap-1">
+                        <span className="font-semibold">${item.min_price}</span>
+                        <span className="text-xs text-gray-500 line-through">
+                          ${item.price}
+                        </span>
+                      </div>
+                      {/* 购物车图标 */}
+                      <OpenAddToCartModalButton
+                         productId={item.id}
+                        icon={
+                          <svg
+                            xmlns="http://www.w3.org/2000/svg"
+                            width="32"
+                            height="32"
+                            viewBox="0 0 32 32"
+                            fill="none"
+                          >
+                            <rect
+                              x="0"
+                              y="0"
+                              width="32"
+                              height="32"
+                              rx="8"
+                              fill="#036141"
+                            ></rect>
+                            <path
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                              d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
+                            ></path>
+                            <ellipse
+                              cx="11.066040515899658"
+                              cy="23.199928283691406"
+                              rx="1.4000248908996582"
+                              ry="1.4000015258789062"
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                            ></ellipse>
+                            <ellipse
+                              cx="23.1998291015625"
+                              cy="23.199928283691406"
+                              rx="1.4000244140625"
+                              ry="1.4000015258789062"
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                            ></ellipse>
+                            <path
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                              d="M15.2656 15.2665L18.999 15.2665"
+                            ></path>
+                            <path
+                              stroke="rgba(255, 255, 255, 1)"
+                              stroke-width="1.3333333333333333"
+                              stroke-linejoin="round"
+                              stroke-linecap="round"
+                              d="M17.1348 17.1332L17.1348 13.3999"
+                            ></path>
+                          </svg>
+                        }
+                      />
+                    </div>
+                  </div>
+                </div>
+              </SwiperSlide>
+            ))}
+            {/* 自定义左右按钮,放在Swiper内部,同级slide */}
+            <button className="btn-prev absolute left-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
+              {/* 自定义左箭头SVG */}
+              <svg
+                width="16"
+                height="16"
+                viewBox="0 0 24 24"
+                fill="none"
+                stroke="#fff"
+                stroke-width="2"
+              >
+                <path d="M15 18l-6-6 6-6" />
+              </svg>
+            </button>
+            <button className="btn-next absolute right-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
+              {/* 自定义右箭头SVG */}
+              <svg
+                width="16"
+                height="16"
+                viewBox="0 0 24 24"
+                fill="none"
+                stroke="#fff"
+                stroke-width="2"
+              >
+                <path d="M9 18l6-6-6-6" />
+              </svg>
+            </button>
+          </Swiper>
+        </div>
+
+        {/* ====================== 全屏视频遮罩弹窗 ====================== */}
+        {openVideoModal && (
+          <div
+            className="fixed inset-0 z-[9999] bg-black/95 flex items-center justify-center p-4"
+            onClick={closeModal} // 点击遮罩空白关闭
+          >
+            {/* 弹窗内层,阻止冒泡 */}
+            <div
+              className="relative w-full max-w-4xl"
+              onClick={(e) => e.stopPropagation()}
+            >
+              {/* 右上角关闭按钮 */}
+              <button
+                onClick={closeModal}
+                className="absolute -top-12 right-0 text-white text-3xl hover:text-gray-300"
+              >
+                ✕
+              </button>
+              {/* 视频播放器 */}
+              <video
+                ref={videoRef}
+                src={currentVideoSrc}
+                controls
+                className="w-full rounded-lg"
+              />
+            </div>
+          </div>
+        )}
+      </div>
+      <div className="relative w-full aspect-[3/4] mt-8">
+        <Image
+          src={
+            "https://cdn.asteriahair.com/uploads/202607/01/20260701165110_3fff435adf276b89.jpg"
+          }
+          alt={"cscscs"}
+          fill
+          className="object-cover rounded-md"
+        />
+      </div>
+    </div>
+  );
+};
+
+export default Trending;

+ 5 - 0
src/app/(public)/_components/css/swiper-custom.css

@@ -0,0 +1,5 @@
+/* swiper-custom.css */
+.swiper-horizontal > .swiper-pagination-progressbar, .swiper-pagination-progressbar.swiper-pagination-horizontal{
+    bottom: -16px ;
+    top: unset;
+}

+ 0 - 177
src/app/(public)/category/[collection]/page.tsx

@@ -1,177 +0,0 @@
-import { Metadata } from "next";
-import { notFound } from "next/navigation";
-import { isArray } from "@/utils/type-guards";
-import Grid from "@components/theme/ui/grid/Grid";
-import FilterList from "@components/theme/filters/FilterList";
-import SortOrder from "@components/theme/filters/SortOrder";
-import MobileFilter from "@components/theme/filters/MobileFilter";
-import ProductGridItems from "@components/catalog/product/ProductGridItems";
-import Pagination from "@components/catalog/Pagination";
-import {
-  ProductsResponse,
-} from "@components/catalog/type";
-import {
-  GET_FILTER_PRODUCTS,
-  GET_TREE_CATEGORIES,
-} from "@/graphql";
-import { cachedGraphQLRequest, cachedCategoryRequest, getFilterAttributes } from "@/utils/hooks/useCache";
-import { SortByFields } from "@utils/constants";
-import { CategoryDetail } from "@components/theme/search/CategoryDetail";
-import { Suspense } from "react";
-import FilterListSkeleton from "@components/common/skeleton/FilterSkeleton";
-import { TreeCategoriesResponse } from "@/types/theme/category-tree";
-import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
-import { extractNumericId, findCategoryBySlug, buildProductFilters } from "@utils/helper";
-
-/**列表页 */
-export async function generateMetadata({
-  params,
-}: {
-  params: Promise<{ collection: string }>;
-}): Promise<Metadata> {
-  const { collection: categorySlug } = await params;
-
-  const {data:treeData} = await cachedGraphQLRequest<TreeCategoriesResponse>(
-    "category",
-    GET_TREE_CATEGORIES,
-    { parentId: 1 }
-  );
-
-  const categories = treeData?.treeCategories || [];
-  const categoryItem = findCategoryBySlug(categories, categorySlug);
-
-  if (!categoryItem) return notFound();
-
-  const translation = categoryItem.translation;
-
-  return {
-    title: translation?.metaTitle || translation?.name,
-    description: translation?.description || `${translation?.name} products`,
-  };
-}
-
-export default async function CategoryPage({
-  searchParams,
-  params,
-}: {
-  params: Promise<{ collection: string }>;
-  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
-}) {
-  const { collection: categorySlug } = await params;
-  const resolvedParams = await searchParams;
-
-  const [{data:treeData}, filterAttributes] = await Promise.all([
-    cachedGraphQLRequest<TreeCategoriesResponse>(
-      "category",
-      GET_TREE_CATEGORIES,
-      { parentId: 1 }
-    ),
-    getFilterAttributes(),
-  ]);
-
-  const categories = treeData?.treeCategories || [];
-  const categoryItem = findCategoryBySlug(categories, categorySlug);
-
-  if (!categoryItem) return notFound();
-
-  const numericId = extractNumericId(categoryItem.id);
-
-  const {
-    q: searchValue,
-    page,
-    cursor,
-    before,
-  } = (resolvedParams || {}) as {
-    [key: string]: string;
-  };
-
-  const itemsPerPage = 12;
-  const currentPage = page ? parseInt(page) - 1 : 0;
-  const sortValue = resolvedParams?.sort || "name-asc";
-  const selectedSort =
-    SortByFields.find((s) => s.key === sortValue) || SortByFields[0];
-
-  const { filterObject: baseFilterObject } = buildProductFilters(resolvedParams || {});
-
-  const filterObject: Record<string, string> = {
-    ...baseFilterObject,
-  };
-
-  if (numericId) {
-    filterObject.category_id = numericId;
-  }
-
-  const filterInput = JSON.stringify(filterObject);
-  
-  const [{data}] = await Promise.all([
-    cachedCategoryRequest<ProductsResponse>(
-      categorySlug,
-      GET_FILTER_PRODUCTS,
-      {
-        query: searchValue || "",
-        filter: filterInput,
-        ...(before
-          ? { last: itemsPerPage, before: before }
-          : { first: itemsPerPage, after: cursor }),
-        sortKey: selectedSort.sortKey,
-        reverse: selectedSort.reverse,
-      }
-    ),
-  ]);
-
-  const products = data?.products?.edges?.map((e) => e.node) || [];
-  const pageInfo = data?.products?.pageInfo;
-  const totalCount = data?.products?.totalCount || 0;
-  const translation = categoryItem.translation;
-
-  return (
-    <>
-      <MobileSearchBar />
-      <section>
-        <Suspense fallback={<FilterListSkeleton />}>
-          <CategoryDetail
-            categoryItem={{ description: translation?.description ?? "", name: translation?.name ?? "" }}
-
-          />
-        </Suspense>
-        <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full max-w-screen-2xl mx-auto px-4">
-          <FilterList filterAttributes={filterAttributes} />
-          <SortOrder sortOrders={SortByFields} title="Sort by" />
-        </div>
-        <div className="flex items-center justify-between gap-4 py-8 md:hidden w-full max-w-screen-2xl mx-auto px-4">
-          <MobileFilter filterAttributes={filterAttributes} />
-          <SortOrder sortOrders={SortByFields} title="Sort by" />
-        </div>
-
-        {isArray(products) && products.length > 0 ? (
-         <Grid
-                   className="grid grid-flow-row grid-cols-2 gap-5 lg:gap-11.5 w-full max-w-screen-2xl mx-auto md:grid-cols-3 lg:grid-cols-4 px-4 xss:px-7.5"
-                 >
-            <ProductGridItems products={products} />
-          </Grid>
-        ) : (
-          <div className="px-4">
-            <div className="flex h-40 items-center justify-center rounded-lg border border-dashed border-neutral-300">
-              <p className="text-neutral-500">No products found in this category.</p>
-            </div>
-          </div>
-        )}
-
-        {isArray(products) && (totalCount > itemsPerPage || pageInfo?.hasNextPage) && (
-          <nav
-            aria-label="Collection pagination"
-            className="my-10 block items-center sm:flex"
-          >
-            <Pagination
-              itemsPerPage={itemsPerPage}
-              itemsTotal={totalCount || 0}
-              currentPage={currentPage}
-              nextCursor={pageInfo?.endCursor}
-              prevCursor={pageInfo?.startCursor}
-            />
-          </nav>
-        )}
-      </section>
-    </>
-  );
-}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 71 - 0
src/app/(public)/category/[slug]/[id]/_components/CategoryDesc.tsx


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

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

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

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

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

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

+ 74 - 0
src/app/(public)/customer/account/_components/AccountBottomHrefSwiper.tsx

@@ -0,0 +1,74 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+// 只导入 Autoplay 即可!loop 内置了
+import { Autoplay } from "swiper/modules";
+
+import "swiper/css";
+
+const AccountBottomHrefSwiper = () => {
+  const swiperRef = useRef<any>(null);
+
+  const footerCategorySlides = [
+    { url: "/new-arrival.html", title: "New Arrival" },
+    { url: "/hd-lace-wig.html", title: "HD Lace" },
+    { url: "/effortless-wig.html", title: "Ready To Go Wigs" },
+    { url: "/ombrecolorwigs.html", title: "Color" },
+    { url: "/lace-frontal-wig.html", title: "Lace Frontal Wigs" },
+    { url: "/closure-wigs.html", title: "Closure Wigs" },
+  
+  ];
+
+  // 登录/注册/找回密码页面隐藏
+  useEffect(() => {
+    const path = window.location.pathname;
+    const hidePaths = [
+      "/customer/account/login",
+      "/customer/account/forgotpassword",
+      "/customer/account/create",
+    ];
+
+    if (hidePaths.some((p) => path.includes(p))) {
+      const el = document.querySelector(".footer-bottom-category");
+      if (el) el.remove();
+    }
+  }, []);
+
+  return (
+    <div className="footer-bottom-category bg-white py-3">
+        <p className="underline text-center font-bold bg-[#eee] leading-8 ">
+            HOW TO GET MORE POINTS?
+        </p>
+      <div className="footer-bottom-category-list container mx-auto px-4 bg-[#fff8f2] height-12.5 flex items-center ">
+        <Swiper
+          ref={swiperRef}
+          // ✅ 只需要 Autoplay
+          modules={[Autoplay]}
+          // ✅ loop 直接写,不需要模块!
+        //   loop={true}
+          slidesPerView="auto"
+          spaceBetween={12}
+          autoplay={{
+            delay: 1000,
+            disableOnInteraction: false,
+          }}
+          className="footer-bottom-category-swiper"
+        >
+          {footerCategorySlides.map((item, index) => (
+            <SwiperSlide key={index} className="!w-auto footer-bottom-category-item whitespace-nowrap px-4 py-2 text-sm text-gray-700 rounded-lg border border-solid border-[#b32400]">
+              <a
+                href={item.url}
+                className=" "
+              >
+                {item.title}
+              </a>
+            </SwiperSlide>
+          ))}
+        </Swiper>
+      </div>
+    </div>
+  );
+};
+
+export default AccountBottomHrefSwiper;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 158 - 0
src/app/(public)/customer/account/_components/AccountHeader.tsx


+ 341 - 0
src/app/(public)/customer/account/_components/AccountVipPopup.tsx

@@ -0,0 +1,341 @@
+"use client";
+// import Link from "next/link";
+// 轮播逻辑
+import { Swiper, SwiperSlide } from "swiper/react";
+import { Navigation, Scrollbar } from "swiper/modules";
+import { Swiper as SwiperType } from "swiper/types";
+import { useEffect, useState } from "react";
+import Image from "next/image";
+import "swiper/css";
+import "swiper/css/navigation";
+import { clientFetch } from "@/lib/restApiClient";
+
+// 模拟你的每一页数据,方便维护
+const vipList = [
+  {
+    level: 1,
+    title: "V1 Privileges",
+    exppoints: 1000,
+    coupons: [
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130956.png",
+        text1: "Points Redeem",
+        text2: "Giftcard",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130957.png",
+        text1: "Exclusive",
+        text2: "$100 Coupon",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130958.png",
+        text1: "Birthday",
+        text2: "Treat",
+        link: "javascript:void(0)",
+      },
+    ],
+  },
+  {
+    level: 2,
+    title: "V2 Privileges",
+    exppoints: 2000,
+    coupons: [
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130956.png",
+        text1: "POINTS REDEEM",
+        text2: "GIFTCARD",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130957.png",
+        text1: "EXCLUSIVE",
+        text2: "$150 COUPON",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130958.png",
+        text1: "BIRTHDAY",
+        text2: "TREAT",
+        link: "javascript:void(0)",
+      },
+    ],
+  },
+  {
+    level: 3,
+    title: "V3 Privileges",
+    exppoints: 10000,
+    coupons: [
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130956.png",
+        text1: "POINTS REDEEM",
+        text2: "GIFTCARD",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130957.png",
+        text1: "EXCLUSIVE",
+        text2: "$200 COUPON",
+        link: "javascript:void(0)",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130958.png",
+        text1: "BIRTHDAY",
+        text2: "TREAT",
+        link: "javascript:void(0)",
+      },
+    ],
+  },
+  {
+    level: 4,
+    title: "V4 Privileges",
+    exppoints: 50000,
+    coupons: [
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130956.png",
+        text1: "POINTS REDEEM",
+        text2: "GIFTCARD",
+        link: "/customer/account/pointsreceivelist",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130957.png",
+        text1: "EXCLUSIVE",
+        text2: "$300 COUPON",
+        link: "/customer/account/couponsreceivelist/v/4/",
+      },
+      {
+        img: "https://cdn.alipearlhair.com/media/wysiwyg/wap/202301130958.png",
+        text1: "BIRTHDAY",
+        text2: "TREAT",
+        link: "/customer/account/birthday",
+      },
+    ],
+  },
+];
+
+export default function AccountVipPopup({
+  visible,
+  onClose,
+}: {
+  visible: boolean;
+  onClose: () => void;
+}) {
+  // swiper实例
+  const [_swiperRef, setSwiperRef] = useState<SwiperType | null>(null);
+  // 当前slide下标
+  const [activeIndex, setActiveIndex] = useState(0);
+  const [_loading, setLoading] = useState(true);
+  const [VipGrothData, setVipGrothData] = useState<any>(null);
+  // 如果不显示,直接 return null
+  useEffect(() => {
+    const fetchGrothValue = async () => {
+      try {
+        setLoading(true);
+        const res = await clientFetch("/api/customer/growth-value");
+        if (res.data.success) {
+          setVipGrothData(res.data.data);
+          console.log("vip成长值数据:", res.data.data);
+        }
+      } catch (error) {
+        console.error("获取vip成长值失败:", error);
+      } finally {
+        setLoading(false);
+      }
+    };
+    fetchGrothValue();
+  }, []);
+  if (!visible) return null;
+  // 滑动切换触发
+  const handleSlideChange = (swiper: SwiperType) => {
+    setActiveIndex(swiper.activeIndex);
+    console.log("aaa slide huadongle ");
+  };
+  const slideItem = vipList[activeIndex];
+  const slideLevelNum = slideItem?.level ?? 1;
+
+  const progressPercent = VipGrothData
+    ? Math.min(
+        Math.max(
+          (VipGrothData.growth_value /
+            VipGrothData.next_level.min_growth_value) *
+            100,
+          0,
+        ),
+        100,
+      )
+    : 0;
+  const userCurrentLevelNum =
+    Number(VipGrothData?.current_level.name.replace(/[^0-9]/g, "")) || 0;
+  let showText = "";
+  console.log("userCurrentLevelNum", userCurrentLevelNum, slideLevelNum);
+  if (userCurrentLevelNum === slideLevelNum) {
+    showText = `Growth Value ${VipGrothData?.growth_value}/${VipGrothData?.next_level.min_growth_value}`;
+  } else if (userCurrentLevelNum > slideLevelNum) {
+    showText = "aaa";
+  } else {
+    showText = `Integral to 2000`;
+  }
+  return (
+    // 全屏遮罩
+    <div className="fixed inset-0 z-50 bg-black/50">
+      {/* 弹框主体 - 从底部滑上来 */}
+      <div className="absolute bottom-0 rounded-none left-0 right-0 w-full h-full bg-white  overflow-auto">
+        <div className="flex flex-col">
+          {/* 顶部导航 */}
+          <header className="bg-black text-white py-4 px-4 flex items-center">
+            <button onClick={onClose} className="">
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                className="h-6 w-6"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M15 19l-7-7 7-7"
+                />
+              </svg>
+            </button>
+            <h1 className="mx-auto">Vip Center</h1>
+          </header>
+          <div className="vipcenter-privileges-modal-innerscroll-father">
+            <div id="vipcenter-privileges-modal-innerscroll">
+              <div
+                className="   relative
+    h-170
+    mt-8
+    px-6
+    bg-[url('https://cdn.alipearlhair.com/media/wysiwyg/wap/_2.png')]
+    bg-no-repeat
+    bg-[length:100%_100%]"
+              >
+                <div className="absolute top-0.75 left-8 text-4.5 text-[#FFD1B9] font-semibold">
+                  Vip <span className="levelcard">{userCurrentLevelNum}</span>
+                </div>
+                <div className="flex flex-col justify-between items-center text-white">
+                  <div className="w-18 h-18 mt-6">
+                    <Image
+                      src="https://cdn.alipearlhair.com/media/wysiwyg/newap/202303131637.png"
+                      alt=""
+                      width={0}
+                      height={0}
+                      sizes="100vw"
+                      className="max-w-full border-0 align-top w-full h-auto"
+                    />
+                  </div>
+                  <p className="text-4.5 mt-3 font-semibold"></p>
+                  <p className="text-text-sm mt-1.5 text-[#EFEFEF]">
+                    18567768101@163.com
+                  </p>
+                  <p className="text-sm mt-1.5 text-[#EFEFEF]">
+                    Above current level
+                  </p>
+                </div>
+                <div className="box-border w-full mt-10.5 mb-9.75">
+                  <div className="text-center text-sm text-white mb-3">
+                    <span className="account-center-progress-text-pre">
+                      {showText}
+                    </span>
+                  </div>
+                  <div className="relative h-1 bg-[#5F4040]">
+                    <div
+                      className="w-4 h-4 rounded-full bg-black absolute top-1/2 -translate-y-1/2  block"
+                      style={{ left: `${progressPercent}%` }}
+                    ></div>
+                    <div
+                      className="rounded-[1.2821vw] h-full bg-[rgba(255,255,255,1)]"
+                      style={{ width: `${progressPercent}%` }}
+                    ></div>
+                  </div>
+                  {/* 轮播 */}
+                  <div className="swiper-container-center vip-swiper">
+                    <Swiper
+                      modules={[Navigation, Scrollbar]}
+                      slidesPerView={1}
+                      navigation={{
+                        prevEl: ".btn-prev", // 左箭头class
+                        nextEl: ".btn-next", // 右箭头class
+                        disabledClass: "opacity-20", // 滑到首尾自动加这个类,Tailwind直接用
+                      }}
+                      onSwiper={(swiper) => setSwiperRef(swiper)}
+                      onSlideChange={handleSlideChange}
+                      spaceBetween={0}
+                      className="swiper-container-horizontal"
+                    >
+                      {vipList.map((item, idx) => (
+                        <SwiperSlide
+                          key={idx}
+                          className="swiper-slide-color"
+                          style={{ width: "343px" }}
+                          data-exppoints={item.exppoints}
+                          data-level={item.level}
+                        >
+                          {/* 标题 */}
+                          <h3 className="text-lg font-medium mb-4 mt-4">
+                            {item.title}
+                          </h3>
+
+                          {/* 权益卡片容器 */}
+                          <div className="vipcenter-privileges-coupnts flex gap-3 flex-wrap">
+                            {item.coupons.map((coupon, cIdx) => (
+                              <a
+                                key={cIdx}
+                                href={coupon.link}
+                                className="vipcenter-privileges-coupnt-a vipcenter-privileges-coupnt-ponit"
+                              >
+                                <button className="vipcenter-privileges-coupnt flex flex-col items-center p-2 border rounded">
+                                  <img
+                                    src={coupon.img}
+                                    alt=""
+                                    className="w-10 h-10 object-contain mb-1"
+                                  />
+                                  <p className="text-sm">{coupon.text1}</p>
+                                  <p className="text-sm">{coupon.text2}</p>
+                                </button>
+                              </a>
+                            ))}
+                          </div>
+                        </SwiperSlide>
+                      ))}
+                      {/* 自定义左右按钮,放在Swiper内部,同级slide */}
+                      <button className="btn-prev absolute left-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
+                        {/* 自定义左箭头SVG */}
+                        <svg
+                          width="16"
+                          height="16"
+                          viewBox="0 0 24 24"
+                          fill="none"
+                          stroke="#fff"
+                          stroke-width="2"
+                        >
+                          <path d="M15 18l-6-6 6-6" />
+                        </svg>
+                      </button>
+                      <button className="btn-next absolute right-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
+                        {/* 自定义右箭头SVG */}
+                        <svg
+                          width="16"
+                          height="16"
+                          viewBox="0 0 24 24"
+                          fill="none"
+                          stroke="#fff"
+                          stroke-width="2"
+                        >
+                          <path d="M9 18l6-6-6-6" />
+                        </svg>
+                      </button>
+                    </Swiper>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 112 - 0
src/app/(public)/customer/account/_components/HelpModal.tsx

@@ -0,0 +1,112 @@
+import Link from "next/link";
+export default function HelpModal({
+  helpvisible,
+  helponClose,
+}: {
+  helpvisible: boolean;
+  helponClose: () => void;
+}) {
+  // 如果不显示,直接 return null
+  if (!helpvisible) return null;
+  return (
+    <div className="fixed inset-0 z-51 bg-black/50">
+      {/* 弹框主体 - 从底部滑上来 */}
+      <div className="absolute bottom-0 rounded-none left-0 right-0 w-full h-full bg-white  overflow-auto">
+        <div className="flex flex-col">
+          {/* 顶部导航 */}
+          <header className="bg-black text-white py-4 px-4 flex items-center">
+            <button onClick={helponClose} className="mr-4">
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                className="h-6 w-6"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M15 19l-7-7 7-7"
+                />
+              </svg>
+            </button>
+            <h1 className="text-lg font-medium">HELP & SUPPORT</h1>
+          </header>
+          {/* 菜单列表 */}
+          <div className="px-4 py-2">
+            {/* Information */}
+            <Link
+              href={"/help-faqs"}
+              className="py-4 border-b border-gray-100 flex justify-between items-center"
+            >
+              {/* <div > */}
+              <span className="text-black font-medium">Help & FAQ</span>
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                className="h-5 w-5 text-gray-400"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M9 5l7 7-7 7"
+                />
+              </svg>
+              {/* </div> */}
+            </Link>
+            {/* Address */}
+            <Link
+              href={"/track_order"}
+              className="py-4 border-b border-gray-100 flex justify-between items-center"
+            >
+              {/* <div > */}
+              <span className="text-black font-medium">Track Order</span>
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                className="h-5 w-5 text-gray-400"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M9 5l7 7-7 7"
+                />
+              </svg>
+              {/* </div> */}
+            </Link>
+            {/* help & Support */}
+            <Link
+              href={"/hair-care-tips"}
+              className="py-4 border-b border-gray-100 flex justify-between items-center"
+            >
+              {/* <div > */}
+              <span className="text-black font-medium">Hair Care Tips</span>
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                className="h-5 w-5 text-gray-400"
+                fill="none"
+                viewBox="0 0 24 24"
+                stroke="currentColor"
+              >
+                <path
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth={2}
+                  d="M9 5l7 7-7 7"
+                />
+              </svg>
+              {/* </div> */}
+            </Link>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 183 - 0
src/app/(public)/customer/account/mypoints/_components/GetPoints.tsx

@@ -0,0 +1,183 @@
+"use client";
+// import Link from "next/link";
+import React, { useState, useEffect } from "react";
+import { clientFetch } from "@/lib/restApiClient";
+
+export default function GetPoints() {
+  const [taskData, setTaskData] = useState<any>(null);
+  const [loading, setLoading] = useState(true);
+  useEffect(() => {
+    const fetchRewardPointsHistory = async () => {
+      try {
+        setLoading(true);
+        const res = await clientFetch("/api/customer/reward-points/follow-status");
+      
+        setTaskData(res.data.data);
+        console.log("积分任务数据:", res.data.data);
+      } catch (error) {
+        console.error("获取任务状态失败:", error);
+      } finally {
+        setLoading(false);
+      }
+    };
+    fetchRewardPointsHistory();
+  }, []);
+
+  function canTriggerTask(item: (typeof earnPointsItems)[0]) {
+  
+    if (!taskData) return false;
+
+    const { type } = item;
+    const { browsed_product_ids, platforms } = taskData;
+
+    if (!type) return true;
+
+   
+    if (browsed_product_ids.includes(type)) {
+      return false;
+    }
+
+    const platform = platforms.find((p: any) => p.code === type);
+    if (platform) {
+      return !platform.followed;
+    }
+
+    return true;
+  }
+
+  
+  const handleClaimPoints = async (item: any) => {
+ 
+    if (!canTriggerTask(item)) return;
+
+    try {
+      console.log("触发领取积分,任务类型:", item.type);
+      
+      // const getpointsres = await clientFetch("/api/customer/reward-points/follow", {  
+      //   method: "POST",
+      //   body: JSON.stringify({ platform: item.type }),
+      // });
+      // 这个浏览获取积分接口后面放到列表页去调
+      const getpointsres = await clientFetch("/api/customer/reward-points/browse-product", {  
+        method: "POST",
+        body: JSON.stringify({ categorys: item.type }),
+      });
+      if(getpointsres.data.success){
+        alert("Points claimed successfully!");
+      }
+      
+      const res = await clientFetch("/api/customer/reward-points/follow-status");
+      setTaskData(res.data.data);
+    } catch (err) {
+      console.error("领取失败", err);
+      alert("Failed to claim points");
+    }
+  };
+
+  const earnPointsItems = [
+    {
+      title: "Share a plain text review",
+      desc: "To Get 50 points",
+      points: "",
+      type: "",
+    },
+    {
+      title: "Share your wig review with photos/videos",
+      desc: "To Get 100 Points",
+      points: "",
+      type: "",
+    },
+    {
+      title: "Log In Our Website",
+      desc: "Every Day To Get Points",
+      points: "",
+      type: "",
+    },
+    {
+      title: "Browse New Arrival Page ≥ 15 Seconds",
+      desc: "To Get 10 Points",
+      points: "",
+      type: "aaa",
+    },
+    {
+      title: "Browse HD Lace Wigs ≥ 15 Seconds",
+      desc: "To Get 10 Points",
+      points: "",
+      type: "ccc",
+    },
+    {
+      title: "Browse Ready To Go Wigs ≥ 15 Seconds",
+      desc: "To Get 10 Points",
+      points: "",
+      type: "bbbb",
+    },
+    {
+      title: "Follow Our Instagram Account",
+      desc: "To Get 20 Points",
+      points: "",
+      type: "ig",
+    },
+    {
+      title: "Follow Our Facebook Account",
+      desc: "To Get 20 Points",
+      points: "",
+      type: "fb",
+    },
+    {
+      title: "Follow Our Youtube Channel",
+      desc: "To Get 20 Points",
+      points: "",
+      type: "ytb",
+    },
+    {
+      title: "Follow Our Tiktok Account",
+      desc: "To Get 20 Points",
+      points: "",
+      type: "tt",
+    },
+  ];
+
+  return (
+    <div className="space-y-2">
+      {loading ? (
+        <div>Loading tasks...</div>
+      ) : (
+        earnPointsItems.map((item, index) => {
+          const canClick = canTriggerTask(item);
+          return (
+            <div
+              key={index}
+              className="h-15.5 flex items-center justify-between bg-[#F8F8F8] p-2.5 rounded-md "
+            >
+              <div>
+                <h3 className="font-medium text-black">{item.title}</h3>
+                {item.desc && <p className="text-sm text-gray-500">{item.desc}</p>}
+              </div>
+              <div className="flex items-center gap-6">
+                {item.points && (
+                  <span className="font-medium">{item.points}</span>
+                )}
+                <button
+                  onClick={() => handleClaimPoints(item)}
+                  disabled={!canClick}
+                  className={`transition-colors ${
+                    canClick ? "text-black" : "text-gray-300 cursor-not-allowed"
+                  }`}
+                >
+                  <svg
+                    viewBox="0 0 24 24"
+                    width="16"
+                    height="16"
+                    fill="currentColor"
+                  >
+                    <path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
+                  </svg>
+                </button>
+              </div>
+            </div>
+          );
+        })
+      )}
+    </div>
+  );
+}

+ 28 - 0
src/app/(public)/customer/account/mypoints/_components/PointsRule.tsx

@@ -0,0 +1,28 @@
+"use client";
+import React, { useState } from "react";
+import PointsRuleModal from "@/components/customer/PointsRule";
+export default function PointsRule() {
+  // 控制弹窗显隐的状态
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  // 打开弹窗(页面点击触发)
+  const ruleClick = () => {
+    console.log("aaaa");
+    setIsModalOpen(true);
+  };
+  return (
+    <>
+      <div
+        onClick={ruleClick}
+        className="text-black text-sm font-medium underline"
+      >
+        Rules
+      </div>
+      {/* 积分规则组件 */}
+      <PointsRuleModal
+        visible={isModalOpen}
+        handleCloseModal={() => setIsModalOpen(false)}
+      />
+    </>
+  );
+}

+ 16 - 71
src/app/(public)/customer/account/mypoints/page.tsx

@@ -1,9 +1,7 @@
-"use client";
 import Image from "next/image";
 import Link from "next/link";
-import React, { useState } from "react";
-import PointsRuleModal from "@/components/customer/PointsRule";
-
+import PointsRule from "./_components/PointsRule"
+import GetPoints from "./_components/GetPoints"
 const AccountPointsPage = () => {
   // 模拟用户积分数据
   const userPoints = 100009;
@@ -30,34 +28,13 @@ const AccountPointsPage = () => {
     },
   ];
 
-  // 积分获取项配置
-  const earnPointsItems = [
-    { title: "Make a Purchase", desc: "", points: "1 Points Per $1" },
-    { title: "Post a Review", desc: "", points: "+50 points" },
-    {
-      title: "Log In",
-      desc: "",
-      points: "+10 points",
-    },
-    {
-      title: "Reward for Registering",
-      desc: "",
-      points: "+200 points",
-    },
-  ];
-   // 控制弹窗显隐的状态
-  const [isModalOpen, setIsModalOpen] = useState(false);
-
-  // 打开弹窗(页面点击触发)
-  const ruleClick = () => {
-    console.log('aaaa');
-    setIsModalOpen(true);
-  };
-
   return (
     <div className="w-full h-full">
       <div className="bg-[#fff] pr-2.5 pl-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
-        <Link href={"/customer/account"} className="absolute left-2.5 text-2xl inline-block top-1/2  -translate-y-1/2">
+        <Link
+          href={"/customer/account"}
+          className="absolute left-2.5 text-2xl inline-block top-1/2  -translate-y-1/2"
+        >
           <svg
             xmlns="http://www.w3.org/2000/svg"
             width="24"
@@ -73,8 +50,8 @@ const AccountPointsPage = () => {
               d="M15 18L9 12L15 6"
             ></path>
           </svg>
-          </Link>
-        <span className="vipReturnTitles">SHIPPING ADDRESS</span>
+        </Link>
+        <span className="vipReturnTitles">My Points</span>
       </div>
       <div className=" bg-[#FFF5F2] px-4 py-8 max-w-3xl mx-auto pt-6 pb-5">
         {/* 顶部积分展示区 */}
@@ -138,12 +115,13 @@ const AccountPointsPage = () => {
           </div>
           {/* Details & Rules 链接 */}
           <div className="flex gap-4 text-sm font-medium">
-            <a href="#" className="text-black text-sm font-medium underline">
+            <Link
+              href="/customer/account/pointlist"
+              className="text-black text-sm font-medium underline"
+            >
               Details
-            </a>
-            <a href="#" onClick={ruleClick} className="text-black text-sm font-medium underline">
-              Rules
-            </a>
+            </Link>
+            <PointsRule/>
           </div>
         </div>
 
@@ -169,42 +147,9 @@ const AccountPointsPage = () => {
         <h2 className="text-base mt-6 mb-8  font-bold text-black text-center">
           Earn Points
         </h2>
-        <div className="space-y-2">
-          {earnPointsItems.map((item, index) => (
-            <div
-              key={index}
-              className="h-15.5 flex items-center justify-between bg-[#F8F8F8] p-2.5 rounded-md "
-            >
-              <div>
-                <h3 className="font-medium text-black">{item.title}</h3>
-                {item.desc && (
-                  <p className="text-sm text-gray-500">{item.desc}</p>
-                )}
-              </div>
-              <div className="flex items-center gap-6">
-                {item.points && (
-                  <span className="font-medium">{item.points}</span>
-                )}
-                <button className="text-gray-400 hover:text-black transition-colors">
-                  <svg
-                    viewBox="0 0 24 24"
-                    width="16"
-                    height="16"
-                    fill="currentColor"
-                  >
-                    <path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
-                  </svg>
-                </button>
-              </div>
-            </div>
-          ))}
-        </div>
+        <GetPoints />
       </div>
-      {/* 积分规则组件 */}
-      <PointsRuleModal 
-         visible={isModalOpen}
-          handleCloseModal={() => setIsModalOpen(false)}
-      />
+    
     </div>
   );
 };

Dosya farkı çok büyük olduğundan ihmal edildi
+ 14 - 129
src/app/(public)/customer/account/page.tsx


+ 104 - 221
src/app/(public)/customer/account/pointlist/page.tsx

@@ -1,201 +1,97 @@
 "use client";
-// import Image from "next/image";
 import React, { useState, useEffect, useRef, useCallback } from "react";
+import Link from "next/link";
+import { clientFetch } from "@/lib/restApiClient";
 
+// 👉 你的 TS 类型(正确)
 interface ListDates {
+  amount: number;
+  balance: number;
+  created_at: string;
+  detail: string;
+  expired_day: number;
+  expired_time: string | null;
   id: number;
-  points: string;
-  title: string;
-  type: string;
-  // desc: string;
-  date: string;
-  // createTime: string;
-  currentPoints: string;
-  // 你自己加字段...
+  order_id: number | null;
+  remaining_points: number;
+  status: number;
+  status_text: string;
+  type: number;
+  type_text: string;
 }
-// 模拟积分明细数据(可替换为接口请求)
-const mockPointsData: ListDates[] = [
-  {
-    id: 1,
-    type: "rewarded",
-    title: "Check In",
-    points: "+10 Points",
-    date: "May 17, 2026 11:07 PM",
-    currentPoints: "My points:100009",
-  },
-  {
-    id: 2,
-    type: "rewarded",
-    title: "Points Return of Redeem Hair Usage",
-    points: "+12000 Points",
-    date: "Oct 23, 2025 1:11 AM",
-    currentPoints: "My points:99999",
-  },
-  {
-    id: 3,
-    type: "used",
-    title: "Points Redeem Hair",
-    points: "-6000 Points",
-    date: "Oct 17, 2025 10:27 PM",
-    currentPoints: "My points:87999",
-  },
-  {
-    id: 4,
-    type: "used",
-    title: "Points Redeem Hair",
-    points: "-6000 Points",
-    date: "Oct 17, 2025 10:25 PM",
-    currentPoints: "My points:93999",
-  },
-  {
-    id: 5,
-    type: "rewarded",
-    title: "Updated by Admin",
-    points: "+99999 Points",
-    date: "Oct 17, 2025 10:24 PM",
-    currentPoints: "My points:99999",
-  },
-  {
-    id: 6,
-    type: "used",
-    title: "Points Redeem Giftcard",
-    points: "-300 Points",
-    date: "Oct 13, 2025 8:41 PM",
-    currentPoints: "My points:0",
-  },
-  {
-    id: 7,
-    type: "rewarded",
-    title: "Reward for Registering",
-    points: "+200 Points",
-    date: "Jun 9, 2025 7:39 PM",
-    currentPoints: "My points:300",
-  },
-  {
-    id: 8,
-    type: "rewarded",
-    title: "Reward for Signing up Newsletter",
-    points: "+100 Points",
-    date: "Jun 9, 2025 7:39 PM",
-    currentPoints: "My points:100",
-  },
-  // 补充测试数据(凑够分页演示)
-  {
-    id: 9,
-    type: "rewarded",
-    title: "Check In",
-    points: "+10 Points",
-    date: "May 16, 2026 10:00 PM",
-    currentPoints: "My points:100000",
-  },
-  {
-    id: 10,
-    type: "used",
-    title: "Points Redeem Accessory",
-    points: "-500 Points",
-    date: "May 15, 2026 9:00 AM",
-    currentPoints: "My points:99990",
-  },
-  {
-    id: 11,
-    type: "rewarded",
-    title: "Review Product",
-    points: "+50 Points",
-    date: "May 14, 2026 8:00 PM",
-    currentPoints: "My points:100490",
-  },
-  {
-    id: 12,
-    type: "rewarded",
-    title: "Refer a Friend",
-    points: "+200 Points",
-    date: "May 13, 2026 7:00 PM",
-    currentPoints: "My points:100440",
-  },
-  {
-    id: 13,
-    type: "used",
-    title: "Points Redeem Cash",
-    points: "-1000 Points",
-    date: "May 12, 2026 6:00 PM",
-    currentPoints: "My points:100240",
-  },
-  {
-    id: 14,
-    type: "rewarded",
-    title: "Check In",
-    points: "+10 Points",
-    date: "May 11, 2026 5:00 PM",
-    currentPoints: "My points:101240",
-  },
-];
 
 const PointsDetails = () => {
-  // 状态管理
-  const [activeTab, setActiveTab] = useState("all"); // 激活标签:all/rewarded/used
-  const [listData, setListData] = useState<ListDates[]>([]); // 展示的列表数据
-  const [page, setPage] = useState(1); // 当前页码
-  const [loading, setLoading] = useState(false); // 加载状态
-  const [hasMore, setHasMore] = useState(true); // 是否有更多数据
-  const listRef = useRef<HTMLDivElement>(null); // 列表容器ref,用于监听滚动
+  // 状态
+  const [activeTab, setActiveTab] = useState<string>(""); // earned / redeemed / 空
+  const [listData, setListData] = useState<ListDates[]>([]);
+  const [page, setPage] = useState<number>(1);
+  const [loading, setLoading] = useState<boolean>(false);
+  const [hasMore, setHasMore] = useState<boolean>(true);
+  const listRef = useRef<HTMLDivElement>(null);
+  const PAGE_SIZE = 10;
 
-  // 每页展示12条
-  const PAGE_SIZE = 12;
-  const fetchData = useCallback((page: number, tab: string): Promise<ListDates[]> => {
-    return new Promise((resolve) => {
-        setTimeout(() => {
-          let data = mockPointsData;
-          const prePage = page - 1;
-          const start = prePage * PAGE_SIZE;
-          const end = page * PAGE_SIZE;
-          if (tab === "rewarded") {
-            data = mockPointsData.filter(
-              (item) => item.type === "rewarded",
-            );
-          } else if (tab === "used") {
-            data = mockPointsData.filter((item) => item.type === "used");
-          }
-          const res = data.slice(start, end);
-          resolve(res);
-        }, 800);
-    });
-  },[]);
-  // 加载更多数据
-  const loadMore = useCallback(() => {
+  // ==============================================
 
+  const loadData = useCallback(async (currentPage: number, tab: string) => {
     setLoading(true);
-    
-    fetchData(page+1,activeTab).then((res:any) => {
+    try {
+      const resp = await clientFetch(
+        `/api/customer/reward-points/history?page=${currentPage}&per_page=${PAGE_SIZE}&amountType=${tab}`,
+      );
 
-        setListData(res);
+      console.log("接口返回:", resp);
 
-        setLoading(false);
-        setPage(page+1);
-        if(res.length < PAGE_SIZE) {
-           setHasMore(false);
+      if (resp.status === 200) {
+        const formData = resp.data?.data || resp.data || { history: [] };
+        const newList = formData.history || [];
+        // 测试数据
+        //  const testList = [...newList, ...newList, ...newList, ...newList];
+        // 分页判断:如果返回不足10条 = 没有更多
+        if (newList.length < PAGE_SIZE) {
+          setHasMore(false);
+        } else {
+          setHasMore(true);
         }
-    });
-  },[fetchData,page,activeTab]);
-  
-  // 初始化
-  useEffect(() => {
-    // 筛选数据
-    fetchData(1,'all').then((result:any)=> {
-        
-        setListData(result);
-        // 判断是否有更多数据
-        setHasMore(!(result.length < PAGE_SIZE));
-    });
-    
+
+        if (currentPage === 1) {
+          setListData(newList);
+        } else {
+          setListData((prev) => [...prev, ...newList]);
+        }
+      }
+    } catch (err) {
+      console.error("加载失败:", err);
+    } finally {
+      setLoading(false);
+    }
   }, []);
 
-  // 监听滚动:触底加载
+  const loadMore = useCallback(() => {
+    console.log("loading:", loading, "hasMore:", hasMore);
+
+    if (loading || !hasMore) return;
+    const nextPage = page + 1;
+    loadData(nextPage, activeTab);
+    setPage(nextPage);
+  }, [loading, hasMore, page, activeTab, loadData]);
+
+  useEffect(() => {
+    // 封装内部异步函数,规范写法
+    const initLoad = async () => {
+      setListData([]); // 切换时清空列表
+      loadData(1, activeTab);
+      setPage(1);
+    };
+    initLoad();
+  }, [activeTab]);
+
   useEffect(() => {
+    const container = listRef.current;
     const handleScroll = () => {
-      if (loading || !hasMore) return;
-      const container = listRef.current;
-      // 滚动到底部的判断:滚动高度 + 可视高度 ≥ 总高度 - 20(阈值)
-      if (container &&
+      // console.log("container:",container,"container.scrollTop:",container.scrollTop,"container.clientHeight:",container.clientHeight,"container.scrollHeight:",container.scrollHeight);
+
+      if (!container) return;
+      if (
         container.scrollTop + container.clientHeight >=
         container.scrollHeight - 20
       ) {
@@ -203,94 +99,82 @@ const PointsDetails = () => {
       }
     };
 
-    const container = listRef.current;
-    if (container) {
-      container.addEventListener("scroll", handleScroll);
-      return () => container.removeEventListener("scroll", handleScroll);
-    }
-  }, [loading, hasMore, loadMore]);
+    container?.addEventListener("scroll", handleScroll);
+    return () => container?.removeEventListener("scroll", handleScroll);
+  }, [loadMore]);
 
   const clickActiveTab = (tab: string) => {
-      setActiveTab(tab);
-      setPage(1);
-      fetchData(1, tab);
-  }
+    setActiveTab(tab);
+  };
 
   return (
     <div className="min-h-screen bg-white">
-      {/* 顶部导航栏 */}
+      {/* 顶部 */}
       <div className="sticky top-0 z-10 bg-white border-b border-gray-100 py-4 px-4 flex items-center">
-        {/* 返回按钮 */}
-        <button className="mr-4">
+        <Link href={"/customer/account/mypoints"} className="mr-4">
           <svg viewBox="0 0 24 24" width="20" height="20" fill="black">
             <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
           </svg>
-        </button>
-        {/* 标题 */}
+        </Link>
         <h1 className="text-xl font-bold text-black">Points Details</h1>
       </div>
 
-      {/* 标签切换栏 */}
+      {/* Tab */}
       <div className="flex border-b border-gray-200">
         <button
-          onClick={() => clickActiveTab("all")}
+          onClick={() => clickActiveTab("")}
           className={`px-6 py-3 font-medium text-sm ${
-            activeTab === "all"
-              ? "bg-black text-white"
-              : "bg-white text-gray-600 hover:bg-gray-50"
+            activeTab === "" ? "bg-black text-white" : "bg-white text-gray-600"
           }`}
         >
           ALL
         </button>
         <button
-          onClick={() => clickActiveTab("rewarded")}
+          onClick={() => clickActiveTab("earned")}
           className={`px-6 py-3 font-medium text-sm ${
-            activeTab === "rewarded"
+            activeTab === "earned"
               ? "bg-black text-white"
-              : "bg-white text-gray-600 hover:bg-gray-50"
+              : "bg-white text-gray-600"
           }`}
         >
           REWARDED
         </button>
         <button
-          onClick={() => clickActiveTab("used")}
+          onClick={() => clickActiveTab("redeemed")}
           className={`px-6 py-3 font-medium text-sm ${
-            activeTab === "used"
+            activeTab === "redeemed"
               ? "bg-black text-white"
-              : "bg-white text-gray-600 hover:bg-gray-50"
+              : "bg-white text-gray-600"
           }`}
         >
           USED
         </button>
       </div>
 
-      {/* 积分明细列表(带滚动监听) */}
+      {/* 列表 */}
       <div ref={listRef} className="h-[calc(100vh-120px)] overflow-y-auto">
         {listData.length > 0 ? (
           <div className="divide-y divide-gray-100">
             {listData.map((item) => (
               <div key={item.id} className="px-4 py-4">
-                {/* 左侧:标题+日期 */}
                 <div className="flex justify-between items-start">
                   <div>
                     <h3 className="text-base font-medium text-black">
-                      {item.title}
+                      {item.detail}
                     </h3>
-                    <p className="text-xs text-gray-500 mt-1">{item.date}</p>
+                    <p className="text-xs text-gray-500 mt-1">
+                      {item.created_at}
+                    </p>
                   </div>
-                  {/* 右侧:积分变动 */}
                   <div className="text-right">
                     <p
-                      className={`text-base font-medium ${
-                        item.points.startsWith("+")
-                          ? "text-black"
-                          : "text-black"
-                      }`}
+                      className={`text-base font-medium ${item.amount >= 0 ? "text-green-600" : "text-black"}`}
                     >
-                      {item.points}
+                      {item.amount >= 0 ? `+${item.amount}` : item.amount}{" "}
+                      Points
                     </p>
                     <p className="text-xs text-gray-500 mt-1">
-                      {item.currentPoints}
+                      My points: {item.balance}
                     </p>
                   </div>
                 </div>
@@ -298,16 +182,15 @@ const PointsDetails = () => {
             ))}
           </div>
         ) : (
-          // 空数据提示
           <div className="flex items-center justify-center h-32">
             <p className="text-gray-500 text-sm">NO DATA</p>
           </div>
         )}
 
-        {/* 加载中/无更多提示 */}
+        {/* 加载提示 */}
         <div className="px-4 py-3 text-center text-sm">
           {loading && <p className="text-gray-500">Loading...</p>}
-          {!loading && !hasMore && (
+          {!loading && !hasMore && listData.length > 0 && (
             <p className="text-gray-500">NO MORE DATA</p>
           )}
         </div>

+ 58 - 0
src/app/(public)/customer/address/_components/DefaultAddress.tsx

@@ -0,0 +1,58 @@
+"use client";
+import { useState, useEffect } from "react";
+import { clientFetch } from "@/lib/restApiClient";
+import Link from "next/link";
+export default function DefaultAddress() {
+  const [addressDatas, setAddressDatas] = useState<any[]>([]); // 加 any[] 提示
+
+  useEffect(() => {
+    const load = async () => {
+      // const page = 1;
+      const resp = await clientFetch(
+        `/api/customer/token/address`, ///api/shop/customer-addresses?page=${page}
+      );
+      console.log("完整返回:", resp.data);
+      resp.data.data= Array.isArray(resp.data.data) ? resp.data.data : [];
+      const sortedList = [...resp.data.data].sort((a, b) => {
+        // true(1) 排在 false(0) 前面
+        if (a.default_address !== b.default_address) {
+          return b.default_address ? 1 : -1;
+        }
+        // 相同状态,保留原顺序(返回0即可)
+        return 0;
+      });
+      setAddressDatas(sortedList);
+      console.log("aaaa",sortedList,addressDatas);
+      
+    };
+    load();
+  }, []);
+  return (
+    <>
+      {(addressDatas || []).map((item) => (
+        <div key={item.id} className="mb-6">
+          {item.default_address ? (
+            <h2 className="text-xl font-bold uppercase mb-3">
+              Default Billing Address
+            </h2>
+          ) : null}
+          <div className="border border-gray-200 p-4">
+            <p className="mb-1">{item.first_name + item.last_name}</p>
+            <p className="mb-1">{item.address}</p>
+            <p className="mb-1">{item.city + "," + item.postcode}</p>
+            <p className="mb-1">{item.country}</p>
+            <p className="mb-4">T: {item.phone}</p>
+            <div className="text-right">
+              <Link
+                href={`/customer/address/eidt/${item.id}`}
+                className="text-blue-600 underline hover:text-blue-800"
+              >
+                Change Billing Address
+              </Link>
+            </div>
+          </div>
+        </div>
+      ))}
+    </>
+  );
+}

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

@@ -0,0 +1,306 @@
+"use client";
+import { useState, useEffect, useMemo } from "react";
+import { useParams } from "next/navigation";
+import Link from "next/link";
+import { clientFetch } from "@/lib/restApiClient";
+import Select from "@/components/theme/ui/Select";
+import { useForm, FormProvider, Controller } from "react-hook-form";
+import CustomerAddressPhone from "../../new/_components/CustomerAddressPhone";
+const CustomerAddressEditPage = () => {
+  // 定义表单类型(严格对齐你要的字段)
+  interface AddressFormData {
+    first_name: string;
+    last_name: string;
+    email: string;
+    phone: string;
+    address: string[];
+    country: string;
+    state: string;
+    city: string;
+    postcode: string;
+    default_address: number;
+  }
+  const { id } = useParams();
+  // const [form, setForm] = useState({});
+  // 1. 根据ID获取地址详情
+  console.log("addressid:", id);
+  const COUNTRIES = [
+    { id: 1, code: "AF", name: "Afghanistan" },
+    { id: 2, code: "AX", name: "Åland Islands" },
+    { id: 3, code: "AL", name: "Albania" },
+    { id: 4, code: "DZ", name: "Algeria" },
+    { id: 49, code: "CN", name: "China" },
+    { id: 244, code: "US", name: "United States" },
+  ];
+  const countriesOptions = useMemo(() => {
+    return COUNTRIES.map((country) => ({
+      value: country.code,
+      label: country.name,
+      id: String(country.id),
+    }));
+  }, [COUNTRIES]);
+  const [actionType, setActionType] = useState<"save" | "delete">("save");
+  // 修改地址到接口
+  const UpdataAddress = async (formData: AddressFormData) => {
+    const res = await clientFetch(`/api/customer/token/address?id=${id}`, {
+      method: "PUT",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(formData),
+    });
+    const json = await res;
+    console.log("提交成功,返回结果:", json);
+    if (json?.data?.success) {
+      window.location.reload();
+    } else {
+      alert("error");
+    }
+    return json;
+  };
+  // 删除地址接口
+  const DeleteAddress = async () => {
+    const res = await clientFetch(`/api/customer/token/address?id=${id}`, {
+      method: "DELETE",
+      headers: {
+        "Content-Type": "application/json",
+      },
+    });
+    const json = await res;
+    console.log("删除成功,返回结果:", json);
+  };
+  // 表单提交
+  const addressFormOnSubmit = async (formData: AddressFormData) => {
+    console.log("actionType", actionType);
+
+    if (actionType === "save") {
+      UpdataAddress(formData);
+    } else {
+      DeleteAddress();
+      console.log("执行删除");
+    }
+  };
+
+  // 表单实例(严格对齐你需要的字段)
+  const addressForm = useForm<AddressFormData>({
+    mode: "onBlur",
+    reValidateMode: "onChange",
+    defaultValues: {
+      first_name: "",
+      last_name: "",
+      email: "",
+      phone: "",
+      address: ["", ""], // 数组格式
+      country: "",
+      state: "",
+      city: "",
+      postcode: "",
+      default_address: 0,
+    },
+  });
+
+  const {
+    reset,
+    register,
+    handleSubmit,
+    formState: { errors },
+  } = addressForm;
+  useEffect(() => {
+    if (!id) return;
+    const load = async () => {
+      const resp = await clientFetch(`/api/customer/token/address?id=${id}`);
+      console.log("【完整接口响应】", resp);
+
+      // 自动兼容后端 99% 的返回格式
+      const formData = resp.data?.data || resp.data || {};
+      console.log("✅ 回填数据:", formData);
+
+      reset(formData);
+    };
+    load();
+  }, [id, reset]);
+  return (
+    <div className="w-full h-full">
+      <div className="bg-[#fff] pr-2.5 pl-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
+        <Link
+          href={"/customer/address"}
+          // onClick={() => window.history.back()}
+          className="absolute left-2.5 text-2xl inline-block top-1/2  -translate-y-1/2"
+        >
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="24"
+            height="24"
+            viewBox="0 0 24 24"
+            fill="none"
+          >
+            <path
+              stroke="rgba(0, 0, 0, 1)"
+              stroke-width="1.2"
+              stroke-linejoin="round"
+              stroke-linecap="round"
+              d="M15 18L9 12L15 6"
+            ></path>
+          </svg>
+        </Link>
+        <span className="vipReturnTitles">ADDRESS EDIT</span>
+      </div>
+      <div className="max-w-md mx-auto bg-[#f8f8f8]">
+        <FormProvider {...addressForm}>
+          <form
+            className="overflow-hidden"
+            onSubmit={handleSubmit(addressFormOnSubmit)}
+          >
+            <div className="bg-[#f8f8f8] p-[20px_10px]">
+              {/* 姓名 */}
+              <div className="text-start">*Name</div>
+              <div className="flex justify-between items-center gap-3">
+                {/* firstname */}
+                <input
+                  type="text"
+                  placeholder="firstname"
+                  {...register("first_name", {
+                    required: "First name is required",
+                  })}
+                  className="w-43 h-11 rounded-sm border border-solid border-[rgba(102,102,102,1)] indent-4 text-[#a6a6a6] text-base leading-11 font-normal"
+                />
+                {/* lastname */}
+                <input
+                  type="text"
+                  placeholder="last_name"
+                  {...register("last_name", {
+                    required: "Last name is required",
+                  })}
+                  className="w-43 h-11 rounded-sm border border-solid border-[rgba(102,102,102,1)] indent-4 text-[#a6a6a6] text-base leading-11 font-normal box-[unset]"
+                />
+              </div>
+              {errors.first_name && (
+                <p className="text-red-500 text-sm mt-1">
+                  {errors.first_name.message}
+                </p>
+              )}
+              {errors.last_name && (
+                <p className="text-red-500 text-sm mt-1">
+                  {errors.last_name.message}
+                </p>
+              )}
+
+              {/* 手机号 */}
+              <CustomerAddressPhone />
+              <Controller
+                control={addressForm.control}
+                name="country"
+                rules={{ required: "Country is required" }}
+                render={({ 
+                  // field
+
+                 }) => {
+                  return (
+                    <Select
+                      placeholder="Country/Region"
+                      {...register("country", {
+                        required: "Country/Region field is required",
+                      })}
+                      options={countriesOptions}
+                      error={errors.country?.message as string}
+                    />
+                  );
+                }}
+              />
+              {/* <input {...register("country")} /> */}
+              {/* 邮箱 */}
+              <div className="mt-4">
+                <label>Email Address*</label>
+                <input
+                  type="email"
+                  placeholder="Email"
+                  {...register("email", {
+                    required: "Email is required",
+                    pattern: {
+                      value: /^\S+@\S+\.\S+$/,
+                      message: "Invalid email",
+                    },
+                  })}
+                  className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+                />
+                {errors.email && (
+                  <p className="text-red-500 text-sm mt-1">
+                    {errors.email.message}
+                  </p>
+                )}
+              </div>
+
+              {/* 邮编 */}
+              <div className="mt-4">
+                <label>Zip/Postal Code</label>
+                <input
+                  type="text"
+                  placeholder="Postcode"
+                  {...register("postcode")}
+                  className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+                />
+              </div>
+
+              {/* 省/州 */}
+              <div className="mt-4">
+                <label>State / Province</label>
+                <input
+                  type="text"
+                  placeholder="State"
+                  {...register("state")}
+                  className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+                />
+              </div>
+
+              {/* 城市 */}
+              <div className="mt-4">
+                <label>City</label>
+                <input
+                  type="text"
+                  placeholder="City"
+                  {...register("city")}
+                  className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+                />
+              </div>
+
+              {/* 默认地址 */}
+              <label className="flex items-center gap-2 cursor-pointer mt-4">
+                <input
+                  type="checkbox"
+                  {...register("default_address", { valueAsNumber: true })}
+                  className="w-5 h-5"
+                />
+                <span>Use as my default address</span>
+              </label>
+
+              {/* 提交按钮 */}
+              <button
+                className="w-full h-11 leading-11 bg-[#1bbc9b] text-white text-center mt-6 rounded"
+                type="button"
+                onClick={() => {
+                  setActionType("save");
+                  document.getElementById("realSubmit")?.click();
+                }}
+              >
+                Save Address
+              </button>
+              <button
+                className="w-full h-11 leading-11 bg-[#1bbc9b] text-white text-center mt-6 rounded"
+                type="button"
+                onClick={() => {
+                  if (!confirm("确定删除该地址?")) return;
+                  setActionType("delete");
+                  document.getElementById("realSubmit")?.click();
+                }}
+              >
+                Delete Address
+              </button>
+              <button id="realSubmit" type="submit" hidden />
+            </div>
+          </form>
+        </FormProvider>
+      </div>
+    </div>
+  );
+};
+export default CustomerAddressEditPage;

+ 99 - 0
src/app/(public)/customer/address/new/_components/CustomerAddressPhone.tsx

@@ -0,0 +1,99 @@
+"use client";
+import {
+  formatFinalPhoneValue,
+  getPhoneCodeBycountryCode,
+} from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
+
+import {  useEffect, useState } from "react";
+// import { useQuery } from "@apollo/client/react";
+import { useFormContext, Controller, useWatch } from "react-hook-form";
+import { IS_VALID_FULL_PHONE, IS_VALID_PHONECODE } from "@utils/constants";
+// import { GET_COUNTRY_STATES } from "@/graphql";
+import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
+
+export default function CustomerAddressPhone() {
+  const {
+    getValues,
+    setValue,
+    formState: {  },
+    control,
+  } = useFormContext();
+
+  // 🔴 修复1:监听父表单的 country 字段(原 shippingCountry 是错误字段)
+  const selectedCountryCode = useWatch({
+    control,
+    name: "country",
+  });
+
+  // 是否用户手动选择过区号
+  const [hasUserSelectedPhoneCode, setHasUserSelectedPhoneCode] =
+    useState(false);
+
+  // 国家切换 → 自动同步电话区号(兼容 reset 回填)
+  useEffect(() => {
+    const currentPhone = getValues("phone");
+    const regPhoneCode = IS_VALID_PHONECODE;
+
+    if (currentPhone && !hasUserSelectedPhoneCode) {
+      const newPhoneCode = getPhoneCodeBycountryCode(selectedCountryCode);
+      const codeMatch = regPhoneCode.exec(currentPhone);
+      let purePhone = currentPhone;
+
+      // 剥离已有区号
+      if (codeMatch) {
+        purePhone = currentPhone.replace(codeMatch[0], "");
+      }
+
+      // 新区号和原有区号不一致才更新
+      if (codeMatch && newPhoneCode !== codeMatch[0].trim()) {
+        setValue("phone", formatFinalPhoneValue(newPhoneCode, purePhone));
+      }
+    }
+  }, [selectedCountryCode, hasUserSelectedPhoneCode, getValues, setValue]);
+
+  // const { data: statesData, loading: statesLoading } = useQuery(
+  //   GET_COUNTRY_STATES,
+  //   {
+  //     variables: {},
+  //     skip: !selectedCountryCode,
+  //   },
+  // );
+
+  return (
+    <div className="w-full mt-4">
+      <label className="text-ly-12 block mb-4 font-semibold">
+        Phone number *
+      </label>
+      {/* ✅ Controller 本身写法没问题,保留即可,字段 name="phone" 和父表单一致 */}
+      <Controller
+        name="phone"
+        control={control}
+        rules={{
+          required: "Phone Number is required",
+          pattern: {
+            value: IS_VALID_FULL_PHONE,
+            message: "Invalid Phone Number",
+          },
+        }}
+        render={({ field, fieldState }) => {
+          return (
+            <PhoneNumberInput
+              placeholder="Please Input Your Phone Number"
+              value={field.value}
+              onChange={field.onChange}
+              onBlur={field.onBlur}
+              name={field.name}
+              ref={field.ref}
+              error={fieldState.error?.message}
+              countryCode={selectedCountryCode}
+              hasUserSelectedPhoneCode={hasUserSelectedPhoneCode}
+              onHasUserSelectedPhoneCode={() =>
+                setHasUserSelectedPhoneCode(true)
+              }
+            />
+          );
+        }}
+      />
+    </div>
+  );
+}

+ 221 - 0
src/app/(public)/customer/address/new/_components/NewAddressDefault.tsx

@@ -0,0 +1,221 @@
+"use client";
+import { useForm, FormProvider } from "react-hook-form";
+import { useMemo } from "react";
+import CustomerAddressPhone from "./CustomerAddressPhone";
+import Select from "@/components/theme/ui/Select";
+import { clientFetch } from "@/lib/restApiClient";
+import { useRouter } from "next/navigation";
+// 定义表单类型(严格对齐你要的字段)
+interface AddressFormData {
+  first_name: string;
+  last_name: string;
+  email: string;
+  phone: string;
+  address: string[];
+  country: string;
+  state: string;
+  city: string;
+  postcode: string;
+  default_address: number;
+}
+
+export default function NewAddressDefault({ countries }: { countries: any[] }) {
+  // 国家选项
+  const countriesOptions = useMemo(() => {
+    return countries.map((country) => ({
+      value: country.code,
+      label: country.name,
+      id: String(country.id),
+    }));
+  }, [countries]);
+   const router = useRouter();
+  // 提交地址到接口
+  const AddAdress = async (formData: AddressFormData) => {
+    const res = await clientFetch("/api/customer/token/address", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(formData),
+    });
+    const json = await res;
+    console.log("提交成功,返回结果:", json);
+    if(json?.status== 201){
+        router.push("/customer/address/");
+    }else{
+      alert("error");
+    }
+    return json;
+  };
+
+  // 表单提交
+  const addressFormOnSubmit = async (formData: AddressFormData) => {
+    console.log("最终提交给后端的数据:", formData);
+    await AddAdress(formData);
+  };
+
+  // 表单实例(严格对齐你需要的字段)
+  const addressForm = useForm<AddressFormData>({
+    mode: "onBlur",
+    reValidateMode: "onChange",
+    defaultValues: {
+      first_name: "",
+      last_name: "",
+      email: "",
+      phone: "",
+      address: ["", ""], // 数组格式
+      country: "",
+      state: "",
+      city: "",
+      postcode: "",
+      default_address: 0,
+    },
+  });
+
+  const {
+    register,
+    handleSubmit,
+    formState: { errors },
+  } = addressForm;
+  return (
+    <div className="w-full h-full">
+      <FormProvider {...addressForm}>
+        <form
+          className="overflow-hidden"
+          onSubmit={handleSubmit(addressFormOnSubmit)}
+        >
+          {/* 头部 */}
+          <div className="bg-[#fff] px-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
+            <div
+              onClick={() => window.history.back()}
+              className="absolute left-2.5 text-2xl inline-block top-1/2 -translate-y-1/2"
+            >
+              <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)"
+                  strokeWidth="1.2"
+                  strokeLinejoin="round"
+                  strokeLinecap="round"
+                  d="M15 18L9 12L15 6"
+                ></path>
+              </svg>
+            </div>
+            <span>Add New Address</span>
+          </div>
+
+          <div className="bg-[#f8f8f8] p-[20px_10px]">
+            {/* 姓名 */}
+             <div className="text-start">*Name</div>
+            <div className="flex justify-between items-center gap-3">
+              {/* firstname */}
+              <input
+                type="text"
+                placeholder="firstname"
+                {...register("first_name", {
+                  required: "First name is required",
+                })}
+                className="w-43 h-11 rounded-sm border border-solid border-[rgba(102,102,102,1)] indent-4 text-[#a6a6a6] text-base leading-11 font-normal"
+              />
+              {/* lastname */}
+              <input
+                type="text"
+                placeholder="last_name"
+                {...register("last_name", {
+                  required: "Last name is required",
+                })}
+                className="w-43 h-11 rounded-sm border border-solid border-[rgba(102,102,102,1)] indent-4 text-[#a6a6a6] text-base leading-11 font-normal box-[unset]"
+              />
+            </div>
+            {errors.first_name && <p className="text-red-500 text-sm mt-1">{errors.first_name.message}</p>}
+            {errors.last_name && <p className="text-red-500 text-sm mt-1">{errors.last_name.message}</p>}
+
+            {/* 手机号 */}
+            <CustomerAddressPhone />
+
+            {/* 国家 */}
+            <div className="mt-4">
+              <Select
+                placeholder="Country/Region"
+                {...register("country", { required: "Country is required" })}
+                options={countriesOptions}
+                error={errors.country?.message as string}
+              />
+            </div>
+
+            {/* 邮箱 */}
+            <div className="mt-4">
+              <label>Email Address*</label>
+              <input
+                type="email"
+                placeholder="Email"
+                {...register("email", {
+                  required: "Email is required",
+                  pattern: { value: /^\S+@\S+\.\S+$/, message: "Invalid email" },
+                })}
+                className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+              />
+              {errors.email && <p className="text-red-500 text-sm mt-1">{errors.email.message}</p>}
+            </div>
+
+            {/* 邮编 */}
+            <div className="mt-4">
+              <label>Zip/Postal Code</label>
+              <input
+                type="text"
+                placeholder="Postcode"
+                {...register("postcode")}
+                className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+              />
+            </div>
+
+            {/* 省/州 */}
+            <div className="mt-4">
+              <label>State / Province</label>
+              <input
+                type="text"
+                placeholder="State"
+                {...register("state")}
+                className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+              />
+            </div>
+
+            {/* 城市 */}
+            <div className="mt-4">
+              <label>City</label>
+              <input
+                type="text"
+                placeholder="City"
+                {...register("city")}
+                className="w-full h-11 rounded-sm border border-[rgba(102,102,102,1)] indent-4 text-base mt-1"
+              />
+            </div>
+
+            {/* 默认地址 */}
+            <label className="flex items-center gap-2 cursor-pointer mt-4">
+              <input
+                type="checkbox"
+                {...register("default_address", { valueAsNumber: true })}
+                className="w-5 h-5"
+              />
+              <span>Use as my default address</span>
+            </label>
+
+            {/* 提交按钮 */}
+            <button
+              className="w-full h-11 leading-11 bg-[#1bbc9b] text-white text-center mt-6 rounded"
+              type="submit"
+            >
+              Save Address
+            </button>
+          </div>
+        </form>
+      </FormProvider>
+    </div>
+  );
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1286 - 169
src/app/(public)/customer/address/new/page.tsx


+ 13 - 8
src/app/(public)/customer/address/page.tsx

@@ -1,8 +1,7 @@
-"use client";
 import React from "react";
-// import { useState } from "react";
-// import { useEffect } from "react";
-const CustomerAddressPage = () => {
+import Link from "next/link";
+import  DefaultAddress from "./_components/DefaultAddress";
+const CustomerAddressPage = async () => {
   // 模拟地址数据(可根据实际需求替换)
   const addressData = {
     billing: {
@@ -22,11 +21,12 @@ const CustomerAddressPage = () => {
       phone: "2123165641",
     },
   };
+ 
   return (
     <div className="w-full h-full">
       <div className="bg-[#fff] pr-2.5 pl-2.5 w-full text-center text-base h-11 leading-11 font-semibold relative text-[#0b0b0b]">
-        <div
-          onClick={() => window.history.back()}
+        <Link href={"/customer/account"}
+          // onClick={() => window.history.back()}
           className="absolute left-2.5 text-2xl inline-block top-1/2  -translate-y-1/2"
         >
           <svg
@@ -44,10 +44,11 @@ const CustomerAddressPage = () => {
               d="M15 18L9 12L15 6"
             ></path>
           </svg>
-        </div>
+        </Link>
         <span className="vipReturnTitles">SHIPPING ADDRESS</span>
       </div>
       <div className="max-w-md mx-auto p-4 bg-[#f8f8f8]">
+        <DefaultAddress  />
         {/* 账单地址模块 */}
         <div className="mb-6">
           <h2 className="text-xl font-bold uppercase mb-3">
@@ -70,7 +71,7 @@ const CustomerAddressPage = () => {
             </div>
           </div>
         </div>
-
+        
         {/* 收货地址模块 */}
         <div>
           <h2 className="text-xl font-bold uppercase mb-3">
@@ -93,7 +94,11 @@ const CustomerAddressPage = () => {
             </div>
           </div>
         </div>
+        <Link href={"/customer/address/new"} className="text-center bg-[#000] block text-white mt-5  width-full height-11 leading-11 text-lg" >
+          + New Address
+        </Link>
       </div>
+    
     </div>
   );
 };

+ 388 - 0
src/app/(public)/customer/order/history/_components/OrderHistory.tsx

@@ -0,0 +1,388 @@
+"use client";
+import { useEffect, useState, useRef, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { clientFetch } from "@/lib/restApiClient";
+
+// 模拟订单数据
+const mockOrders = [
+  {
+    id: 600217097,
+    date: "3/4/2026",
+    status: "Processing",
+    items: [
+      {
+        name: "Highlight Wigs For Women Headband Wigs with Blonde Highlights",
+        specs: "10, #022, Straight, 5 Free Headbands",
+        price: "88 points + $154.05x1",
+        image: "",
+      },
+    ],
+    progress: "Preparing Order",
+    total: "88 Points + $158.67",
+  },
+  {
+    id: 100055662,
+    date: "3/3/2026",
+    status: "Processing",
+    items: [
+      {
+        name: "Alipearl Lace Front Wig 180% Density Body Wave Human Hair Wigs Pre...",
+        specs: "10+180%, Medium, No Adjustable Band",
+        price: "$128.82x1",
+        image: "",
+      },
+    ],
+    progress: "Preparing Order",
+    total: "$132.68",
+  },
+  {
+    id: 600217055,
+    date: "3/3/2026",
+    status: "Processing",
+    items: [
+      {
+        name: "3 Bundles Deep Wave Hair With 4*4 Lace Closure Alipearl Brazilian Hair",
+        specs: "8 8 8, Free Part, 8",
+        price: "$142.14x1",
+        image: "",
+      },
+      {
+        name: "Princess Braids Wig Blonde Barbie Pre-Styled Highlight Wig 13×4 Front Wig",
+        specs: "12",
+        price: "$279.32x1",
+        image:
+          "https://cdn.alipearlhair.com/media/catalog/product/cache/6/thumbnail/750x1000/9df78eab33525d08d6e5fb8d27136e95/b/o/body_wave_lace_front_wig_2.jpg", // 替换为实际图片路径
+      },
+      {
+        name: "Deep Wave Wig 100 Human Hair Swiss Lace Wig Lace Front Wig",
+        specs: "10, Medium, 100%",
+        price: "$252.00x1",
+        image: "",
+      },
+      {
+        name: "Braids Wig Blonde Barbie Pre-Styled Highlight Wig 13×4 Front Wig",
+        specs: "12",
+        price: "6000 points + $279.32x1",
+        image:
+          "https://cdn.alipearlhair.com/media/catalog/product/cache/6/thumbnail/750x1000/9df78eab33525d08d6e5fb8d27136e95/b/o/body_wave_lace_front_wig_2.jpg", // 替换为实际图片路径
+      },
+      {
+        name: "random free wigs-2",
+        specs: "",
+        price: "$0.00x1",
+        image: "",
+      },
+    ],
+    progress: "Preparing Order",
+    discount: "-$80.00",
+    total: "$872.78",
+  },
+];
+
+// 标签列表
+const tabs = [
+  { key: "all", label: "All" },
+  { key: "processing", label: "Processing" },
+  { key: "complete", label: "Complete" },
+  { key: "pending_payment", label: "Payment Review" }, // 补充模拟标签,测试滚动
+];
+
+export default function OrderHistory() {
+  const [loadMoreLoading, setLoadMoreLoading] = useState(false);
+  const [page, setPage] = useState(1);
+  const [total, setTotal] = useState(0);
+  const PAGE_SIZE = 10;
+  // 是否还有下一页
+  const hasMore = page * PAGE_SIZE < total;
+  //  防抖定时器ref
+  const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
+  const DEBOUNCE_DELAY = 500; // 防抖延迟毫秒
+  const sentinelRef = useRef<HTMLDivElement>(null);
+  const listWrapRef = useRef<HTMLDivElement>(null);
+  const router = useRouter();
+  const [activeTab, setActiveTab] = useState("all");
+  const [filteredOrders, setFilteredOrders] = useState<any[]>([]);
+  const lastHashRef = useRef<string>("");
+  const fetchOrders = async (tabKey: string, isLoadMore = false) => {
+    if (loadMoreLoading) return;
+    setLoadMoreLoading(true);
+    console.log("请求接口参数:", tabKey, page, isLoadMore);
+    try {
+      const resp = await clientFetch(
+        `/api/customer/orders?page=${page}&limit=${PAGE_SIZE}&status=${tabKey}`,
+      );
+      let records = resp.data.records ?? [];
+      setTotal(resp.data.total ?? 0);
+      // 模拟数据后面换成真正的接口数据
+      setTotal(100);
+      records = mockOrders; //
+      if (isLoadMore) {
+        setFilteredOrders((prev) => [...prev, ...records]);
+      } else {
+        setFilteredOrders(records);
+        setPage(1);
+        listWrapRef.current?.scrollTo({ top: 0 });
+      }
+    } catch (err) {
+      console.error("请求失败", err);
+    } finally {
+      setLoadMoreLoading(false);
+    }
+  };
+
+  const updateTabByHash = useCallback(
+    (hashKey: string) => {
+      console.log("执行切换tab逻辑", hashKey);
+      const validTab = tabs.find((t) => t.key === hashKey.toLowerCase());
+      const targetKey = validTab ? validTab.key : "all";
+      // setActiveTab(targetKey);
+      fetchOrders(targetKey, false);
+    },
+    [tabs],
+  );
+  useEffect(() => {
+    const getHash = () =>
+      window.location.hash.replace("#", "").toLowerCase() || "all";
+    const hash = getHash();
+    if (lastHashRef.current !== hash) {
+      lastHashRef.current = hash;
+      updateTabByHash(hash);
+    }
+  }, [updateTabByHash]);
+
+  // 专门用来销毁定时器,防止内存泄漏,依赖空数组只执行一次挂载/卸载
+  useEffect(() => {
+    return () => {
+      if (debounceTimerRef.current) {
+        clearTimeout(debounceTimerRef.current);
+      }
+    };
+  }, []);
+  useEffect(() => {
+    const sentinel = sentinelRef.current;
+    const wrap = listWrapRef.current;
+    if (!sentinel || !wrap) return;
+
+    const observer = new IntersectionObserver(
+      (entries) => {
+        const entry = entries[0];
+        if (entry.isIntersecting && hasMore && !loadMoreLoading) {
+          const nextPage = page + 1;
+          setPage(nextPage);
+          fetchOrders(activeTab, true);
+        }
+      },
+      { root: wrap, rootMargin: "200px 0px 0px 0px" },
+    );
+
+    observer.observe(sentinel);
+    return () => observer.disconnect();
+  }, [activeTab, hasMore, loadMoreLoading, page]);
+  // 点击标签切换
+  const handleTabClick = (tabKey: string) => {
+    // 点击立刻更新激活态,按钮样式实时切换
+    setActiveTab(tabKey);
+
+    // 清除上一次防抖任务
+    if (debounceTimerRef.current) {
+      clearTimeout(debounceTimerRef.current);
+    }
+
+    // 防抖:延迟更新URL + 请求接口
+    debounceTimerRef.current = setTimeout(() => {
+      const hash = tabKey.toLowerCase();
+      if (lastHashRef.current !== hash) {
+        lastHashRef.current = hash;
+        router.push(`#${tabKey}`, { scroll: false });
+        updateTabByHash(hash); // 仅负责调接口
+      }
+    }, DEBOUNCE_DELAY);
+  };
+
+  return (
+    <>
+      <div className="min-h-screen bg-gray-50">
+        {/* 顶部返回栏 + 标题 */}
+        <Link
+          href={"/customer/account"}
+          className="sticky top-0 z-20 bg-white px-4 py-3 border-b flex justify-between items-center w-full"
+        >
+          <button className="text-black text-xl mr-4">
+            <svg
+              className="w-6 h-6"
+              viewBox="0 0 1024 1024"
+              version="1.1"
+              xmlns="http://www.w3.org/2000/svg"
+              p-id="111033"
+              width="200"
+              height="200"
+            >
+              <path
+                d="M391.466667 512l339.882666-339.882667a46.592 46.592 0 1 0-65.877333-65.877333L292.693333 479.061333a46.592 46.592 0 0 0 0 65.877334l372.821334 372.821333a46.592 46.592 0 1 0 65.877333-65.877333L391.466667 512z"
+                p-id="111034"
+              ></path>
+            </svg>
+          </button>
+          <h1 className="text-xl font-bold mx-auto">Orders</h1>
+          <div className="w-6"></div>
+        </Link>
+
+        {/* 标签栏(固定顶部 + 横向滚动) */}
+        <div className="sticky top-14 z-10 bg-white px-4 py-2 border-b">
+          <div className="flex items-center space-x-6 overflow-x-auto pb-1 scrollbar-hide">
+            {tabs.map((tab) => (
+              <button
+                key={tab.key}
+                onClick={() => handleTabClick(tab.key)}
+                className={`whitespace-nowrap py-1 px-3 rounded-full text-sm font-medium transition-colors
+                ${
+                  activeTab === tab.key
+                    ? "bg-black text-white"
+                    : "text-gray-600 hover:bg-gray-100"
+                }`}
+              >
+                {tab.label}
+              </button>
+            ))}
+          </div>
+        </div>
+
+        {/* 订单列表 */}
+        <div className="px-4 py-3  h-[72vh] overflow-y-auto" ref={listWrapRef}>
+          {filteredOrders.length === 0 && !loadMoreLoading ? (
+            <div className="text-center py-8 text-gray-500">
+              No orders found
+            </div>
+          ) : (
+            filteredOrders.map((order) => (
+              <div
+                key={order.id}
+                className="bg-white rounded-lg p-4 mb-4 border"
+              >
+                {/* 订单状态 + 编号 + 日期 */}
+                <div className="mb-3">
+                  <p className="font-medium text-gray-800">• {order.status}</p>
+                  <div className="flex justify-between text-sm text-gray-600 mt-1">
+                    <span>Order Number: {order.id}</span>
+                    <span>Date: {order.date}</span>
+                  </div>
+                </div>
+
+                {/* 订单商品列表 */}
+                <div className="mb-4 space-y-3">
+                  {order.items.map((item:any, idx:any) => (
+                    <div key={idx} className="flex gap-3">
+                      {/* 商品图片(有则显示,无则占位) */}
+                      {item.image ? (
+                        <img
+                          src={item.image}
+                          alt={item.name}
+                          className="w-22.5 h-22.5 object-cover rounded"
+                        />
+                      ) : (
+                        <div className="w-22.5 h-22.5 bg-gray-100 rounded"></div>
+                      )}
+                      {/* 商品信息 */}
+                      <div className="flex-1">
+                        <p className="text-sm text-gray-800 line-clamp-2">
+                          {item.name}
+                        </p>
+                        {item.specs && (
+                          <p className="text-xs text-gray-500 mt-1">
+                            {item.specs}
+                          </p>
+                        )}
+                        <p className="text-xs text-gray-600 mt-1">
+                          {item.price}
+                        </p>
+                      </div>
+                    </div>
+                  ))}
+                </div>
+
+                {/* 订单进度条 */}
+                <Link
+                  href={`/customer/order/track/order_id/${order.id}`}
+                  className="mb-4  bg-gray-100 rounded-full h-8 flex items-center px-2"
+                >
+                  <span className="ml-2 text-xs text-gray-700 flex-1 flex justify-between">
+                    {order.progress}
+                    <span>
+                      <svg
+                        className="w-4 h-4"
+                        viewBox="0 0 1024 1024"
+                        version="1.1"
+                        xmlns="http://www.w3.org/2000/svg"
+                        p-id="3784"
+                        width="200"
+                        height="200"
+                      >
+                        <path
+                          d="M761.056 532.128c0.512-0.992 1.344-1.824 1.792-2.848 8.8-18.304 5.92-40.704-9.664-55.424L399.936 139.744a48 48 0 0 0-65.984 69.76l316.96 299.84L335.2 813.632a48 48 0 0 0 66.624 69.12l350.048-337.376c0.672-0.672 0.928-1.6 1.6-2.304 0.512-0.48 1.056-0.832 1.568-1.344 2.72-2.848 4.16-6.336 6.016-9.6z"
+                          p-id="3785"
+                        ></path>
+                      </svg>
+                    </span>
+                  </span>
+                </Link>
+
+                {/* 折扣 + 总计 + 操作按钮 */}
+                <div className="text-right text-sm">
+                  {order.discount && (
+                    <p className="text-red-500 flex items-center justify-between">
+                      <span>Discount</span> <span>{order.discount}</span>
+                    </p>
+                  )}
+                  <p className="font-bold mt-1 flex items-center justify-between">
+                    <span>
+                      Order Total ({order.items.length} item
+                      {order.items.length > 1 ? "s" : ""})
+                    </span>{" "}
+                    <span>{order.total}</span>
+                  </p>
+
+                  <div className="flex justify-between mt-3">
+                    <Link
+                      href={`/customer/order/view/order_id/${order.id}`}
+                      className="text-black-600 text-sm underline"
+                    >
+                      View Order
+                    </Link>
+                    <Link
+                      href={`/customer/order/track/order_id/${order.id}`}
+                      // state={{
+                      //   addressData: {
+                      //     /* 你的地址对象 */
+                      //   },
+                      // }}
+                      className="border border-gray-300 rounded-full px-4 py-1 text-sm text-gray-800 hover:bg-gray-50"
+                    >
+                      Track Order
+                    </Link>
+                  </div>
+                </div>
+              </div>
+            ))
+          )}
+          {/* 无限滚动哨兵:监听触底 */}
+          <div ref={sentinelRef} className="h-1"></div>
+
+          {/* 加载更多 loading */}
+          {loadMoreLoading && (
+            <div className="text-center py-4 text-gray-500">
+              Loading more orders...
+            </div>
+          )}
+
+          {/* 无下一页提示 */}
+          {!hasMore && filteredOrders.length > 0 && !loadMoreLoading && (
+            <div className="text-center py-4 text-gray-400 text-sm">
+              No more orders
+            </div>
+          )}
+        </div>
+      </div>
+    </>
+  );
+}

+ 11 - 0
src/app/(public)/customer/order/history/page.tsx

@@ -0,0 +1,11 @@
+import OrderHistory from "./_components/OrderHistory";
+import { Suspense } from "react";
+export default function OderIndex() {
+  return (
+    <>
+      <Suspense fallback={<div className="p-4">Loading order tabs...</div>}>
+        <OrderHistory />
+      </Suspense>
+    </>
+  );
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 142 - 0
src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx


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

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

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

@@ -0,0 +1,74 @@
+import OrderHeader from "../../../view/order_id/[id]/_components/OrderHeader";
+import AccountSwiper from "@components/customer/accountSwiper";
+import TrackDetail from "./_components/TrackDetail";
+import { restApiFetch } from "@utils/bagisto/index";
+import type { OrderData,OrderApiResponse } from "@/types/api/customer/order/vieworder";
+type Params = {
+  id: string;
+};
+interface OrderAddress {
+  first_name: string;
+  last_name: string;
+  phone: string;
+  address1: string | null;
+  address2: string | null;
+  city: string;
+  state: string;
+  country: string;
+  postcode: string;
+  company: string;
+}
+export interface AddressData {
+  name: string;
+  line1: string;
+  line2: string;
+}
+
+function formatOrderAddress(addr: OrderAddress): AddressData {
+  // 拼接姓名
+  const fullName = `${addr.first_name} ${addr.last_name}`;
+
+  // line1:街道 + 城市 + 州
+  const addrParts: string[] = [];
+  if (addr.address1) addrParts.push(addr.address1);
+  if (addr.address2) addrParts.push(addr.address2);
+  addrParts.push(`${addr.city}, ${addr.state}`);
+  const line1 = addrParts.join(", ");
+
+  // line2:邮编 + 国家
+  const line2 = `${addr.postcode}, ${addr.country}`;
+
+  return {
+    name: fullName,
+    line1,
+    line2,
+  };
+}
+export default async function TrackOrderDetail({ params }: { params: Params }) {
+  // 拿到动态订单ID:211600
+  // 解包拿到真实订单ID
+  const realParams = await params;
+  let orderId = realParams.id;
+  orderId = "9";
+  console.log("orderId:", orderId);
+
+  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  const res = await restApiFetch<OrderApiResponse>({
+    api: `/customer/orders/${orderId}`,
+    method: "GET",
+  });
+  const orderData: OrderData = res.body.data;
+  const shippingAddr: AddressData = formatOrderAddress(orderData.shipping_address);
+  console.log("父组件 orderData:", orderData, !!orderData);
+  return (
+    <div className="bg-gray-50 min-h-screen">
+      {/* 顶部固定导航栏(客户端组件:返回按钮) */}
+      <OrderHeader title="Track Order" />
+
+      <div className="px-5 py-4">
+        <TrackDetail address={shippingAddr} id={orderId} />
+      </div>
+      <AccountSwiper />
+    </div>
+  );
+}

+ 30 - 0
src/app/(public)/customer/order/view/order_id/[id]/_components/OrderHeader.tsx

@@ -0,0 +1,30 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+
+export default function OrderHeader({ title }: { title: string }) {
+  const router = useRouter();
+  return (
+    <div className="sticky top-0 z-50 bg-white px-5 py-4 border-b">
+      <div className="flex items-center w-full">
+        <button onClick={() => router.back()} className="text-xl">
+          <svg
+            className="w-6 h-6"
+            viewBox="0 0 1024 1024"
+            version="1.1"
+            xmlns="http://www.w3.org/2000/svg"
+            p-id="111033"
+            width="200"
+            height="200"
+          >
+            <path
+              d="M391.466667 512l339.882666-339.882667a46.592 46.592 0 1 0-65.877333-65.877333L292.693333 479.061333a46.592 46.592 0 0 0 0 65.877334l372.821334 372.821333a46.592 46.592 0 1 0 65.877333-65.877333L391.466667 512z"
+              p-id="111034"
+            ></path>
+          </svg>
+        </button>
+        <h1 className="flex-grow text-center text-lg font-bold">{title}</h1>
+      </div>
+    </div>
+  );
+}

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

@@ -0,0 +1,51 @@
+// OrderInfoBlock.tsx
+import type { OrderData } from "@/types/api/customer/order/vieworder";
+
+interface Props {
+  orderData: OrderData; // 直接传入完整订单对象
+}
+
+export default function OrderInfoBlock({ orderData }: Props) {
+  console.log("orderData:::",orderData);
+  
+  // 拼接收货信息
+  const shipToText = `${orderData.shipping_address.first_name} ${orderData.shipping_address.last_name}, 
+Phone: ${orderData.shipping_address.phone}, 
+${orderData.shipping_address.city}, ${orderData.shipping_address.state}, ${orderData.shipping_address.country} ${orderData.shipping_address.postcode}`;
+
+  return (
+    <>
+      <h2 className="text-xl font-bold mb-6 border-b pb-3">Order Information</h2>
+
+      {/* 两列网格布局 */}
+      <div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-5">
+        <div className="font-medium text-gray-700">Ship To:</div>
+        <div>{shipToText}</div>
+
+        <div className="font-medium text-gray-700">Order Number:</div>
+        <div>{orderData.increment_id}</div>
+
+        <div className="font-medium text-gray-700">Status:</div>
+        <div>{orderData.status}</div>
+
+        <div className="font-medium text-gray-700">Order Date:</div>
+        <div>{orderData.created_at}</div>
+
+        <div className="font-medium text-gray-700">Customer Email:</div>
+        <div>{orderData.customer_email}</div>
+
+        <div className="font-medium text-gray-700">Shipping Method:</div>
+        <div>{orderData.shipping_title}</div>
+
+        <div className="font-medium text-gray-700">Payment Method:</div>
+        <div className="bg-pink-300 px-3 py-1 rounded w-fit">
+          {orderData.payment.method_title}
+        </div>
+
+        {/* 可选:显示总价信息 */}
+        <div className="font-medium text-gray-700">Total Amount:</div>
+        <div>{orderData.order_currency_code} {orderData.grand_total}</div>
+      </div>
+    </>
+  );
+}

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

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

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

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

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

@@ -0,0 +1,57 @@
+import OrderHeader from "./_components/OrderHeader";
+import OrderInfoBlock from "./_components/OrderInfoBlock";
+import ProductList from "./_components/ProductList";
+import PriceSummary from "./_components/PriceSummary";
+import {restApiFetch} from "@utils/bagisto/index";
+import type {  OrderData,OrderApiResponse } from "@/types/api/customer/order/vieworder";
+type Params = {
+  id: string;
+};
+export default async function OrderDetail({ params }: { params: Params }) {
+  // 解包拿到真实订单ID
+  const realParams = await params;
+  let orderId = realParams.id;
+  orderId = '9';
+  console.log("orderId:", orderId);
+
+  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  const res = await restApiFetch<OrderApiResponse>({
+    api:`/customer/orders/${orderId}`,
+    method:"GET",
+  }
+  );
+
+  const orderData: OrderData = res.body.data;
+  console.log("父组件 orderData:", orderData, !!orderData); 
+  return (
+    <div className="bg-gray-50 min-h-screen">
+      {/* 顶部固定导航栏(客户端组件:返回按钮) */}
+      <OrderHeader title="Order Information" />
+
+      <div className="px-5 py-4">
+        {/* 收货信息、支付信息区块 */}
+        <OrderInfoBlock orderData={orderData} />
+
+        {/* 分享按钮区域 */}
+        <div className="my-6">
+          <h3 className="text-lg font-bold mb-4">Share order:</h3>
+          <div className="flex gap-6">
+            <span>fb</span>
+            <span>X</span>
+            <span>pinterest</span>
+            <span>whatsapp</span>
+          </div>
+        </div>
+
+        {/* 商品列表:循环渲染,自动兼容1件或多件商品 */}
+            <ProductList
+          orderItems={orderData.items}
+          currencyCode={orderData.order_currency_code}
+        />
+
+        {/* 金额汇总 */}
+        <PriceSummary orderData={orderData} />
+      </div>
+    </div>
+  );
+}

+ 207 - 0
src/app/(public)/hair-care-tips/page.tsx

@@ -0,0 +1,207 @@
+import HeadBack from "@/components/head/HeadBack"
+export default function HairCareTips() {
+  return (
+    <>
+    <HeadBack />
+      <div className="min-h-screen bg-gray-50">
+        <div className="max-w-4xl mx-auto bg-white  shadow-sm p-8 sm:p-10">
+          <h1 className="text-2xl font-bold text-center text-gray-800 mb-10">
+            Human Hair Care Instructions
+          </h1>
+
+          <div className="space-y-8">
+        
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-2">
+                1. About Combing
+              </h2>
+              <p className="text-gray-700 leading-relaxed">
+                When Brushing Use The Appropriate Tools For Different Textures,
+                Start At The Ends Of Your Hair And Gently Work Your Way Up, In
+                This Way It Avoids Tangling And Pulling Damage. Always Brush In
+                A Downward Motion And Do Not Brush Harshly.
+              </p>
+            </div>
+
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-2">
+                2. About Washing
+              </h2>
+              <p className="text-gray-700 leading-relaxed">
+                Detangle Your Wig Gently By Using A Wide Tooth Comb. Curly Wigs
+                Should Be Untangled By Hands, No Combs Or Brushes.
+                <br />
+                Fill A Basin Or A Container With Cool Water And Mix In A Bit Of
+                Shampoo And Conditioner.
+                <br />
+                Immerse Your Wig And Swirl It Around Gently.
+                <br />
+                Let It Soak For About 5 Minutes.
+                <br />
+                Rinse Your Wig By Gently Running Cool Water Over It. Do Not Rub
+                As It Will Tangle Up The Wig.
+                <br />
+                Pat Dry With A Towel. Do Not Wring Or Rub, As Again, It Will
+                Mess Up The Wig.
+                <br />
+                Place Your Wig On A Wig Stand And Leave It To Air Dry.
+              </p>
+            </div>
+
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-2">
+                3. About Day & Night Maintenance
+              </h2>
+              <p className="text-gray-700 leading-relaxed">
+                We Highly Suggested The Hair Be Washed And Conditioned At Least
+                Twice A Week.
+                <br />
+                Please Use A Soft Fabric Or Satin Hair Scrunchie To Achieve A
+                High Ponytail In Order To Avoid Tangling And Dryness.
+                <br />
+                When Not Wearing Your Wig, Place Your Wig On A Wig Stand Or
+                Mannequin Head.
+                <br />
+                At Night, It Is Better To Tie Hair Up With A Satin Scrunchie Or
+                Into A Satin Bonnet, An Alternative Satin Pillowcase.
+              </p>
+            </div>
+
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-2">
+                4. About Moisturize
+              </h2>
+              <p className="text-gray-700 leading-relaxed">
+                At Least Once A Week .Go Through The Rows Of Your Braid Base And
+                Gently Apply Moisturizing Hair With Scalp Cream Or Argan Oil
+                (Eg, Mizani Cream, Coconut Oil, Shea Butter, Lotion)
+                <br />
+                It Is Advisable To Use The Products Containing The Herb In The
+                Case Where The Hair Begins To Become Dry And Yellow And Use The
+                Products With Special Effects Or Further Maintenance Which Are
+                Made For Protection&Cleaning In The Case Where The Hair
+                Bifurcates Or Even Becomes Invulnerable To Fragility.
+              </p>
+            </div>
+
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-2">
+                5. Straight Hair Care
+              </h2>
+              <p className="text-gray-700 leading-relaxed">
+                Straight Hair Is Easy To Manage. Use Small Amounts Of Hair Care
+                Products Is Suggested.
+                <br />
+                We Suggest Lightweight Serums For Straight Hair Products.
+                <br />
+                When Combing, A Wide Tooth Comb Is The Best Choice.
+                <br />
+                Hair Care Products That Contain Sulfate, Parabens, Or Alcohol
+                Will Dry Out The Air And Cause The Tangling,Please Do Not Choose
+                These Kinds Of Hair Care Products.
+              </p>
+            </div>
+
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-2">
+                6. Curly Hair Care
+              </h2>
+              <p className="text-gray-700 leading-relaxed">
+                Due To The Curl's Characteristics, Curly Hair Needs More
+                Hydration.
+                <br />
+                We Suggest Lightweight Serums And Leave-In Conditioners For
+                Curly Hair Products. These Care Products Will Help Keep Curly
+                Hair Moisturized And Tangle-Free. When Applying The Products, A
+                Quarter Size Of Each Product Is Enough To Keep Hair Defined, No
+                Need For More.
+                <br />
+                When Combing, A Wide Tooth Comb Is The Best Choice.
+                <br />
+                Please Avoid Using A Fine Tooth Comb For Curly Hair Since It
+                Will Cause Tangling And Shedding.
+                <br />
+                Hair Care Products That Contain Sulfate, Parabens, Or Alcohol
+                Will Dry Out The Air And Cause Tangling, Please Do Not Choose
+                These Kinds Of Hair Care Products.
+              </p>
+            </div>
+
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-2">
+                7. About Hot Tools
+              </h2>
+              <p className="text-gray-700 leading-relaxed">
+                Heat Is One Of The Biggest Enemy, The More You Use It, The
+                Shorter Your Hair Extension Will Last. We Don’t Suggest You Use
+                Hot Tools Too Often.
+                <br />
+                It Is Better To Use Heat Protectant Before Styling Your Wig And
+                We Do Suggest You Guys Flat Iron Your Wig In High Temperatures
+                With Caution. For Colored Hair, 350F Degrees Is The Suggested
+                Temperature; Flat Ironing Your Wig In A Temperature Above 400F
+                Degrees Is Not Suggested For Natural Black Hair Since It Will
+                Cause The Dryness Of The Wig. Meantime, Please Be Aware Of That
+                Be Sure To Leave The Hot Tools On Your Wig For No More Than 30
+                Seconds.
+                <br />
+                Tips: When Curling Or Straightening Your Wig With Hot Tools,
+                Please Clean Your Tools In Time To Avoid There Have Few Loose
+                Hair On The Tools. It Is Normal That There Have Loose Hair As
+                Well As You Clean Up The Hot Tools When Restyling.
+              </p>
+            </div>
+
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-2">
+                8. Notes
+              </h2>
+              <div className="text-gray-700 leading-relaxed space-y-1">
+                <p>
+                  (1) Oil Sheen, Oil Sheen Is Not Recommended For Blond Hair,
+                  Oil May Cause Weave To Ship Out.
+                </p>
+                <p>
+                  (2) Wear A Cap Or Keep Your Hair Out Of Water. When Swimming.
+                  Chlorine And Salt Water Can Cause The Hair To Tangle And Mat
+                  Up.
+                </p>
+                <p>
+                  (3) Even If It’s 100% Human Hair Made, It’s Not Advised To
+                  Perm, Color, Or Any Other Chemical Process To Your Own Hair
+                  Extensions. At Least, Not Perm Too Often.
+                </p>
+                <p>
+                  (4) It Is Okay To Bleach The Knots But It Is Better To Find
+                  The Professional Hair Stylist To Help. ( It Is Better To Use
+                  The Foundation Method Rather Than Bleaching The Knots Which
+                  Can Help The Hair Lasts Longer.)
+                </p>
+                <p>
+                  (5) As A General Rule It Is Easier To Darken The Hair Than To
+                  Lighten Hair. We Recommend To Dyeing Darker, Since It Is
+                  Difficult For The Original Color To Fade. Improper Dying May
+                  Ruin The Hair. It Is Better To Find The Professional Hair
+                  Stylist To Help Dye The Hair.
+                </p>
+                <p>
+                  (6) Keep Your Hair Clean. Hair Tangles When Dirt And Sweat
+                  Build Up, Ash It After Exercise, Swimming, Etc.
+                </p>
+                <p>
+                  (7) Use Good Shampoo ( Sulfate Free, Parabens Free, Or Alcohol
+                  Free ), Your Stylist Can Advise You What Product Is Best For
+                  Your Hair. When Lots Of Water Is Added At Once To Very Dry
+                  Hair, Hair Can Swell Up And Tangle.
+                </p>
+                <p>
+                  (8) When Removing, Please Consult Your Professional Stylist.
+                </p>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </>
+  );
+}

+ 688 - 0
src/app/(public)/help-faqs/_components/Faqs.tsx

@@ -0,0 +1,688 @@
+"use client";
+import { useState } from "react";
+import HeadBack from "@/components/head/HeadBack";
+type FaqItem = {
+  q: React.ReactNode;
+  a: React.ReactNode;
+};
+
+type FaqSection = {
+  title: string;
+  items: FaqItem[];
+};
+
+type FaqGroup = {
+  groupTitle: string;
+  sections: FaqSection[];
+};
+
+const faqData: FaqGroup[] = [
+  {
+    groupTitle: "Question Before Purchase",
+    sections: [
+      {
+        title: "1. About Order",
+        items: [
+          {
+            q: "How do I pay for an order?",
+            a: (
+              <div className="space-y-2">
+                <p>
+                  To pay for your order on www.asteriahair.com, follow the steps
+                  below:
+                </p>
+                <p>
+                  (1) Select the hair items you would like to order and click
+                  the "Buy Now" button.
+                </p>
+                <p>
+                  (2) Fill in the address information and choose the shipping
+                  method and payment method on the checkout page, then click the
+                  "Place Order" button.
+                </p>
+                <p>(3) Go to the payment platform to complete the payment.</p>
+              </div>
+            ),
+          },
+          {
+            q: "How do I use my coupon code? Can I use two coupon codes in one order?",
+            a: (
+              <div className="space-y-2">
+                <p>
+                  The purchasing process on our website is: Buy Now &gt Checkout
+                  &gt Confirm &gt Payment.
+                </p>
+                <p>
+                  When you are in the process of Checkout or Confirm, you can
+                  apply the coupon code if you have one, and please note that
+                  only one coupon code is allowed for one order.
+                </p>
+              </div>
+            ),
+          },
+          {
+            q: "I have bought something but seen it at a different price on your website.",
+            a: (
+              <p>
+                As an online business we act much faster in adjusting our prices
+                in response to trends, stock and demand from customers. It's our
+                general view not to refund the difference.
+              </p>
+            ),
+          },
+          {
+            q: "Is there any hidden charges to my order?",
+            a: (
+              <p>
+                Basically, the final amount you will pay is just the price of
+                the product plus shipping(if you choose a paid shipping).
+                Sometimes, because of the exchange rate of currency, when the
+                payment finally cleared through the bank, the total amount will
+                be a little different to the price of the product, but its only
+                less than 0.5% of the total price.
+              </p>
+            ),
+          },
+          {
+            q: "Do I have to pay the customs duties & taxes?",
+            a: (
+              <p>
+                Orders shipped outside of the United States may be subject to
+                import taxes, customs duties and fees levied by the destination
+                country. The recipient of an international shipment may be
+                subject to such import taxes, customs duties and fees.
+                Additional charges for customs clearance must be fulfilled by
+                the recipient. Customs policies vary widely, the customs duty is
+                determined by the custom of your country, and it is uncertain to
+                know how much about it, you should contact your local customs
+                office for more information.
+              </p>
+            ),
+          },
+        ],
+      },
+      {
+        title: "2. About Products",
+        items: [
+          {
+            q: "Is your hair 100% human hair?",
+            a: <p>Yes, we only sell high quality 100% human virgin hair.</p>,
+          },
+          {
+            q: "Can I ask for small/large cap?",
+            a: (
+              <div className="space-y-2">
+                <p>
+                  Our Stock Size is Medium cap which is 22.5 inches, and we have
+                  the combs and elastic band or adjustable straps inside to help
+                  fit the head. Kindly Reminder:
+                </p>
+                <p>
+                  For full Lace Wig and 360 Lace Wig: The size of our full lace
+                  wigs and 360 lace wigs could not be changed.
+                </p>
+                <p>
+                  For U-part Wig and Headband Wig: The cap size of U-part wigs
+                  and headband wigs could not be changed, but the U-part wig’s
+                  top is empty so you can adjust the wig according to your head
+                  size, and the headband wig’s cap can be available with
+                  adjustable strap, elastic velcro snapping design wig with 5
+                  Clips, which can fit all head sizes perfectly, cap can be
+                  available with adjustable strap.
+                </p>
+                <p>For lace front / lace closure wig:</p>
+                <p>
+                  (1) Small Size is 21.5 inches, Large Size is 23.5 inches, you
+                  should know your own size and make a suitable decision.
+                </p>
+                <p>
+                  (2) If you change to those special cap sizes, you may need to
+                  wait about another extra 5-10 days. Would you like to wait? If
+                  you can wait and you want to ask for the small/big cap, please
+                  remember to leave the comments about it when you check out or
+                  send emails to us. Customer service email:{" "}
+                  <a
+                    href="mailto:server@asteriahair.com"
+                    className="text-blue-600 underline"
+                  >
+                    server@asteriahair.com
+                  </a>
+                  .
+                </p>
+                <p>
+                  (3) Custom-sized hairnets are subject to an additional charge
+                  of approximately $10.
+                </p>
+              </div>
+            ),
+          },
+          {
+            q: "Does your wig come in bleached?",
+            a: (
+              <p>
+                Yes, The lace of our wigs are slightly bleached under the
+                condition that the hair would not shed, if you need more
+                bleaching, we advise to find a professional hairstylist to help.
+              </p>
+            ),
+          },
+          {
+            q: "Is your wig pre plucked?",
+            a: (
+              <p>
+                Yes, all of our wigs come slightly pre plucked, you can
+                customize it to your liking once you receive it.
+              </p>
+            ),
+          },
+          {
+            q: "Can your hair be dyed / bleached?",
+            a: (
+              <div className="space-y-2">
+                <p>
+                  Yes. Our hairs are 100% human virgin hair and all our natural
+                  color hair can be dyed or bleached to #27, as a general rule
+                  it is easier to darken the hair than to lighter the hair. We
+                  do suggest getting a professional opinion by an experienced
+                  colorist and having your hairdresser dye the hair as you will
+                  get the results you want, coloring them yourself always poses
+                  a high risk of not coming out the shade you want.
+                </p>
+                <p>
+                  We would advise for a one clip weft to be tested prior to
+                  coloring or restyling the full set to make sure the resulting
+                  shade is what you anticipate.
+                </p>
+                <p>
+                  Note: Any dyeing and bleaching is done at the customer's own
+                  discretion and we cannot be held accountable if the results do
+                  not come out as intended.
+                </p>
+              </div>
+            ),
+          },
+          {
+            q: "How many hair bundles can make a full head?",
+            a: (
+              <p>
+                Normally the weight of 1 bundle is 95g-105g,we suggest you to
+                take 3-4 bundles for a full head,Especially for the length from
+                20inch to 30inch,4 bundles are necessary.
+              </p>
+            ),
+          },
+        ],
+      },
+      {
+        title: "3. About Payment",
+        items: [
+          {
+            q: "What types of payment do you accept?",
+            a: (
+              <p>
+                We accept the following payment methods: Paypal, Credit Cards
+                (Visa Card, MasterCard, and American Express), Klarna, Western
+                Union, Moneygram. If you have any payment problems, please
+                contact with us:{" "}
+                <a
+                  href="mailto:server@asteriahair.com"
+                  className="text-blue-600 underline"
+                >
+                  server@asteriahair.com
+                </a>
+                .
+              </p>
+            ),
+          },
+          {
+            q: "Do you accept payment by installment?",
+            a: <p>Yes, Now we support PayPal Pay later and Klarna payment.</p>,
+          },
+          {
+            q: "When I pay via PayPal, do I have to have a PayPal account? Can I use credit cards?",
+            a: (
+              <p>
+                It is not necessary to have a PayPal account to order, you can
+                pay with Credit/Debit Card directly. When you check out, please
+                click paypal button, then you can choose the "Pay with Credit
+                Card" on pop-up payment page.
+              </p>
+            ),
+          },
+          {
+            q: "Why is my card declined?",
+            a: (
+              <p>
+                Sometimes when your card is expired, insufficient balance or
+                poor credit which may have a record of Chargeback or others will
+                lead to card declined when paying. So we suggest customers not
+                take a dispute for no reasons, because your credit record will
+                be affected, and your next shopping will be declined. When your
+                card is declined, you can call your bank for the problem. Or you
+                can try another card. If you still have problems, please send an
+                email to us :{" "}
+                <a
+                  href="mailto:server@asteriahair.com"
+                  className="text-blue-600 underline"
+                >
+                  server@asteriahair.com
+                </a>
+                .
+              </p>
+            ),
+          },
+          {
+            q: "Why did my payment fail?",
+            a: (
+              <div className="space-y-2">
+                <p>
+                  If your payment for an order has failed, please check it is
+                  not due to the following situations:
+                </p>
+                <p>
+                  (1) Card security code failed, Please note that an incorrect
+                  card security code could cause payment failure. Card security
+                  code is the short numeric code on the back of your credit
+                  card. Please verify that you have entered the correct card
+                  security code and try again.
+                </p>
+                <p>
+                  (2) Insufficient fund, If you meet the insufficient fund error
+                  log when you make payment by credit card, please make sure
+                  that you have sufficient funds in your account and that
+                  payment for your order does not exceed your credit limit.
+                </p>
+                <p>
+                  (3) Please also make sure you have used a Visa or MasterCard
+                  personal credit card to finish payment, because at present
+                  debit card or business card is unable to be supported.
+                </p>
+                <p>
+                  (4) Exceed limit, Please make sure the order amount does not
+                  exceed your credit limit,or the payment is unable to go
+                  through smoothly.
+                </p>
+              </div>
+            ),
+          },
+          {
+            q: "If I pay on your website, Is my information safe?",
+            a: (
+              <p>
+                We are sure that our website is legal, which has been certified
+                by the card organization. So please don’t worry about that. Your
+                payment information is kept by your bank or card organization,
+                but not saved by us. We only use your shipping info for
+                delivery. Your information is safe and we will keep your
+                information confidential.
+              </p>
+            ),
+          },
+          {
+            q: "Can you send me an invoice?",
+            a: (
+              <p>
+                Yes, we can send you the invoice via Paypal Or Klarna if you
+                need, just feel free to contact us via email:{" "}
+                <a
+                  href="mailto:server@asteriahair.com"
+                  className="text-blue-600 underline"
+                >
+                  server@asteriahair.com
+                </a>
+                .
+              </p>
+            ),
+          },
+        ],
+      },
+      {
+        title: "4. About Shipment",
+        items: [
+          {
+            q: "How long will my hair be shipped after I place an order?",
+            a: (
+              <p>
+                Generally, your package will be shipped within 24 hours after
+                order confirm (except Weekends and Holiday). However, we may
+                need to wait for 2 or 3 days for the stock at times, thus the
+                delivery will be delayed for 2 or 3 days.
+              </p>
+            ),
+          },
+          {
+            q: "Which express do you use? How long can I receive my order?",
+            a: (
+              <p>
+                We use DHL, FedEx , USPS, GES, TNT, DPEX or UPS. Generally, the
+                package will be delivered within 3-7 business days to most
+                countries once shipped out. The time you receive your order will
+                depend on many factors, such as the shipping method and the
+                location of the shipping address. Please be aware of that bad
+                weathers, pandemic may cause the shipping delays sometimes. Once
+                your order has shipped out, you will receive the tracking number
+                by email.
+              </p>
+            ),
+          },
+          {
+            q: "Can you provide the Overnight / Expedite shipping service?",
+            a: (
+              <p>
+                1-2 day expedite shipping is available for USA orders if the
+                hair you want is in stock in our US warehouse. Please contact us
+                to confirm it before you order. Our Customer service email:{" "}
+                <a
+                  href="mailto:server@asteriahair.com"
+                  className="text-blue-600 underline"
+                >
+                  server@asteriahair.com
+                </a>
+                .
+              </p>
+            ),
+          },
+          {
+            q: "Can I receive my order on Saturday or Sunday?",
+            a: (
+              <p>
+                Most express companies are weekend off. If the package has been
+                arrived your city, you can go to the local sites to pick it up.
+              </p>
+            ),
+          },
+          {
+            q: "I am in a remote area, can you ship it?",
+            a: (
+              <p>
+                Generally, we can ship to most of the countries and regions
+                worldwide, but individual places may be charged remote fees. If
+                the zip code in the area can not be found on the express
+                delivery system, we will not be able to ship it, you need to
+                provide a new address.
+              </p>
+            ),
+          },
+          {
+            q: "Can I use Po.box as the shipping address?",
+            a: (
+              <p>
+                We don't accept PO.box (including FPO or APO) as the shipping
+                address, because the Express(DHL/FedEx/UPS/USPS/GES/TNT) can't
+                deliver and we only accept the physical address as shipping
+                address.
+              </p>
+            ),
+          },
+        ],
+      },
+      {
+        title: "5. About Account",
+        items: [
+          {
+            q: "How to register a member?",
+            a: (
+              <div className="space-y-1">
+                <p>(1) Open the Web page, click on the "sign up".</p>
+                <p>
+                  (2) You need fill out your mailbox address、password and
+                  telephone number.
+                </p>
+                <p>
+                  (3) Click “Create An Account” the specified page appears, you
+                  need to improve your information.
+                </p>
+              </div>
+            ),
+          },
+          {
+            q: "How to reset my password?",
+            a: (
+              <p>
+                Go to sign in page and click forget password. We will quickly
+                email you a link to reset your password, if you don't get the
+                email, please check your spam email at first. If still no email,
+                please feel free to contact customer service, we can help reset
+                your password.
+              </p>
+            ),
+          },
+          {
+            q: "How can I check my reward points?",
+            a: (
+              <p>
+                Please be aware of that you need to be registered to earn and
+                spend Reward Points. Sign in your account and you can check your
+                reward points at “Points” Section. For more details, please
+                check our reward points page.
+              </p>
+            ),
+          },
+        ],
+      },
+    ],
+  },
+  {
+    groupTitle: "Question After Purchase",
+    sections: [
+      {
+        title: "1. About Track Order",
+        items: [
+          {
+            q: "How to track my order?",
+            a: (
+              <div className="space-y-2">
+                <p>
+                  Method 1: Log into “MY ACCOUNT” to view the status of your
+                  order Track your shipment by using the tracking number
+                  provided to you once your items have been shipped. You cannot
+                  track your order before it has been shipped. If you don’t know
+                  what your tracking number is or if you’re having difficulty
+                  tracking your package, please feel free to contact our
+                  customer service department for assistance.
+                </p>
+                <p>
+                  Method 2: You can use your order number or shipment number to
+                  track your order though our Track Order page.
+                </p>
+              </div>
+            ),
+          },
+          {
+            q: "Why haven't I received my order yet?",
+            a: (
+              <p>
+                Once the package has been shipped out, we will email you the
+                tracking number, you can check the status. If not, please feel
+                free to contact our customer service:{" "}
+                <a
+                  href="mailto:server@asteriahair.com"
+                  className="text-blue-600 underline"
+                >
+                  server@asteriahair.com
+                </a>
+                , we will help you to check it.
+              </p>
+            ),
+          },
+          {
+            q: "Why is the shipment of my order always at the pending status?",
+            a: (
+              <p>
+                If it shows abnormality or pending in short time, that maybe
+                normal transport state. If it lasts a long time( over 1 day),
+                you can call the DHL/FedEx/UPS/USPS/TNT to ask about what is
+                going on about it. Or you can send us the Email to{" "}
+                <a
+                  href="mailto:server@asteriahair.com"
+                  className="text-blue-600 underline"
+                >
+                  server@asteriahair.com
+                </a>
+                , we will help you out.
+              </p>
+            ),
+          },
+          {
+            q: "If I'm not at home, how can I sign for my package? Can I have a delivery service without signature",
+            a: (
+              <p>
+                If you are not at home, you can ask your family and neighbors to
+                sign for it for you, or you can contact the local express
+                customer service and reschedule with them to deliver your
+                package, or you can choose to pick up the package yourself. In
+                order for you to successfully receive the package, we recommend
+                that you sign for it yourself, after you applied the service
+                without signature, if the package is lost or stolen, we do not
+                take any responsibility.
+              </p>
+            ),
+          },
+          {
+            q: "Why the shipment status shows that I have signed. But I did not receive the parcel yet.",
+            a: (
+              <p>
+                Once that happens, it means someone has signed the package for
+                you. It may be your neighbor or family and friends, It is also
+                possible that the courier drop your package around your house.
+                Please confirm it first. If not, please contact the express
+                company first to tell them your situation and let them help you
+                find the package, and please ask them as many details as
+                possible, whch is important to find the package later.
+              </p>
+            ),
+          },
+        ],
+      },
+      {
+        title: "2. About Return / Refunds",
+        items: [
+          {
+            q: "What is your return & exchange policy?",
+            a: (
+              <p>
+                We offer 30 days no reason return if the hair is in its original
+                state. For more details please check our return policy /
+                exchange policy page.
+              </p>
+            ),
+          },
+          {
+            q: "When will I get a refund after returning the package?",
+            a: (
+              <p>
+                A full refund will be provided to the original method of payment
+                after we have received, inspected and confirmed. All customers
+                must contact us after sending the package back and provide us
+                with the tracking information in order for us to process the
+                refund. Some financial institutions can take up to 3 business
+                days to process the refund. Please allow a few business days for
+                your financial institution to reflect the refund on your
+                statement prior to contacting us.
+              </p>
+            ),
+          },
+          {
+            q: "If I refuse to accept the package, can I get a full refund?",
+            a: (
+              <p>
+                We do not accept the refusal without any reasons. If you refuse
+                to sign the package and lead to the return, you will need to pay
+                for the return shipping and the shipping fee we paid the first
+                time. Any expenses incurred during the return of the parcel will
+                also be deducted from your payment. So we suggest that customers
+                contact us within 24 hours via email before shipping if they
+                have any problems (such as place the wrong order/ want to cancel
+                /want to exchange), so that we can help you to solve it timely.
+              </p>
+            ),
+          },
+        ],
+      },
+    ],
+  },
+];
+export default function Faqs() {
+  const [openSections, setOpenSections] = useState<string[]>([]);
+  const [openItems, setOpenItems] = useState<string[]>([]);
+
+  const toggleSection = (g: number, s: number) => {
+    const k = `${g}-${s}`;
+    setOpenSections((p) =>
+      p.includes(k) ? p.filter((x) => x !== k) : [...p, k],
+    );
+  };
+
+  const toggleItem = (g: number, s: number, i: number) => {
+    const k = `${g}-${s}-${i}`;
+    setOpenItems((p) => (p.includes(k) ? p.filter((x) => x !== k) : [...p, k]));
+  };
+
+  return (
+    <>
+      <HeadBack />
+      <div className="max-w-4xl mx-auto px-4 py-12">
+        {faqData.map((group, groupIdx) => (
+          <div key={groupIdx} className="mb-14">
+            <h2 className="text-2xl font-bold mb-6 text-gray-800">
+              {group.groupTitle}
+            </h2>
+
+            <div className="space-y-4">
+              {group.sections.map((sec, secIdx) => {
+                const secKey = `${groupIdx}-${secIdx}`;
+                const secOpen = openSections.includes(secKey);
+
+                return (
+                  <div
+                    key={secKey}
+                    className="border border-gray-200 rounded-lg overflow-hidden"
+                  >
+                    <button
+                      onClick={() => toggleSection(groupIdx, secIdx)}
+                      className="w-full flex justify-between items-center px-5 py-3 bg-gray-50 text-left font-semibold text-gray-800"
+                    >
+                      {sec.title}
+                      <span className="text-xl">{secOpen ? "−" : "+"}</span>
+                    </button>
+
+                    {secOpen && (
+                      <div className="divide-y divide-gray-100">
+                        {sec.items.map((item, itemIdx) => {
+                          const itemKey = `${groupIdx}-${secIdx}-${itemIdx}`;
+                          const itemOpen = openItems.includes(itemKey);
+
+                          return (
+                            <div key={itemKey} className="px-5 py-4">
+                              <button
+                                onClick={() =>
+                                  toggleItem(groupIdx, secIdx, itemIdx)
+                                }
+                                className="w-full flex justify-between items-center text-left font-medium text-gray-700"
+                              >
+                                <span>{item.q}</span>
+                                <span className="text-xl ml-2">
+                                  {itemOpen ? "−" : "+"}
+                                </span>
+                              </button>
+
+                              {itemOpen && (
+                                <div className="mt-3 text-gray-600 text-sm md:text-base">
+                                  {item.a}
+                                </div>
+                              )}
+                            </div>
+                          );
+                        })}
+                      </div>
+                    )}
+                  </div>
+                );
+              })}
+            </div>
+          </div>
+        ))}
+      </div>
+    </>
+  );
+}

+ 10 - 0
src/app/(public)/help-faqs/page.tsx

@@ -0,0 +1,10 @@
+
+import Faqs from "./_components/Faqs"
+export default function FaqPage() {
+  
+  return (
+    <>
+        <Faqs />
+    </>
+  );
+}

+ 31 - 14
src/app/(public)/page.tsx

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,48 @@
+"use client";
+import { useState } from "react";
+
+export default function ProductText({ ProductText }: { ProductText: string }) {
+  // 模拟商品长标题
+  const titleText =
+    ProductText ||
+    "Wiggins Hair HD Lace Frontal Wigs straight A Grade Human Hair 13x4 Transparent Lace Front Wig For Women Natural Black 18-30 Inch";
+  const [isExpand, setIsExpand] = useState(false);
+
+  return (
+    <div className="flex mt-2 items-start py-2 pr-2 pl-3 gap-2 w-full bg-[#F9F9F9]">
+      {/* 标题文本容器 */}
+      <div
+        className={`flex-1 text-ly-14 leading-ly-22  font-normal overflow-hidden transition-all ${
+          isExpand ? "" : "line-clamp-1 text-ellipsis whitespace-nowrap"
+        }`}
+      >
+        {titleText}
+      </div>
+
+      {/* 展开/收起SVG按钮 */}
+      <button
+        onClick={() => setIsExpand(!isExpand)}
+        className="shrink-0  transition-transform duration-300"
+        style={{ transform: isExpand ? "rotate(180deg)" : "rotate(0deg)" }}
+      >
+        {/* 向下对勾/箭头SVG */}
+
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          width="24"
+          height="24"
+          viewBox="0 0 24 24"
+          fill="none"
+        >
+          <path
+            stroke="rgba(0, 0, 0, 1)"
+            stroke-width="1.5"
+            stroke-linejoin="round"
+            stroke-linecap="square"
+            d="M18 9L12 15L6 9"
+          ></path>
+        </svg>
+      </button>
+    </div>
+  );
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 158 - 0
src/app/(public)/product/_components/PromotionDiscountCardProps .tsx


Dosya farkı çok büyük olduğundan ihmal edildi
+ 148 - 0
src/app/(public)/product/_components/ShopServicePanel.tsx


Dosya farkı çok büyük olduğundan ihmal edildi
+ 170 - 0
src/app/(public)/product/_components/SwitchButton.tsx


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

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

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

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

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

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

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

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

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

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

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

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

Dosya farkı çok büyük olduğundan ihmal edildi
+ 157 - 0
src/app/(public)/product/_components/question/ComponentsQuestionModal.tsx


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

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

Dosya farkı çok büyük olduğundan ihmal edildi
+ 390 - 0
src/app/(public)/product/_components/question/QAModal.tsx


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

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

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

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

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

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

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

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

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

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

Dosya farkı çok büyük olduğundan ihmal edildi
+ 156 - 62
src/app/(public)/product/_components/review/ReviewDetail.tsx


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

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

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

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

Dosya farkı çok büyük olduğundan ihmal edildi
+ 449 - 0
src/app/(public)/product/_components/review/ReviewModal.tsx


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

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

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

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

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

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

+ 76 - 0
src/app/(public)/track_order/_components/TrackFaqs.tsx

@@ -0,0 +1,76 @@
+"use client";
+import { useState } from "react";
+// FAQ 数据
+const faqList = [
+  {
+    question: "Tracking Number & Where Can I Find It?",
+    answer: `Your tracking number will be sent to you via email along with detailed order information. You can use the order number or tracking number to track your package here. If you have any further questions, feel free to contact us at: service@asteriahair.com.`,
+  },
+  {
+    question: "Why Can't I Track My Order?",
+    answer: `The tracking information may not be updated immediately after shipping. Please recheck after 24-48 hours. Or you can contact us via live chat or mail to:service@asteriahair.com`,
+  },
+  {
+    question: "Why Is My Order Still Processing?",
+    answer: `Most of orders processed within 72 hours, but if the product is customized wig, hot-selling wig or length, the processing time may be delayed by 2-3 days. Please don't worry, we will try our best to ship your package out ASAP.`,
+  },
+  {
+    question: "Delivery Failed, What Should I Do?",
+    answer: `If the delivery failed, we will notify you to pick it up by SMS/Email. Please contact the logistics company for re-delivery or pick it up by yourself. Otherwise, the loss caused by not picking up the package in time shall be borne by yourself.`,
+  },
+];
+export default function TrackFaqs() {
+  // 当前展开的FAQ索引,null=全部收起
+  const [activeIndex, setActiveIndex] = useState<number | null>(null);
+
+  // 切换折叠面板(互斥,只展开一个)
+  const toggleFaq = (idx: number) => {
+    setActiveIndex(activeIndex === idx ? null : idx);
+  };
+
+  return (
+    <div>
+      {faqList.map((item, idx) => (
+        <div
+          key={idx}
+          className={`border-t border-gray-200 ${idx === 0 ? "border-t-0" : ""}`}
+        >
+          {/* 问题标题栏 */}
+          <button
+            type="button"
+            onClick={() => toggleFaq(idx)}
+            className="w-full flex justify-between items-center px-6 py-5 text-left text-lg"
+          >
+            <span>Q:{item.question}</span>
+            {/* 上下箭头图标 */}
+            <svg
+              width="20"
+              height="20"
+              viewBox="0 0 24 24"
+              fill="none"
+              stroke="currentColor"
+              strokeWidth="1.8"
+              strokeLinecap="round"
+              strokeLinejoin="round"
+              className={`transition-transform duration-200 ${
+                activeIndex === idx ? "rotate-180" : ""
+              }`}
+            >
+              <path d="M18 15l-6-6-6 6" />
+            </svg>
+          </button>
+          {/* 回答内容,展开/收起 */}
+          <div
+            className={`px-6 overflow-hidden transition-all duration-300 ${
+              activeIndex === idx ? "max-h-[800px] pb-6" : "max-h-0"
+            }`}
+          >
+            <p className="text-base leading-relaxed text-gray-800">
+              A:{item.answer}
+            </p>
+          </div>
+        </div>
+      ))}
+    </div>
+  );
+}

+ 32 - 0
src/app/(public)/track_order/_components/TrackOrder.tsx

@@ -0,0 +1,32 @@
+"use client";
+import { useState } from "react";
+
+export default function TrackOrder() {
+  const [orderNo, setOrderNo] = useState("");
+  // 查询提交
+  const handleTrackSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    console.log("查询单号:", orderNo);
+    // 这里写你的查询接口逻辑
+  };
+  return (
+    <>
+      {/* 查询表单 */}
+      <form onSubmit={handleTrackSubmit} className="space-y-6">
+        <input
+          type="text"
+          value={orderNo}
+          onChange={(e) => setOrderNo(e.target.value)}
+          placeholder="* Please Input Your Number Here"
+          className="w-full px-5 py-4 text-base bg-white border-0 rounded-[8px] outline-none focus:ring-2 focus:ring-emerald-700"
+        />
+        <button
+          type="submit"
+          className="w-full py-5 bg-emerald-800 text-white text-xl rounded-full hover:bg-emerald-900 transition-colors"
+        >
+          Track Order
+        </button>
+      </form>
+    </>
+  );
+}

+ 47 - 0
src/app/(public)/track_order/page.tsx

@@ -0,0 +1,47 @@
+import HeadBack from "@/components/head/HeadBack";
+import TrackOrder from "./_components/TrackOrder";
+import TrackFaqs from "./_components/TrackFaqs";
+
+export default function TrackOrderPage() {
+  return (
+    <>
+      <HeadBack />
+      <div className="min-h-screen bg-[#f0ede4] text-black">
+        <div className="max-w-3xl mx-auto px-4 py-8">
+          {/* 顶部 Track Order 区域 */}
+          <section className="mb-10">
+            <h1 className="text-[clamp(1.8rem,4vw,2.5rem)] font-normal mb-6">
+              Track Order
+            </h1>
+            <div className="space-y-3 mb-8 text-base leading-relaxed">
+              <p>Want To Check The Status Of Your Order?</p>
+              <p>
+                Please Input Your Order Number And Click The Track Order Button
+                To Find The Tracking Information.
+              </p>
+            </div>
+
+            <TrackOrder />
+          </section>
+
+          {/* FAQ 区域 */}
+          <section className="bg-white rounded-[8px] overflow-hidden">
+            <h2 className="text-[clamp(1.5rem,3vw,2rem)] font-normal p-6 pb-4">
+              People Also Ask
+            </h2>
+            {/* 问答 */}
+            <TrackFaqs />
+          </section>
+
+          {/* You May Like 标题区域 */}
+          <div className="mt-10">
+            <h2 className="text-[clamp(1.4rem,3vw,1.8rem)] font-normal">
+              You May Like
+            </h2>
+            {/* 下方可自行添加商品列表 */}
+          </div>
+        </div>
+      </div>
+    </>
+  );
+}

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

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

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

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

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

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

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

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

+ 50 - 0
src/app/api/customer/reward-points/browse-product/route.ts

@@ -0,0 +1,50 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+export async function POST(req: NextRequest) {
+  try {
+    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+    const params = await req.json();
+    const response = await restApiFetch<{
+      data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
+      variables:{
+        categorys:string,
+      } ;
+    }>({
+      api: "/customer/reward-points/browse-product",
+      method: "POST",
+      cache: "no-store",
+      variables: params,
+      guestToken: authorizationToken,
+    });
+    // 打印后端原始返回结构
+    console.log(
+      "接口原始response.body =",
+      JSON.stringify(response.body, null, 2),
+    );
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    console.log("/customer/token/address---", error); // 调试用
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 },
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 },
+    );
+  }
+}

+ 40 - 0
src/app/api/customer/reward-points/follow-status/route.ts

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

+ 62 - 0
src/app/api/customer/reward-points/follow/route.ts

@@ -0,0 +1,62 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+export async function POST(req: NextRequest) {
+  try {
+    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+    const params = await req.json();
+    const response = await restApiFetch<{
+      data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
+      variables:{
+        platform:string,
+      } ;
+    //   {
+    //     first_name: string;
+    //     last_name: string;
+    //     email: string;
+    //     phone: string;
+    //     address: string;
+    //     country: string;
+    //     state: string;
+    //     city: string;
+    //     postcode: string;
+    //     default_address: number;
+    //   }
+    }>({
+      api: "/customer/reward-points/follow",
+      method: "POST",
+      cache: "no-store",
+      variables: params,
+      guestToken: authorizationToken,
+    });
+    // 打印后端原始返回结构
+    console.log(
+      "接口原始response.body =",
+      JSON.stringify(response.body, null, 2),
+    );
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    console.log("/customer/token/address---", error); // 调试用
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 },
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 },
+    );
+  }
+}

+ 61 - 0
src/app/api/customer/reward-points/history/route.ts

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

+ 193 - 0
src/app/api/customer/token/address/route.ts

@@ -0,0 +1,193 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+// import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
+export async function GET(req: NextRequest) {
+  try {
+    const guestToken = getAuthToken(req);
+    // 从url取查询参数 id
+    const addressId = req.nextUrl.searchParams.get("id");
+
+    const apiUrl = addressId
+      ? `/customer/token/address/${addressId}`
+      : `/customer/token/address`;
+
+    console.log("11111----:", apiUrl);
+  
+    const response = await restApiFetch<any>({
+      api: apiUrl, //
+      method: "GET",
+      cache: "no-store",
+      guestToken,
+    });
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 },
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 },
+    );
+  }
+}
+export async function POST(req: NextRequest) {
+  try {
+    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+    const params = await req.json();
+    const response = await restApiFetch<{
+      data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
+      variables: {
+        first_name: string;
+        last_name: string;
+        email: string;
+        phone: string;
+        address: string;
+        country: string;
+        state: string;
+        city: string;
+        postcode: string;
+        default_address: number;
+      };
+    }>({
+      api: "/customer/token/address",
+      method: "POST",
+      cache: "no-store",
+      variables: params,
+      guestToken: authorizationToken,
+    });
+    // 打印后端原始返回结构
+    console.log(
+      "接口原始response.body =",
+      JSON.stringify(response.body, null, 2),
+    );
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    console.log("/customer/token/address---", error); // 调试用
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 },
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 },
+    );
+  }
+}
+export async function PUT(req: NextRequest) {
+  try {
+    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+    const params = await req.json();
+    // 从url取查询参数 id
+    const addressId = req.nextUrl.searchParams.get("id");
+    const response = await restApiFetch<{
+      data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
+      variables: {
+        first_name: string;
+        last_name: string;
+        email: string;
+        phone: string;
+        address: string;
+        country: string;
+        state: string;
+        city: string;
+        postcode: string;
+        default_address: number;
+      };
+    }>({
+      api: `/customer/token/address/${addressId}`,
+      method: "PUT",
+      cache: "no-store",
+      variables: params,
+      guestToken: authorizationToken,
+    });
+    // 打印后端原始返回结构
+    console.log(
+      "接口原始response.body =",
+      JSON.stringify(response.body, null, 2),
+    );
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    console.log("/customer/token/address---", error); // 调试用
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 },
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 },
+    );
+  }
+}
+export async function DELETE(req: NextRequest) {
+  try {
+    const guestToken = getAuthToken(req);
+    const addressId = req.nextUrl.searchParams.get("id");
+    const response = await restApiFetch<any>({
+      api: `/customer/token/address/${addressId}`, //
+      method: "DELETE",
+      cache: "no-store",
+      guestToken,
+    });
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+  } catch (error) {
+    if (isBagistoError(error)) {
+      return NextResponse.json(
+        {
+          data: null,
+          error: error.cause ?? error,
+        },
+        { status: 200 },
+      );
+    }
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 },
+    );
+  }
+}

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

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

+ 40 - 0
src/app/api/shop/customer-address-gets/route.ts

@@ -0,0 +1,40 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+// import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
+export async function GET(req: NextRequest) {
+    try {
+        const guestToken = getAuthToken(req);
+        // 从查询参数取 page,默认 1
+        const id = req.nextUrl.searchParams.get("id") || "1";
+        const response = await restApiFetch<any>({
+            api: `/shop/customer-address-gets?id=${id}`,
+            method:'GET',
+            cache:'no-store',
+            guestToken,
+        });
+        return NextResponse.json({
+            status: response.status,
+            data: response.body,
+        });
+    } catch (error) {
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

+ 41 - 0
src/app/api/shop/customer-addresses/route.ts

@@ -0,0 +1,41 @@
+import { NextResponse,NextRequest } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+// import { NextRequestHint } from "next/dist/server/web/adapter";
+// import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
+export async function GET(req: NextRequest) {
+    try {
+        const guestToken = getAuthToken(req);
+        // 从查询参数取 page,默认 1
+        const page = req.nextUrl.searchParams.get("page") || "1";
+        const response = await restApiFetch<any>({
+            api: `/shop/customer-addresses?page=${page}`,
+            method:'GET',
+            cache:'no-store',
+            guestToken,
+        });
+        return NextResponse.json({
+            status: response.status,
+            data: response.body,
+        });
+    } catch (error) {
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

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


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor