Explorar o código

个人中心礼品卡相关接口联调

zhangzf hai 2 días
pai
achega
63496da5eb

+ 90 - 0
src/app/(public)/customer/account/mycoupon/_components/CouponList.tsx

@@ -0,0 +1,90 @@
+// src/components/CouponList.tsx
+import React, { useState, useEffect } from 'react';
+import { useInfiniteScroll } from "./UseInfiniteScroll";
+
+// 优惠券数据类型
+interface CouponItem {
+  id: number;
+  discount: string; // 优惠金额(如 $15 OFF)
+  desc: string; // 优惠描述
+  valid: string; // 有效期
+}
+
+const CouponList: React.FC = () => {
+  // 初始化优惠券数据
+  const [coupons, setCoupons] = useState<CouponItem[]>([
+    { id: 1, discount: '$15 OFF', desc: 'For All Products', valid: 'Valid:6/2/2026-12/31/2026' },
+    { id: 2, discount: '$10 OFF', desc: 'For All Products', valid: 'Valid:6/2/2026-12/31/2026' },
+  ]);
+  const [hasMore, setHasMore] = useState(true); // 模拟:初始有更多数据,加载2次后无更多
+
+  // 加载更多优惠券(模拟异步请求)
+  const loadMoreCoupons = async () => {
+    return new Promise<void>((resolve) => {
+      setTimeout(() => {
+        if (coupons.length >= 6) { // 模拟最多加载6条
+          setHasMore(false);
+          resolve();
+          return;
+        }
+        // 模拟新增数据
+        const newCoupons = [
+          { 
+            id: coupons.length + 1, 
+            discount: `$${5 * (coupons.length + 1)} OFF`, 
+            desc: 'For All Products', 
+            valid: 'Valid:6/2/2026-12/31/2026' 
+          },
+        ];
+        setCoupons(prev => [...prev, ...newCoupons]);
+        resolve();
+      }, 800);
+    });
+  };
+
+  // 复用懒加载Hook
+  const { containerRef, isLoading } = useInfiniteScroll({
+    loadMore: loadMoreCoupons,
+    hasMore,
+  });
+
+  return (
+    <div 
+      ref={containerRef}
+      className="h-[calc(100vh-80px)] overflow-auto px-2 py-4"
+    >
+      {/* 优惠券列表 */}
+      <div className="space-y-4">
+        {coupons.map((item) => (
+          <div 
+            key={item.id}
+            className="flex items-center bg-pink-100 rounded-lg overflow-hidden"
+          >
+            {/* 优惠信息区 */}
+            <div className="flex-1 p-4 text-pink-600">
+              <h3 className="text-2xl font-bold">{item.discount}</h3>
+              <p className="text-sm mt-1">{item.desc}</p>
+              <p className="text-xs mt-2 text-pink-500">{item.valid}</p>
+            </div>
+            {/* 分割线 */}
+            <div className="w-0.5 bg-pink-300 h-16 relative">
+              <div className="absolute top-0 left-[-4px] w-2 h-2 bg-white border border-pink-300 rounded-full"></div>
+              <div className="absolute bottom-0 left-[-4px] w-2 h-2 bg-white border border-pink-300 rounded-full"></div>
+            </div>
+            {/* 使用按钮区 */}
+            <div className="px-6 py-4 font-bold text-black">
+              Use Now
+            </div>
+          </div>
+        ))}
+      </div>
+
+      {/* 加载状态/无更多提示 */}
+      <div className="text-center mt-4 text-gray-400 text-sm">
+        {isLoading ? 'Loading...' : hasMore ? '' : 'No More Data'}
+      </div>
+    </div>
+  );
+};
+
+export default CouponList;

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

@@ -0,0 +1,114 @@
+import React, { useState, useEffect } from "react";
+import { useInfiniteScroll } from "./UseInfiniteScroll";
+import { clientFetch } from "@/lib/restApiClient";
+import type { GiftCardItem } from "@/types/api/gift/my-gift-cards";
+const GiftCardList: React.FC = () => {
+  // 初始化礼品卡数据
+  //  {
+  //     id: 1,
+  //     amount: "$50 Gift Card",
+  //     status: "Unused",
+  //     valid: "Valid:6/2/2026-12/31/2026",
+  //   },
+  //   {
+  //     id: 2,
+  //     amount: "$100 Gift Card",
+  //     status: "Unused",
+  //     valid: "Valid:6/2/2026-12/31/2026",
+  //   },
+  // {
+  //   expirationdate: "2026-07-04",
+  //   formatted_remaining_giftcard_amount: "$30.00",
+  //   giftcard_amount: "30.00",
+  //   giftcard_number: "CFVG-RYUD-AOXF-GSLE",
+  //   giftcard_status: "1",
+  //   id: 7,
+  //   remaining_giftcard_amount: "30.00",
+  //   used_giftcard_amount: "0.00",
+  // }
+  const [giftCards, setGiftCards] = useState<GiftCardItem[]>([]);
+  const [hasMore, setHasMore] = useState(true);
+
+  useEffect(() => {
+    const load = async () => {
+      const resp = await clientFetch("/api/gift/my-gift-cards");
+      const dataObj = resp.data.data;
+      setGiftCards((prev) => [...prev, ...dataObj]);
+      console.log("完整返回:", dataObj);
+    };
+
+    load();
+  }, []);
+  // 加载更多礼品卡(模拟异步请求)
+  const loadMoreGiftCards = async () => {
+    return new Promise<void>((resolve) => {
+      setTimeout(() => {
+        if (giftCards.length >= 6) {
+          setHasMore(false);
+          resolve();
+          return;
+        }
+        const newGiftCards = [
+          {
+            expirationdate: "2026-07-04",
+            formatted_remaining_giftcard_amount: "$30.00",
+            giftcard_amount: "30.00",
+            giftcard_number: "CFVG-RYUD-AOXF-GSLE",
+            giftcard_status: "1",
+            id: 7,
+            remaining_giftcard_amount: "30.00",
+            used_giftcard_amount: "0.00",
+          },
+        ];
+        setGiftCards((prev) => [...prev, ...newGiftCards]);
+        resolve();
+      }, 800);
+    });
+  };
+
+  // 复用懒加载Hook
+  const { containerRef, isLoading } = useInfiniteScroll({
+    loadMore: loadMoreGiftCards,
+    hasMore,
+  });
+
+  return (
+    <div
+      ref={containerRef}
+      className="h-[calc(100vh-80px)] overflow-auto px-2 py-4"
+    >
+      {/* 礼品卡列表 */}
+      <div className="space-y-4">
+        {giftCards.map((item,index) => (
+          <div
+            key={index}
+            className="flex items-center bg-blue-100 rounded-lg overflow-hidden"
+          >
+            {/* 礼品卡信息区 */}
+            <div className="flex-1 p-4 text-blue-600">
+              <h3 className="text-2xl font-bold">${Number(item.giftcard_amount)} OFF</h3>
+              <p className="text-sm mt-1">
+                For All Products
+              </p>
+              <p className="text-xs mt-2 text-blue-500">{item.expirationdate}</p>
+            </div>
+            {/* 分割线 */}
+            <div className="w-0.5 bg-blue-300 h-16 relative">
+              <div className="absolute top-0 left-[-4px] w-2 h-2 bg-white border border-blue-300 rounded-full"></div>
+              <div className="absolute bottom-0 left-[-4px] w-2 h-2 bg-white border border-blue-300 rounded-full"></div>
+            </div>
+            {/* 使用按钮区 */}
+            <div className="px-6 py-4 font-bold text-black">Use Now</div>
+          </div>
+        ))}
+      </div>
+
+      {/* 加载状态/无更多提示 */}
+      <div className="text-center mt-4 text-gray-400 text-sm">
+        {isLoading ? "Loading..." : hasMore ? "" : "No More Data"}
+      </div>
+    </div>
+  );
+};
+
+export default GiftCardList;

+ 42 - 0
src/app/(public)/customer/account/mycoupon/_components/TabButton.tsx

@@ -0,0 +1,42 @@
+"use client";
+import React, { useState } from 'react';
+import CouponList from './CouponList';
+import GiftCardList from './GiftCardList';
+const TabButton: React.FC = () => {
+ const [activeTab, setActiveTab] = useState<'coupon' | 'giftcard'>('coupon');
+  return (
+    <div>
+      {/* Tab 切换栏 */}
+      <div className="flex w-full">
+        {/* My Coupon Tab */}
+        <button
+          onClick={() => setActiveTab("coupon")}
+          className={`flex-1 py-3 text-center font-medium ${
+            activeTab === "coupon"
+              ? "bg-black text-white "
+              : "bg-white text-black"
+          }`}
+        >
+          My Coupon
+        </button>
+        {/* My Giftcard Tab */}
+        <button
+          onClick={() => setActiveTab("giftcard")}
+          className={`flex-1 py-3 text-center font-medium ${
+            activeTab === "giftcard"
+              ? "bg-black text-white"
+              : "bg-white text-black"
+          }`}
+        >
+          My Giftcard
+        </button>
+      </div>
+
+      {/* 内容展示区 */}
+      <div className="mt-2">
+        {activeTab === "coupon" ? <CouponList /> : <GiftCardList />}
+      </div>
+    </div>
+  );
+};
+export default TabButton;

+ 36 - 0
src/app/(public)/customer/account/mycoupon/_components/UseInfiniteScroll.tsx

@@ -0,0 +1,36 @@
+// src/hooks/useInfiniteScroll.ts
+import { useEffect, useRef, useState } from 'react';
+
+type UseInfiniteScrollProps = {
+  loadMore: () => Promise<void>; // 加载更多的异步方法
+  hasMore: boolean; // 是否还有更多数据
+};
+
+export const useInfiniteScroll = ({ loadMore, hasMore }: UseInfiniteScrollProps) => {
+  const [isLoading, setIsLoading] = useState(false);
+  const containerRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    const container = containerRef.current;
+    if (!container) return;
+
+    const handleScroll = async () => {
+      // 触底判断:滚动条距离底部 < 100px 且 有更多数据 且 不在加载中
+      const { scrollTop, scrollHeight, clientHeight } = container;
+      if (
+        scrollTop + clientHeight >= scrollHeight - 100 &&
+        hasMore &&
+        !isLoading
+      ) {
+        setIsLoading(true);
+        await loadMore();
+        setIsLoading(false);
+      }
+    };
+
+    container.addEventListener('scroll', handleScroll);
+    return () => container.removeEventListener('scroll', handleScroll);
+  }, [loadMore, hasMore, isLoading]);
+
+  return { containerRef, isLoading };
+};

+ 2 - 39
src/app/(public)/customer/account/mycoupon/page.tsx

@@ -1,13 +1,6 @@
-"use client";
-import React, { useState } from 'react';
 import Link from "next/link";
-import CouponList from '@/components/customer/mycounpon/couponList';
-import GiftCardList from '@/components/customer/mycounpon/giftCardList';
-
+import TabButton from "./_components/TabButton"
 const CouponsPage: React.FC = () => {
-  // Tab 切换状态:'coupon' / 'giftcard'
-  const [activeTab, setActiveTab] = useState<'coupon' | 'giftcard'>('coupon');
-
   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]">
@@ -34,37 +27,7 @@ const CouponsPage: React.FC = () => {
         </Link>
         <span className="vipReturnTitles">Accessories</span>
       </div>
-
-      {/* Tab 切换栏 */}
-      <div className="flex w-full">
-        {/* My Coupon Tab */}
-        <button
-          onClick={() => setActiveTab('coupon')}
-          className={`flex-1 py-3 text-center font-medium ${
-            activeTab === 'coupon' 
-              ? 'bg-white text-black border-b-2 border-black' 
-              : 'bg-gray-100 text-gray-500'
-          }`}
-        >
-          My Coupon
-        </button>
-        {/* My Giftcard Tab */}
-        <button
-          onClick={() => setActiveTab('giftcard')}
-          className={`flex-1 py-3 text-center font-medium ${
-            activeTab === 'giftcard' 
-              ? 'bg-black text-white' 
-              : 'bg-gray-100 text-gray-500'
-          }`}
-        >
-          My Giftcard
-        </button>
-      </div>
-
-      {/* 内容展示区 */}
-      <div className="mt-2">
-        {activeTab === 'coupon' ? <CouponList /> : <GiftCardList />}
-      </div>
+      <TabButton/>
     </div>
   );
 };

+ 3 - 5
src/app/(public)/customer/account/mypoints/page.tsx

@@ -1,5 +1,6 @@
 "use client";
 import Image from "next/image";
+import Link from "next/link";
 import React, { useState } from "react";
 import PointsRuleModal from "@/components/customer/PointsRule";
 
@@ -56,10 +57,7 @@ const AccountPointsPage = () => {
   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()}
-          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"
@@ -75,7 +73,7 @@ const AccountPointsPage = () => {
               d="M15 18L9 12L15 6"
             ></path>
           </svg>
-        </div>
+          </Link>
         <span className="vipReturnTitles">SHIPPING ADDRESS</span>
       </div>
       <div className=" bg-[#FFF5F2] px-4 py-8 max-w-3xl mx-auto pt-6 pb-5">

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 148 - 0
src/app/(public)/customer/account/pointsreceivelist/_components/RedeemGiftCard.tsx


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 5 - 115
src/app/(public)/customer/account/pointsreceivelist/page.tsx


+ 48 - 0
src/app/api/gift/add/route.ts

@@ -0,0 +1,48 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+
+export async function POST(req: NextRequest) {
+    try {
+        const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+        const params = await req.json();
+        const response = await restApiFetch<{
+            data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
+            variables: {
+                id: number; // 到时候你自己改看是number还是string
+            }
+        }>({
+            api: '/gift/add',
+            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('/gift/add --- ', error); // 调试用
+        if (isBagistoError(error)) {
+            return NextResponse.json(
+                {
+                    data: null,
+                    error: error.cause ?? error,
+                },
+                { status: 200 }
+            );
+        }
+
+        return NextResponse.json(
+            {
+                message: "Network error",
+                error: error instanceof Error ? error.message : error,
+            },
+            { status: 500 }
+        );
+    }
+}

+ 38 - 0
src/app/api/gift/lists/route.ts

@@ -0,0 +1,38 @@
+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,{ params }: { params: Promise<{ id: string }> }) {
+    try {
+        const guestToken = getAuthToken(req);
+        const response = await restApiFetch<FetchWrap<GiftListBody>>({
+            api: `/gift/lists`,
+            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 }
+        );
+    }
+}

+ 38 - 0
src/app/api/gift/my-gift-cards/route.ts

@@ -0,0 +1,38 @@
+import { NextRequest, NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+import { isBagistoError } from "@/utils/type-guards";
+import { getAuthToken } from "@/utils/helper";
+import type { GiftCardRespBody,FetchResult  } from '@/types/api/gift/my-gift-cards';
+export async function GET(req: NextRequest,{ params }: { params: Promise<{ id: string }> }) {
+    try {
+        const guestToken = getAuthToken(req);
+        const response = await restApiFetch<FetchResult<GiftCardRespBody>>({
+            api: `/gift/my-gift-cards`,
+            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 }
+        );
+    }
+}

+ 1 - 1
src/lib/restApiClient.ts

@@ -21,7 +21,7 @@ async function getCachedSession(): Promise<BagistoSession | null> {
   return session;
 }
 
-export async function clientFetch(apiUrl: string, options: RequestInit) {
+export async function clientFetch(apiUrl: string, options: RequestInit = {}) {
     // 请求的是nextjs的代理接口
 
     const session = await getCachedSession();

+ 25 - 0
src/types/api/gift/lists.ts

@@ -0,0 +1,25 @@
+// 单礼品项
+export interface GiftItem {
+  id: number;
+  name: string;
+  amount: number;
+  points: number;
+  num: number;
+  expirationdate: number;
+  channel: string;
+  notes: string;
+  status: boolean;
+}
+
+// body整体结构
+export interface GiftListBody {
+  success: boolean;
+  message: string;
+  data: Record<string, GiftItem>; // key:"1","2"...
+}
+
+// 请求外层结构
+export type FetchWrap<T> = {
+  status: number;
+  body: T;
+};

+ 24 - 0
src/types/api/gift/my-gift-cards.ts

@@ -0,0 +1,24 @@
+// 单张礼品卡结构
+export interface GiftCardItem {
+  id: number;
+  giftcard_number: string;
+  giftcard_amount: string;
+  remaining_giftcard_amount: string;
+  formatted_remaining_giftcard_amount: string;
+  used_giftcard_amount: string;
+  expirationdate: string;
+  giftcard_status: string;
+}
+
+// 后端整体body结构
+export interface GiftCardRespBody {
+  success: boolean;
+  message: string;
+  data: GiftCardItem[];
+}
+
+// restApiFetch返回外层结构
+export interface FetchResult<T> {
+  status: number;
+  body: T;
+}

+ 1 - 1
src/utils/constants.ts

@@ -29,7 +29,7 @@ export const CHECKOUT = {
 export const HIDDEN_PRODUCT_TAG = "nextjs-frontend-hidden";
 export const DEFAULT_OPTION = "Default Title";
 export const BAGISTO_GRAPHQL_API_ENDPOINT = "/api/graphql";
-export const BAGISTO_REST_API_ENDPOINT = "/api/shop";
+export const BAGISTO_REST_API_ENDPOINT = "/api";
 
 /**
  * productJsonLd constant