Jelajahi Sumber

产品详情页接口对接

zhangzf 2 hari lalu
induk
melakukan
68b713a7e0

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

@@ -20,8 +20,9 @@ import { RelatedProductsSection } from "@components/catalog/product/RelatedProdu
 import { HeroCarouselShimmer } from "@components/common/slider";
 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 { ProductShortDescription } from "@/app/(public)/product/_components/ProductShortDescription";
 import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
+import ProductDescription from "../_components/ProductDescription";
 import SwitchButton from "../_components/SwitchButton";
 import ShopServicePanel from "../_components/ShopServicePanel";
 import Recommend from "../_components/youmaylike/Recommend"
@@ -69,12 +70,12 @@ export default async function ProductPage({
   //   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 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("productCustomerFeatures----------------------",product);
   const questionTotal = 73;
   // const imageUrl = getImageUrl(product?.baseImageUrl, baseUrl, NOT_IMAGE);
 
@@ -131,16 +132,17 @@ console.log("res----------------------",allReviews);
             productOptions={productOptions}
             flexibleVariants={flexibleVariants}
             isSaleable={product.isSaleable}
+            shortDescription={product.shortDescription || ""}
           />
           <ShopServicePanel />
-          <ProductShortDescription
+          {/* <ProductShortDescription
             shortDescription={product.shortDescription || ""}
-          />
-
+          /> */}
+         <ProductDescription   htmlString={product?.feature || ""} />
           <SwitchButton
             productId={"3792"} //|| String(product._id)
-            reviewList={allReviews}
-            reviewTotal={allReviews.length}
+            // reviewList={allReviews}
+            // reviewTotal={allReviews.length}
             questionTotal={questionTotal}
             questionList={["aaa"]}
           />

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

@@ -70,6 +70,7 @@ type VariantPriceInfo = {
   totalNowPrice: number; // 现价 $119
   totalLinePrice: number; // 划线原价 $170
   save: number; // 优惠金额 $51
+ 
 };
 
 export function ProductAddToCart({
@@ -78,12 +79,14 @@ export function ProductAddToCart({
   onAddToCart,
   onBuyNow,
   priceInfo, // 父组件传入价格数据
+  currencyCode
 }: {
   isAvailable: boolean;
   isLoading: boolean;
   onAddToCart: () => void;
   onBuyNow: () => void;
   priceInfo: VariantPriceInfo; // 价格数据源
+  currencyCode:string;
 }) {
   const btnClass =
     "flex items-center justify-center rounded-full text-ly-16 font-bold text-white h-[46px]";
@@ -120,7 +123,7 @@ export function ProductAddToCart({
           <Price
             className="text-ly-gray  text-base line-through "
             amount={String(totalLinePrice)}
-            currencyCode="USD"
+            currencyCode={currencyCode}
           />
         )}
 
@@ -130,14 +133,14 @@ export function ProductAddToCart({
             <Price
               className="bg-ly-gold px-2  leading-ly-22  text-sm mr-2  font-bold"
               amount={`-${save}`}
-              currencyCode="USD"
+              currencyCode={currencyCode}
             />
           )}
           {/* 现价 */}
           <Price
             className="  text-base font-bold mr-1"
             amount={String(totalNowPrice)}
-            currencyCode="USD"
+            currencyCode={currencyCode}
           />
           {/* 右侧下拉箭头SVG */}
           <span onClick={() => setCouponModalOpen(true)}>
@@ -181,6 +184,7 @@ export function ProductAddToCart({
         visible={couponModalOpen}
         onClose={() => setCouponModalOpen(false)}
         priceInfo={priceInfo}
+        currencyCode={currencyCode}
       />
     </div>
   );

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

@@ -0,0 +1,95 @@
+"use client";
+import { useState, useMemo } from "react";
+
+interface SpecItem {
+  label: string;
+  value: string;
+}
+interface ProductSpecsProps {
+  htmlString: string;
+}
+
+// html表格字符串解析工具函数
+function parseTableHtml(html: string): SpecItem[] {
+  const items: SpecItem[] = [];
+  // 正则匹配每一行 <tr> 里面 th.label 和 td.data
+  const reg = /<tr[\s\S]*?<th class="label">([\s\S]*?)<\/th>[\s\S]*?<td class="data last">([\s\S]*?)<\/td>/g;
+  let match;
+  while ((match = reg.exec(html)) !== null) {
+    const label = match[1].replace(/<[^>]*>/g, "").trim();
+    const value = match[2].replace(/<[^>]*>/g, "").trim();
+    if (label && value) {
+      items.push({ label, value });
+    }
+  }
+  return items;
+}
+
+export default function ProductDescription({ htmlString }: ProductSpecsProps) {
+  // 整体收展(标题旁边箭头)
+  const [panelCollapsed, setPanelCollapsed] = useState(false);
+  // 是否点击了View More,展示全部属性
+  const [showAll, setShowAll] = useState(false);
+
+  const specList = useMemo(() => {
+    return parseTableHtml(htmlString);
+  }, [htmlString]);
+
+  // 默认4条
+  const displayList = showAll ? specList : specList.slice(0, 4);
+  // 是否需要显示View More按钮
+  const needViewMore = specList.length > 4 && !showAll;
+
+  return (
+    <div className="w-full px-4">
+      {/* 头部标题 + 下拉箭头,可整体收展 */}
+      <div
+        className="flex justify-between items-center cursor-pointer pb-2"
+        onClick={() => setPanelCollapsed(!panelCollapsed)}
+      >
+        <h2 className="text-[clamp(1.5rem,3vw,2.25rem)] font-bold">Description</h2>
+        {/* 箭头svg 旋转180代表展开 */}
+        <svg
+          className={`w-7 h-7 transition-transform duration-200 ${panelCollapsed ? "rotate-180" : ""}`}
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+        >
+          <path d="M6 9l6 6 6-6" />
+        </svg>
+      </div>
+
+      {/* 主体属性区域,整体收展控制 */}
+      {!panelCollapsed && (
+        <>
+          <div className="pl-4">
+            {displayList.map((item, index) => (
+              <div
+                key={index}
+                className="flex justify-start py-2 items-center"
+              >
+                <span className="text-black text-ly-13 leading-ly-22  font-normal w-35.5 shrink-0">{item.label}</span>
+                <span className="text-[#A6A6A6FF] text-ly-13 leading-ly-22  font-normal text-start ">
+                  {item.value}
+                </span>
+              </div>
+            ))}
+          </div>
+
+          {/* View More按钮 */}
+          {needViewMore && (
+            <div className="flex justify-center mt-5">
+              <button
+                onClick={() => setShowAll(true)}
+                className="px-8 py-2 border border-black rounded-full text-base hover:bg-gray-50 transition"
+              >
+                View More
+              </button>
+            </div>
+          )}
+        </>
+      )}
+    </div>
+  );
+}

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

@@ -13,6 +13,7 @@ import ProductText from "./ProductText";
 import NewUserBanner from "./NewUserBanner";
 import PromotionDiscountCardProps from "./PromotionDiscountCardProps ";
 import Image from "next/image";
+import {useConfig} from "@/utils/hooks/useConfig";
 import {
   isValueAvailable,
   isOptionValueAvailable,
@@ -29,12 +30,14 @@ export function ProductInformation({
   productOptions,
   flexibleVariants,
   isSaleable,
+  shortDescription,
 }: {
   name: string;
   productId: number;
   productOptions: ProductOption[];
   flexibleVariants: ResolvedVariant[];
   isSaleable: string | undefined;
+  shortDescription:string;
 }) {
   const { isCartLoading, onAddToCart } = useAddProduct();
   const { showToast } = useCustomToast();
@@ -234,7 +237,12 @@ export function ProductInformation({
       addProductToCart("buynow");
     }
   };
-  const currencySymbol = "$";
+  const {getCurrentCurrencyItem} = useConfig();
+  const currentCurrency = getCurrentCurrencyItem();
+  const currencySymbol = currentCurrency.symbol;
+  const currencyCode = currentCurrency.code;
+  
+  // const currencySymbol = "$";
 
   return (
     <>
@@ -242,12 +250,12 @@ export function ProductInformation({
         <h3 className="text-ly-14 text-black mt-4 leading-ly-22">
           {name} {isSaleable}
         </h3>
-        <ProductText ProductText="" />
+        <ProductText ProductText={shortDescription} />
         <div className="flex items-center mt-2">
           <Price
             className="text-ly-16 leading-ly-24 font-bold"
             amount={String(currentVariantInfo.totalNowPrice)}
-            currencyCode="USD"
+            currencyCode={currencyCode}
           />
           {currentVariantInfo.totalLinePrice !==
             currentVariantInfo.totalNowPrice && (
@@ -255,17 +263,17 @@ export function ProductInformation({
               <Price
                 className="text-ly-12 leading-ly-20 text-ly-gray line-through ml-1"
                 amount={String(currentVariantInfo.totalLinePrice)}
-                currencyCode="USD"
+                currencyCode={currencyCode}
               />
               <Price
                 className="text-ly-12 leading-ly-20 text-ly-gray line-through ml-1"
                 amount={String(currentVariantInfo.totalLinePrice)}
-                currencyCode="USD"
+                currencyCode={currencyCode}
               />
               <Price
                 className="text-ly-12 leading-ly-16 bg-ly-gold pr-1.5 pl-1.5 ml-3"
                 amount={String(currentVariantInfo.save)}
-                currencyCode="USD"
+                currencyCode={currencyCode}
               />
             </>
           )}
@@ -423,6 +431,7 @@ export function ProductInformation({
         isLoading={isCartLoading}
         onAddToCart={addToCartHandler}
         onBuyNow={buyNowHandler}
+        currencyCode={currencyCode}
         priceInfo={{
           totalNowPrice: currentVariantInfo.totalNowPrice,
           totalLinePrice: currentVariantInfo.totalLinePrice,

+ 3 - 0
src/app/(public)/product/_components/ProductReviewSection.tsx

@@ -12,6 +12,7 @@ interface ProductReviewSectionProps {
   onOpenBaseModal: () => void;
   activeTab: string;
   onChangeTab: (tabKey: string) => void;
+  uuid:string;
 }
 
 function ProductReviewSection({
@@ -22,6 +23,7 @@ function ProductReviewSection({
   activeTab,
   onOpenBaseModal,
   onChangeTab,
+  uuid,
 }: ProductReviewSectionProps) {
   // const getAllreviews = await getProductReviews(productId)
   console.log("productId-------------:", productId, reviews);
@@ -39,6 +41,7 @@ function ProductReviewSection({
               onClose={closeModal}
               activeTab={activeTab}
               onChangeTab={onChangeTab} // 关键:把切换tab的函数传给弹窗
+              uuid={uuid}
             />
           </>
         ) : (

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

@@ -2,9 +2,13 @@
 import { useState } from "react";
 
 export default function ProductText({ ProductText }: { ProductText: string }) {
+  const stripHtml = (html: string) => {
+  if(!html) return ""
+  return html.replace(/<[^>]*>/g, "").trim()
+}
   // 模拟商品长标题
   const titleText =
-    ProductText ||
+     (ProductText ? stripHtml(ProductText) : null) ||
     "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);
 

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

@@ -5,11 +5,12 @@ import Image from "next/image";
 import ReviewAdd from "@/app/(public)/product/_components/review/ReviewAdd";
 import FaqList, { FaqItem } from "./question/FaqList";
 import HairPhotoGallery from "./review/HairPhotoGallery";
+import { clientFetch } from "@/lib/restApiClient";
 interface ReviewQuestionTabProps {
   productId: string;
   questionList: any[];
-  reviewList: any[]; // 替换为你的 ProductReviewList 类型
-  reviewTotal: number; // 评论总数 (allReviews.length)
+  // reviewList: any[]; // 替换为你的 ProductReviewList 类型
+  // reviewTotal: number; // 评论总数 (allReviews.length)
   questionTotal: number; // 问答总数,后端接口获取
 }
 // 模拟截图内FAQ数据
@@ -45,14 +46,52 @@ const mockFaqData: FaqItem[] = [
 ];
 export default function SwitchButton({
   productId,
-  reviewList,
   questionList,
-  reviewTotal,
   questionTotal,
 }: ReviewQuestionTabProps) {
+    const [uuid, setUuidId] = useState<string>('');
+  // 自己通过接口拿到,不再由父组件传入
+  const [reviewList, setReviewList] = useState<any[]>([]);
+  const [reviewTotal, setReviewTotal] = useState<number>(0);
+    // 获取/生成访客UUID
+  const getUuId = () => {
+    const key = "GUEST_UUID";
+    let id = localStorage.getItem(key);
+    if (!id) {
+      id = crypto.randomUUID();
+      localStorage.setItem(key, id);
+    }
+    return id;
+  };
+    // 2. 封装评论请求函数
+  const fetchReviews = async () => {
+      if (!productId) return;
+    try {
+      const id = getUuId();
+      setUuidId(id);
+      const res = await clientFetch(
+        `/api/shop/products/${productId}/reviews?page=1&per_page=10&has_images=0&sort=all&client_id=${id}`
+      );
+      console.log('res------------------------------ccc:',res);
+      
+      // 3. 处理接口返回数据,严格对齐子组件需要的格式
+      if (res.data.success) {
+      setReviewList(res?.data?.data ?? []);
+      setReviewTotal(res?.pagination?.total ?? 0);
+      } 
+    }  catch (err) {
+      console.error("获取评论失败", err);
+    } finally {
+      
+    }
+  };
+  useEffect(() => {
+    fetchReviews();
+  }, [productId]);
+
   // 切换状态:默认激活评论
   const [activeTab, setActiveTab] = useState<"review" | "question">("review");
-  // 提前计算空状态,减少模板内嵌套
+  // 提前计算空状态,减少模板内嵌套 
   const isEmptyReview = useMemo(() => reviewList.length === 0, [reviewList]);
   const isEmptyQuestion = useMemo(
     () => questionList.length === 0,
@@ -147,6 +186,7 @@ export default function SwitchButton({
               onOpenBaseModal={openAllReviewModal}
               activeTab={activeModalTab}
               onChangeTab={onTabChange}
+              uuid={uuid}
             />
           )
         ) : isEmptyQuestion ? (

+ 5 - 3
src/app/(public)/product/_components/popup/PriceModal.tsx

@@ -13,12 +13,14 @@ interface PriceModalModalProps {
   visible: boolean;
   onClose: () => void;
   priceInfo: PriceInfo;
+  currencyCode:string;
 }
 
 export default function PriceModal({
   visible,
   onClose,
   priceInfo,
+  currencyCode,
 }: PriceModalModalProps) {
   if (!visible) return null;
 
@@ -59,7 +61,7 @@ export default function PriceModal({
               <Price
                 className="text-ly-20 leading-ly-25 font-normal block "
                 amount={String(totalLinePrice)}
-                currencyCode="USD"
+                currencyCode={currencyCode}
               />
               <p className="text-ly-12 leading-ly-20 font-normal">Retail price</p>
             </div>
@@ -72,7 +74,7 @@ export default function PriceModal({
               <Price
                 className="text-ly-20 leading-ly-25 font-normal "
                 amount={String(realDiscount)}
-                currencyCode="USD"
+                currencyCode={currencyCode}
               />
               <p className="text-ly-12 leading-ly-20 font-normal">Max Discount</p>
             </div>
@@ -85,7 +87,7 @@ export default function PriceModal({
               <Price
                 className="text-ly-20 leading-ly-25 font-bold"
                 amount={String(totalNowPrice)}
-                currencyCode="USD"
+                currencyCode={currencyCode}
               />
               <p className="text-ly-12 leading-ly-20 font-normal">Deal Price</p>
             </div>

File diff ditekan karena terlalu besar
+ 184 - 102
src/app/(public)/product/_components/review/ReviewDetail.tsx


+ 79 - 36
src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx

@@ -12,6 +12,13 @@ interface GalleryModalProps {
   onClose: () => void;
   galleryList: GalleryItem[];
   initialSlide: number;
+  // 新增:父组件传进来点赞回调
+  onLike: (reviewItem: any) => Promise<void>;
+  // 新增:读取点赞缓存状态
+  getCurrentLike: (reviewItem: any) => {
+    liked: boolean;
+    like_count: number;
+  };
 }
 
 export default function ReviewImageGalleryModal({
@@ -19,6 +26,8 @@ export default function ReviewImageGalleryModal({
   onClose,
   galleryList,
   initialSlide,
+  onLike,
+  getCurrentLike,
 }: GalleryModalProps) {
   const swiperRef = useRef<any>(null);
   // 新增状态存储当前下标,初始值传入initialSlide
@@ -51,45 +60,79 @@ export default function ReviewImageGalleryModal({
           // 滑动切换时更新下标
           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>
+          {galleryList.map((item, idx) => {
+            const likeInfo = getCurrentLike(item.review);
+            return (
+              <SwiperSlide
+                key={idx}
+                className="flex flex-col justify-between h-full py-4"
+              >
+                {/* 大图 */}
+                <div className="flex-1 flex items-center justify-center 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="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"
+                {/* 底部用户评论信息 */}
+                <div className="bg-black/80 text-white p-4 mt-4">
+                  <div className="flex items-center gap-2 mb-2">
+                    <div className="w-10 h-10 rounded-full bg-gray-300"></div>
+                    <span className="font-medium">{item.review.name}</span>
+                    <div
+                      className="ml-auto flex items-center gap-1 cursor-pointer"
+                      onClick={() => onLike(item.review)}
                     >
-                      <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>
-                  ))}
+                      <span
+                        className={likeInfo.liked ? "text-[#036141FF]" : ""}
+                      >
+                        {likeInfo.like_count}
+                      </span>
+                      {likeInfo.liked ? (
+                        <svg
+                          xmlns="http://www.w3.org/2000/svg"
+                          width="16"
+                          height="16"
+                          viewBox="0 0 16 16"
+                          fill="#036141FF"
+                        >
+                          <path d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z" />
+                        </svg>
+                      ) : (
+                        <svg
+                          xmlns="http://www.w3.org/2000/svg"
+                          width="16"
+                          height="16"
+                          viewBox="0 0 16 16"
+                          fill="white"
+                        >
+                          <path d="M8 2.748l-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.981 6.186 6.362 3.352-2.38 5.265-4.547 6.186-6.362.955-1.884.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01L8 2.748zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143c.06.055.119.112.176.171a3.12 3.12 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15z" />
+                        </svg>
+                      )}
+                    </div>
+                    {/* <span className="ml-auto">{item.review.like_count} 👍</span> */}
+                  </div>
+                  <div className="flex gap-0.5 mb-2">
+                    {Array.from({ length: 5 }).map((_, i) => (
+                      <svg
+                        key={i}
+                        width="14"
+                        height="14"
+                        fill={i < item.review.rating ? "#fff" : "#666"}
+                        viewBox="0 0 24 24"
+                      >
+                        <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
+                      </svg>
+                    ))}
+                  </div>
+                  <p className="text-sm text-gray-200">{item.review.comment}</p>
                 </div>
-                <p className="text-sm text-gray-200">{item.review.comment}</p>
-              </div>
-            </SwiperSlide>
-          ))}
+              </SwiperSlide>
+            );
+          })}
         </Swiper>
       </div>
     </div>

File diff ditekan karena terlalu besar
+ 140 - 32
src/app/(public)/product/_components/review/ReviewModal.tsx


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

@@ -18,6 +18,7 @@ export async function GET(req: NextRequest, { params }: { params: Params }) {
         per_page: searchParams.get("per_page") ?? 10,
         has_images:searchParams.get("has_images") ?? 0, 
         sort:searchParams.get("sort") ?? 'all', 
+        client_id:searchParams.get("client_id") ?? '', 
       },
        guestToken,
     });

+ 42 - 0
src/app/api/shop/reviews/[id]/like/route.ts

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

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

@@ -112,6 +112,7 @@ export interface ProductNode {
   name?: string;
   urlKey?: string;
   isSaleable?: string | undefined;
+  feature?:string;
   description?: string;
   shortDescription?: string;
   specialPrice?: string;

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

@@ -10,6 +10,7 @@ export const PRODUCT_DETAILED_FRAGMENT = gql`
     urlKey
     description
     shortDescription
+    feature
     price
     baseImageUrl
     minimumPrice

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

@@ -11,7 +11,7 @@ export interface ProductReviewNode {
   migrated_from_asteria_id:number;
   isVip: boolean;
   tag: "influencer" | "verified" | "";
-  likeCount: number;
+  like_count: number;
 }
 
 export interface ProductReviewEdge {