Ver código fonte

购物车页添加未支付订单模块

fogwind 3 dias atrás
pai
commit
a0ae9cb3f8

+ 8 - 7
README.md

@@ -278,15 +278,15 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 
 ## TODO
 需要做的工作:
-1. 注册成功后需要走next-auth的登录流程
-2. 登录成功后需要合并购物车(在服务端完成) -- 待定
+1. 注册成功后需要走next-auth的登录流程 -- 完成
+2. 登录成功后需要合并购物车(在服务端完成) -- 前端合并,已完成
 
 3. createCart 接口报错 -- 已解决
 4. 由于apollo client缓存策略以及graphql代理接口问题导致获取不到数据 -- 已解决
      - 3和4的问题是相互关联的
 
 5. 尝试升级apollo client版本到v4 -- 完成
-6. jwt-cookie 放弃base64编码,尝试改为加密解密
+6. jwt-cookie 放弃base64编码,尝试改为加密解密 -- 不用了,已删除jwt-cookie 文件
 
 7. 登录状态点击退出登录后GetCartItem接口会报错 -- 待处理
   - 错误信息:
@@ -312,10 +312,10 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 13. 详情页缓存策略(不要缓存吧)
 14. getSession  过期提示重新登录;token校验(见第47条)
 15. checkout 页面 src\components\Portal.tsx (15:33) 会报 document is not defined -- 已解决
-16. redux cartDetail 里的billingAddress 和 shippingAddress 字段改到cart里的billingAddress 和 shippingAddress以及保存完地址后同步到redux
+16. redux cartDetail 里的billingAddress 和 shippingAddress 字段改到cart里的billingAddress 和 shippingAddress以及保存完地址后同步到redux -- 已完成
 17. redux 中 cartDetail 异步获取数据 https://chat.deepseek.com/share/76kj30h2cws9jcl4jt  -- 已完成
 18. 需要一个遮挡整个视口的loading组件  -- 已完成
-19. 结账页地址模块还差两个功能:登录用户从地址列表选择地址;未登录用户输入邮箱后判断是否注册过,如果注册过让用户登录;
+19. 结账页地址模块,登录用户从地址列表选择地址;--- 已完成
      - 电话区号切换还有问题
 20. 登录用户结账页支付失败后要跳转到支付失败页面提醒用户可以再次支付 -- 待定
 21. 游客结账页支付失败或者取消支付,先调paymentCallback 再调cancelOrder。但是不删除cookie里之前的cartToken --- 完成
@@ -332,7 +332,8 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理; -- 已完成
 31. 当前货币等采用redux管理;-- 废弃,已改为全局ConfigProvider管理
 32. 当前货币切换功能      --- 已完成
-33. 登录用户购物车页获取未支付订单
+33. 登录用户购物车页获取未支付订单 --- 已完成
+
 34. 结账页下单成功后清空redux购物车数据导致结账页重新渲染问题  -- 已解决
     - 通过修改结账页数据初始化逻辑解决
 35. 结账页默认设置运输方式问题(导致结算联动风暴)
@@ -355,7 +356,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
     对于需要token的接口:登录用户校验token; 游客只需要检查有没有token.
     ---- 47 条 请求方法已经扩展是否携带token的参数,但是不再添加对于token的有效性校验,以后端接口返回的错误为依据判断有效性
 48. 游客token的创建优化(想在服务端创建写入token)--- 已完成
-
+49. 结账页地址模块未登录用户输入邮箱后判断是否注册过,如果注册过让用户登录;
 
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054

+ 5 - 1
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx

@@ -140,9 +140,13 @@ export default function ContinueToPayOrderInfo({
                 <div className="relative w-full box-border pr-9 mt-3.5">
                     <div className="w-full box-border flex overflow-x-auto">
                         {productItems.map((product) => { 
+                            const baseImage = JSON.parse(product.baseImage);
                             return(
                                 <div key={product.id} className="relative flex-none w-16 h-21.5 mr-3 last:mr-0">
-                                    <Image src="https://cdn.asteriahair.com/media/catalog/product/6/_/6_3_2.png" alt="product" fill={true} />
+                                    <Image className="object-cover" fill
+                                        src={baseImage.medium_image_url}
+                                        alt={product.name} 
+                                    />
                                 </div>
                             )
                         })}

+ 5 - 1
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx

@@ -87,10 +87,14 @@ export default function ProductsModalPendingOrder({
       body={
         <div className="w-full px-4 pb-6">
             {products.map((product) => {
+                const baseImage = JSON.parse(product.baseImage);
                 return (
                     <div key={product.id} className="flex justify-between w-full box-border pb-4 border-b-1 border-[#f0f0f0] mt-4 last:border-0 last:pb-0 last:mt-0">
                         <div className="relative flex-none w-33 h-44 mr-2">
-                            <Image fill={true} src="https://cdn.asteriahair.com/media/catalog/product/6/_/6_3_2.png" alt={product.name} />
+                            <Image className="object-cover" fill
+                                src={baseImage.medium_image_url}
+                                alt={product.name} 
+                            />
                         </div>
                         <div className="flex-auto flex flex-col justify-between ">
                             <div>

+ 288 - 0
src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx

@@ -0,0 +1,288 @@
+"use client";
+
+import { useState } from "react";
+import dynamic from "next/dynamic";
+import Image from "next/image";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import { useRouter } from 'next/navigation';
+import { useCustomToast } from "@/utils/hooks/useToast";
+import {useConfig} from "@/utils/hooks/useConfig";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { OrderDetails, ProductsInOrderDetails } from "@/types/customer/type";
+import CommonModal from "@/components/theme/ui/CommonModal";
+const CountDown = dynamic(() => import("@/components/common/CountDown"), {
+  ssr: false,
+});
+export function UnpaidOrderSection({
+    orderDetail
+}: {
+    orderDetail: Omit<OrderDetails, 'addresses'>
+}) {
+    const { cancelOrder } = usePlaceOrder();
+    const router = useRouter();
+    const { showToast } = useCustomToast();
+    const {currencies,getCurrentCurrencyItem} = useConfig();
+    const currentCurrency = getCurrentCurrencyItem();
+    const currencySymbol = currentCurrency.symbol;
+    const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
+    const orderSymbol = orderCurrency?.symbol || currencySymbol;
+
+    const [isShowUpaidOrder, setIsShowUpaidOrder] = useState(true);
+    const [showProductsModal, setShowProductsModal] = useState(false);
+    const [showDetails, setShowDetails] = useState(false);
+
+    const countDownEnd = (new Date(orderDetail.createdAt)).getTime() + 24*3600*1000;
+    const productsData: ProductsInOrderDetails[] = orderDetail.items.edges.map((edge) => {
+        return edge.node;
+    });
+    const totalItemTxt = orderDetail.totalItemCount > 1 ? `${orderDetail.totalItemCount} items` :  `${orderDetail.totalItemCount} item`;
+
+
+    const cancelOrderHandler = async () => {
+        const confirmRes = await confirmDialog({
+            title: "Warning",
+            content: "Would you want to cancel the unpaid order?",
+        });
+        if(!confirmRes) return;
+        overlayLoading.start();
+        const res = await cancelOrder({
+            orderId: Number(orderDetail._id),
+            userManual: true
+        });
+        overlayLoading.stop();
+        if(!res.error) {
+            showToast('Unpaid order has been canceled!', 'success');
+            setIsShowUpaidOrder(false);
+        } else {
+            showToast(res.msg, 'danger');
+        }
+    };
+
+    // 倒计时结束不再显示未支付订单(取消未支付订单)
+    const handleCountDownEnd = () => { 
+        setIsShowUpaidOrder(false);
+    };
+    const openProductsModal = () => {
+        setShowProductsModal(true);
+    };
+    const closeProductsModal = () => {
+        setShowProductsModal(false);
+    };
+    const toContinuePay = () => {
+        if(isShowUpaidOrder) {
+            router.push("/checkout/continuetopay?orderid=" + encodeURIComponent(orderDetail._id));
+        }
+    };
+
+    const openDetails = () => {
+        setShowDetails(true);
+    };
+    const closeDetails = () => {
+        setShowDetails(false);
+    };
+    if(!isShowUpaidOrder) return null; 
+    return (<div className="w-full box-border p-4">
+        <div className="relative w-full box-border p-3 bg-ly-lightgray rounded-lg">
+            <div className="flex items-center w-full">
+                Unpaid Order ({totalItemTxt})
+                <div className="ml-2">
+                    <CountDown
+                        endTime={countDownEnd}
+                        onCountDownEnd={handleCountDownEnd}
+                        boxClassName={"flex items-center gap-2 flex-none text-ly-14"}
+                        timerClassName={"flex items-center justify-center w-6 h-6 bg-ly-middlegreen rounded-xs text-ly-12 font-medium text-white"}
+                    />
+                </div>
+            </div>
+            <div className="w-full mt-3 overflow-x-scroll" onClick={openProductsModal}>
+                <div className="w-full flex gap-3">
+                    {productsData.map((product) => {
+                        const baseImage = JSON.parse(product.baseImage);
+                        return(
+                        <div className="relative w-16.5 h-22 flex-none" key={product.id}>
+                            <Image className="object-cover" fill
+                                src={baseImage.medium_image_url}
+                                alt={product.name}
+                            />
+                        </div>
+                        );
+                    })}
+
+                </div>
+            </div>
+
+            <div className=" flex items-center justify-between w-full mt-3">
+                <span className="text-ly-14 font-medium">Total: {orderSymbol}{orderDetail.grandTotal}</span>
+                <button className="flex-none flex justify-center items-center px-2 h-8 text-ly-14 text-white bg-ly-middlegreen rounded-md"
+                    onClick={toContinuePay}
+                >
+                    Pay Now
+                </button>
+            </div>
+
+            <button className="absolute top-3 right-3 w-6 h-6" onClick={cancelOrderHandler}>
+                <svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" 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="M7 7L17 17"></path><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.2" strokeLinejoin="round" strokeLinecap="round"  d="M7 17L17 7"></path></svg>
+            </button>
+        </div>
+
+        <CommonModal
+            isOpen={showProductsModal}
+            onClose={closeProductsModal}
+            header={
+                <div className="w-full box-border p-4">
+                    <div className="w-full box-border flex justify-between">
+                        <span className="text-ly-16 font-medium">Order Summary (2 items)</span>
+                        <button className="w-6 h-6" onClick={closeProductsModal}>
+                            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
+                        </button>
+                    </div>
+                </div>
+            }
+            footer={
+                <div className="w-full box-border p-4 inset-shadow-gray-50">
+                    <div className="flex w-full justify-between items-center">
+                        <span className="text-ly-14 font-medium">Total:</span>
+                        <button onClick={openDetails} className="flex items-center text-ly-14 font-medium">
+                            {orderSymbol}{orderDetail.grandTotal}
+                            <svg className="flex-none w-6 h-6" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M18 9L12 15L6 9"></path></svg>
+                        </button>
+                    </div>
+                    <button className="w-full h-12 rounded-3xl mt-3 flex justify-center items-center bg-ly-middlegreen text-ly-16 font-medium text-white"
+                        onClick={toContinuePay}
+                    >
+                        Pay Now
+                    </button>
+                </div>
+            }
+            body={
+                <div className="relative box-border w-full h-auto px-4 pt-2 pb-8">
+                    {productsData.map((product) => {
+                        const baseImage = JSON.parse(product.baseImage);
+                        return(
+
+                            <div key={product.id} className="w-full flex gap-2 mt-4 first:mt-0 pb-4 border-b-1 border-b-[#f0f0f0] last:border-none">
+                                <div className="relative w-33 h-44 flex-none">
+                                    <Image fill className="object-cover" 
+                                        src={baseImage.medium_image_url}
+                                        alt={product.name}
+                                    />
+                                </div>
+                                <div className="w-full flex flex-col justify-between">
+                                    <div className="w-full">
+                                        <div className="w-full">
+                                            <div className="w-full line-clamp-2 text-ly-12 text-ly-placeholder leading-5">
+                                                {product.name}
+                                            </div>
+                                        </div>
+                                        <div className="w-full mt-2 text-ly-12 border-1 rounded-sm border-[#d9d9d9] px-2.5 py-1.5 leading-5">
+                                            {product.sku}
+                                        </div>
+                                    </div>
+                                    <div className="flex items-center justify-between">
+                                        <div className="flex items-center gap-1.5">
+                                            <span className="text-ly-14 font-bold">{orderSymbol}{product.price}</span>
+                                            {/* <span className="text-ly-12 text-[#d3d3d3] line-through">$200.12</span> */}
+                                        </div>
+
+                                        <span className="flex-none text-ly-14">x{product.qtyOrdered}</span>
+
+                                    </div>
+                                </div>
+                            </div>
+                        );
+                    })}
+
+                            
+                </div>
+            }
+        />
+
+        {/**金额明细 */}
+        <CommonModal
+            zIndex="z-51"
+            isOpen={showDetails}
+            onClose={closeDetails}
+            contentClassName={"top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 w-82 bg-white rounded-xl"}
+            body={
+                <div className="relative box-border w-full h-auto p-4">
+                    <div className="flex justify-between items-center text-ly-14 font-bold">
+                        Promotion Details
+                        <button className="flex-none w-6 h-6" onClick={closeDetails}>
+                            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
+                        </button>
+                    </div>
+                    <div className="w-full mt-4">
+
+                        <div className="flex justify-between mt-3 first:mt-0">
+                            <p className="text-ly-12 leading-ly-20">Subtotal</p>
+                            <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.subTotal}</p>
+                        </div>
+                        {orderDetail.rewardPointsEarned > 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">You Earn</p>
+                                <p className="text-ly-12 leading-ly-20">{orderDetail.rewardPointsEarned} Reward Points</p>
+                            </div>
+                        }
+                        {orderDetail.shippingAmount !== 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Shipping</p>
+                                <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.shippingAmount}</p>
+                            </div>
+                        }
+                        {Number(orderDetail?.shippingInsuranceAmount) > 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Shipping</p>
+                                <p className="text-ly-12 leading-ly-20">{orderSymbol}{Number(orderDetail.shippingInsuranceAmount)}</p>
+                            </div>
+                        }
+                        {orderDetail.taxAmount !== 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Tax</p>
+                                <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.taxAmount}</p>
+                            </div>
+                        }
+                        {(orderDetail.vipPlusAmount !== '0.00' && orderDetail.vipPlusAmount !== null)  &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Plus Fee</p>
+                                <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.vipPlusAmount}</p>
+                            </div>
+                        }
+                        {orderDetail.discountAmount !== 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Discount</p>
+                                <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.discountAmount}</p>
+                            </div>
+                        }
+                        {(orderDetail.giftcardAmount !== '0.00' && orderDetail.giftcardAmount !== null) &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Gift Card</p>
+                                <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.giftcardAmount}</p>
+                            </div>
+                        }
+                        {(orderDetail.vipDiscountAmount !== '0.00' && orderDetail.vipDiscountAmount !== null) &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Plus Save</p>
+                                <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.vipDiscountAmount}</p>
+                            </div>
+                        }
+                        {orderDetail.rewardPointsAmount > 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">You Redeem ({orderDetail.rewardPointsUsed} Reward Points)</p>
+                                <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.rewardPointsAmount}</p>
+                            </div>
+                        }
+
+                        <div className="flex justify-between mt-3 first:mt-0">
+                            <p className="text-ly-16 leading-ly-24">Grand Total</p>
+                            <p className="text-ly-16 leading-ly-24 font-bold">{orderSymbol}{orderDetail.grandTotal}</p>
+                        </div>
+                    </div>
+
+                            
+                </div>
+            }
+        />
+
+    </div>);
+}

+ 33 - 3
src/app/(checkout)/checkout/cart/page.tsx

@@ -1,12 +1,14 @@
 import { createHash } from "crypto";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
-import { GET_CART_ITEM } from "@/graphql";
+import { GET_CART_ITEM, GET_CUSTOMER_ORDERS } from "@/graphql";
 import { auth } from "@/utils/auth/auth-helper";
 import { GetCartItemData } from "@/types/cart/type";
+import { OrderListData, OrderListDataVariables, OrderDetails } from "@/types/customer/type";
 import TitleHeader from "@components/layout/navbar/TitleHeader";
 import { EmptyCart } from "./_components/EmptyCart";
 import { PromiseInCart } from "./_components/PromiseInCart";
 import { CartWrapper } from "./_components/CartWrapper";
+import { UnpaidOrderSection } from "./_components/UnpaidOrderSection";
 /**
  * 购物车页 
  * 登录用户的话,获取用户地址列表 从auth session 中获取email
@@ -22,8 +24,28 @@ export const dynamic = "force-dynamic";
 export default async function CheckoutPage() {
 
     const session = await auth(); // 游客是null
-    const isGuest = session? true : false;
-
+    const isGuest = session === null ? true : false;
+    let unpaidOrder: Omit<OrderDetails, 'addresses'> | null = null;
+    let unpaidOrderExpiried = false;
+    if(!isGuest) {
+        try {
+            const {data: ordersResponse } = await serverGraphqlFetch<OrderListData,OrderListDataVariables>({
+                query: GET_CUSTOMER_ORDERS,
+                variables: {
+                    first: 1,
+                    status: 'pending'
+                }
+            }); 
+            unpaidOrder = ordersResponse?.customerOrders?.edges[0]?.node ?? null;
+            if(unpaidOrder) {
+                const countDownEnd = (new Date(unpaidOrder.createdAt)).getTime() + 24*3600*1000;
+                unpaidOrderExpiried = countDownEnd <= (new Date()).getTime() ? true : false;
+            }
+            
+        } catch(err) {
+            console.error('get unpaid order:', err)
+        }
+    }
     const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
         query: GET_CART_ITEM
     });
@@ -38,6 +60,14 @@ export default async function CheckoutPage() {
         <>
             <TitleHeader title={title} />
             <PromiseInCart />
+            {(unpaidOrder && !unpaidOrderExpiried) &&
+
+                <UnpaidOrderSection 
+                    orderDetail={unpaidOrder}
+                />
+
+            }
+            
             {!cartDetails && 
                 <EmptyCart 
                     isGuest={isGuest}

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

@@ -68,7 +68,7 @@ export default function CancelOrFailure({
                         {isGuest ?
                             "payment cancelled! Your order has been cancelled."
                         :
-                            "Payment cancelled! You can try again."
+                            "Payment cancelled! You can continue to pay."
                         }
                         
                     </p>

+ 5 - 1
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -147,10 +147,14 @@ export default function OrderDetailWrapper({
 
             <div className="box-border px-4 w-full mt-6">
                 {productItems.map((product) => {
+                    const baseImage = JSON.parse(product.baseImage);
                     return (
                         <div key={product.id} className="w-full flex gap-2 pb-4 border-b-1 border-[#f0f0f0] last:border-0 last:pb-0">
                             <div className="relative w-33 h-44 flex-none">
-                                <Image fill src="https://cdn.alipearlhair.com/media/catalog/product/2/_/2_104_20.jpg" alt="logo" />
+                                <Image className="object-cover" fill
+                                    src={baseImage.medium_image_url}
+                                    alt={product.name} 
+                                />
                             </div>
                             <div className="flex-none w-50.75 flex flex-col justify-between">
                                 <div>

+ 0 - 1
src/app/(public)/product/_components/review/ReviewDetail.tsx

@@ -9,7 +9,6 @@ import {
 } from "@/utils/helper";
 import { isArray } from "@/utils/type-guards";
 import { Avatar } from "@heroui/avatar";
-// import { Tooltip } from "@heroui/tooltip";
 import clsx from "clsx";
 import React, { FC, useState } from "react";
 import { ProductReviewNode } from "@/components/catalog/type";

+ 0 - 123
src/components/catalog/product/ProductMoreDetail.tsx

@@ -1,123 +0,0 @@
-"use client";
-import { Accordion, AccordionItem } from "@heroui/accordion";
-import React, { FC } from "react";
-import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
-import Prose from "@/components/theme/search/Prose";
-import ReviewSection from "../review/ReviewSection";
-import ReviewDetail from "../review/ReviewDetail";
-import { additionalDataTypes } from "../type";
-
-export const ProductMoreDetails: FC<{
-  description: string;
-  additionalData: additionalDataTypes[];
-  productId: string;
-  reviews: any[];
-  totalReview: number;
-  expandedKeys: Set<string>;
-  setExpandedKeys: (keys: Set<string>) => void;
-}> = ({ description, additionalData, reviews, productId, totalReview, expandedKeys, setExpandedKeys }) => {
-
-  const filterAdditionalData = additionalData.filter((item) => item?.attribute?.isVisibleOnFront == "1");
-
-
-  return (
-    <div className="mt-7 sm:my-7">
-      <Accordion
-        itemClasses={{
-          base: "shadow-none  bg-neutral-100 dark:bg-neutral-800",
-        }}
-        className="px-0"
-        selectionMode="multiple"
-        showDivider={false}
-        variant="splitted"
-        selectedKeys={expandedKeys}
-         onSelectionChange={(keys) => setExpandedKeys(keys as Set<string>)}
-      >
-        <AccordionItem
-          key="1"
-          classNames={{
-            title: "text-start",
-            trigger: "cursor-pointer",
-          }}
-          indicator={({ isOpen }) =>
-            isOpen ? (
-              <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-            ) : (
-              <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-            )
-          }
-          aria-label="Description"
-          title="Description"
-        >
-          <Prose className="pb-2 text-selected-black dark:text-white font-light" html={description} />
-        </AccordionItem>
-
-        {filterAdditionalData.length > 0
-          ?
-          <AccordionItem
-            key="2"
-            classNames={{
-              title: "text-start",
-              trigger: "cursor-pointer",
-            }}
-            indicator={({ isOpen }) =>
-              isOpen ? (
-                <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-              ) : (
-                <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-              )
-            }
-            aria-label="Additional Information"
-            title="Additional Information"
-          >
-            <div className="grid max-w-max grid-cols-[auto_1fr] gap-x-8 gap-y-4 px-1 pb-2">
-              {filterAdditionalData?.map((item) => (
-                <React.Fragment key={item.label}>
-                  <div className="grid">
-                    <p className="text-base font-normal text-selected-black dark:text-white">
-                      {item?.attribute?.adminName}
-                    </p>
-                  </div>
-                  <div className="grid">
-                    <p className="text-base font-normal text-selected-black dark:text-white">
-                      {item?.value || "--"}
-                    </p>
-                  </div>
-                </React.Fragment>
-              ))}
-            </div>
-          </AccordionItem>
-
-          : null}
-        <AccordionItem
-          key={filterAdditionalData.length > 0 ? "3" : "2"}
-          classNames={{
-            title: "text-start",
-            trigger: "cursor-pointer",
-          }}
-          indicator={({ isOpen }) =>
-            isOpen ? (
-              <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-            ) : (
-              <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-            )
-          }
-          aria-label="Ratings"
-          title="Ratings"
-        >
-          {totalReview > 0 ? (
-            <>
-              <ReviewDetail
-                reviewDetails={reviews}
-                totalReview={totalReview}
-                productId={productId}
-              />
-            </>
-          ) : (
-            <ReviewSection productId={productId}  totalReview={totalReview} />
-          )}
-        </AccordionItem>
-      </Accordion>
-    </div>
-  );
-};

+ 0 - 204
src/components/catalog/review/ReviewDetail.tsx

@@ -1,204 +0,0 @@
-"use client";
-
-import { Rating } from "@components/common/Rating";
-import { GridTileImage } from "@components/theme/ui/grid/Tile";
-import { formatDate, getInitials, getReviews } from "@/utils/helper";
-import { isArray } from "@/utils/type-guards";
-import { Avatar } from "@heroui/avatar";
-import { Tooltip } from "@heroui/tooltip";
-import clsx from "clsx";
-import { FC, useState } from "react";
-import ReviewSection from "./ReviewSection";
-import { ProductReviewNode } from "@/components/catalog/type";
-
-interface ProductReviewEdge {
-  __typename: "ProductReviewEdge";
-  node: ProductReviewNode;
-}
-
-interface ReviewDetailProps {
-  reviewDetails: ProductReviewEdge[];
-  totalReview: number;
-  productId: string;
-}
-
-const ReviewDetail: FC<ReviewDetailProps> = ({
-  reviewDetails,
-  totalReview,
-  productId,
-}) => {
-  const [visibleCount, setVisibleCount] = useState(5);
-
-  const reviews: ProductReviewNode[] =
-    reviewDetails?.map((edge) => edge.node) || [];
-
-  const { reviewAvg, ratingCounts } = getReviews(reviews);
-
-  const visibleReviews = reviews.slice(0, visibleCount);
-
-  return (
-    <>
-      <div className="flex flex-col flex-wrap gap-x-5 sm:gap-x-10">
-        <div className="my-2 flex w-full flex-col flex-wrap justify-between gap-4 sm:flex-row sm:items-center min-[1350px]:flex-nowrap">
-          <div className="flex items-center gap-x-2">
-            <Rating
-              length={5}
-              size="size-5"
-              star={reviewAvg}
-              reviewCount={totalReview}
-            />
-          </div>
-
-          <div className="flex w-full max-w-[280px] overflow-hidden rounded-sm">
-            {Object.entries(ratingCounts)
-              .reverse()
-              .filter(([_, count]) => (count as number) > 0)
-              .map(([star, count]) => (
-                <div
-                  key={star}
-                  style={{ flex: count as number }}
-                  className="min-h-4"
-                >
-                  <Tooltip
-                    content={
-                      <p className="text-center">
-                        {star} Star <br /> {count as number}{" "}
-                        {(count as number) >= 2 ? "Reviews" : "Review"}
-                      </p>
-                    }
-                    placement="top"
-                    className="cursor-pointer"
-                  >
-                    <div
-                      className={clsx(
-                        "h-full w-full !cursor-pointer",
-                        star === "5"
-                          ? "bg-green-700"
-                          : star === "4"
-                            ? "bg-cyan-400"
-                            : star === "3"
-                              ? "bg-violet-600"
-                              : star === "2"
-                                ? "bg-yellow-400"
-                                : "bg-red-600",
-                      )}
-                    />
-                  </Tooltip>
-                </div>
-              ))}
-          </div>
-        </div>
-
-        <div className="flex w-full flex-1 flex-col gap-5 py-2 sm:pt-6">
-          {/* Scrollable Container */}
-          <div
-            className="max-h-[380px] overflow-y-auto pr-2 
-        scrollbar-thin scrollbar-track-transparent 
-        scrollbar-thumb-neutral-400 dark:scrollbar-thumb-neutral-600"
-          >
-            {visibleReviews &&
-              visibleReviews.map(
-                (
-                  {
-                    name,
-                    title,
-                    comment,
-                    createdAt,
-                    rating,
-                    images,
-                    customer,
-                  }: ProductReviewNode,
-                  index: number,
-                ) => (
-                  <div
-                    key={index}
-                    className={clsx("flex flex-col gap-y-2 py-3", {
-                      "border-b border-neutral-200 dark:border-neutral-700":
-                        visibleReviews.length > 1 &&
-                        index < visibleReviews.length - 1,
-                    })}
-                  >
-                    <h1 className="font-outfit text-xl font-medium text-black/[80%] dark:text-white">
-                      {title}
-                    </h1>
-                    <Rating
-                      className="my-1"
-                      length={5}
-                      size="size-5"
-                      star={rating}
-                    />
-
-                    <h2 className="font-outfit text-base font-normal text-black/[80%] dark:text-white">
-                      {comment}
-                    </h2>
-
-                    <div className="flex gap-4">
-                      <div className="flex w-full items-center gap-2">
-                        <Avatar
-                          className="h-[56px] min-w-[56px] border border-solid border-black/10 bg-white text-large dark:bg-neutral-900"
-                          name={getInitials(name)}
-                          src={customer?.imageUrl}
-                        />
-                        <div>
-                          <h1 className="font-outfit text-base font-medium text-black/80 dark:text-white">
-                            {name}
-                          </h1>
-                          <p className="text-base text-black/80 dark:text-white">
-                            {formatDate(createdAt)}
-                          </p>
-                        </div>
-                      </div>
-                    </div>
-
-                    {isArray(images) && images && images.length > 0 && (
-                      <div className="mt-2 flex h-full min-h-[50px] w-full max-w-[60px] flex-wrap gap-2">
-                        {images.map((img) => (
-                          <GridTileImage
-                            key={img.reviewId}
-                            fill
-                            alt={`${img.reviewId}-review`}
-                            className="rounded-lg"
-                            src={img.url}
-                          />
-                        ))}
-                      </div>
-                    )}
-                  </div>
-                ),
-              )}
-
-            {reviews.length > visibleCount && (
-              <div className="py-4">
-                <button
-                  onClick={() => setVisibleCount((prev) => prev + 5)}
-                  className="flex items-start gap-2 text-primary-600 cursor-pointer hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium transition-colors duration-200 group"
-                >
-                  <span>Load More</span>
-                  <svg
-                    className="w-4 h-4 transition-transform duration-200 group-hover:translate-x-1 mt-1"
-                    fill="none"
-                    stroke="currentColor"
-                    viewBox="0 0 24 24"
-                    xmlns="http://www.w3.org/2000/svg"
-                  >
-                    <path
-                      strokeLinecap="round"
-                      strokeLinejoin="round"
-                      strokeWidth={2}
-                      d="M9 5l7 7-7 7"
-                    />
-                  </svg>
-                </button>
-              </div>
-            )}
-          </div>
-        </div>
-        <div className="mx-auto ">
-          <ReviewSection productId={productId} totalReview={totalReview} />
-        </div>
-      </div>
-    </>
-  );
-};
-
-export default ReviewDetail;

+ 6 - 2
src/components/theme/ui/CommonModal.tsx

@@ -18,7 +18,8 @@ export default function CommonModal({
     animationEndCallback = () => {},
     header,
     body,
-    footer
+    footer,
+    zIndex,
 }: {
     isOpen: boolean;
     destroyOnClose?: boolean;
@@ -30,6 +31,7 @@ export default function CommonModal({
     header?: React.ReactNode;
     body: React.ReactNode;
     footer?: React.ReactNode;
+    zIndex?: string;
 }) {
     // show - 显示
     // outing - 隐藏动画中
@@ -145,7 +147,9 @@ export default function CommonModal({
         <Portal>
             <div 
                 ref={rootRef} 
-                className={clsx("w-full h-full backdrop-blur-md backdrop-saturate-150 bg-black/30 fixed inset-0 z-50 transition-all ease-in-out",{
+                className={clsx(
+                    "w-full h-full backdrop-blur-md backdrop-saturate-150 bg-black/30 fixed inset-0 transition-all ease-in-out",
+                    (zIndex ? zIndex : "z-50"),{
                     "block fade-in": rootVisible === "show",
                     "hidden": rootVisible === "hidden",
                     "fade-out": rootVisible === "outing"

+ 6 - 4
src/graphql/checkout/mutations/CreateCancelOrder.ts

@@ -1,13 +1,15 @@
 import { gql,TypedDocumentNode } from "@apollo/client";
-import { CreateCancelOrderData } from "@/types/checkout/type";
+import { CreateCancelOrderData,CreateCancelOrderVariables } from "@/types/checkout/type";
 
-export const CREATE_CANCEL_ORDER: TypedDocumentNode<CreateCancelOrderData> = gql`
+export const CREATE_CANCEL_ORDER: TypedDocumentNode<CreateCancelOrderData,CreateCancelOrderVariables> = gql`
   mutation CreateCancelOrder(
-      $orderId: Int
+      $orderId: Int!
+      $userManual: Boolean
   ) {
       createCancelOrder(
         input: {
-          orderId: $orderId
+          orderId: $orderId,
+          userManual: $userManual
         }
       ) {
           cancelOrder {

+ 1 - 1
src/graphql/customer/mutations/index.ts

@@ -2,4 +2,4 @@ export { CUSTOMER_REGISTRATION} from "./CustomerRegistration";
 export { CUSTOMER_LOGIN} from "./CustomerLogin";
 export { CUSTOMER_LOGOUT} from "./CustomerLogout";
 export { VERIFY_CUSTOMER} from "./VerifyCustomer";
-export {FORGET_PASSWORD} from "./ForgetPassword"
+export {FORGET_PASSWORD} from "./ForgetPassword";

+ 111 - 0
src/graphql/customer/query/GetCustomerOrders.ts

@@ -0,0 +1,111 @@
+
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { OrderListData, OrderListDataVariables } from "@/types/customer/type";
+
+export const GET_CUSTOMER_ORDERS: TypedDocumentNode<OrderListData,OrderListDataVariables> = gql`
+query GetCustomerOrders(
+    $status:String,
+    $first:Int, 
+    $after: String, 
+    $last:Int, 
+    $before: String
+) {
+    customerOrders(
+        status: $status,
+        first: $first, 
+        after: $after,
+        last: $last,
+        before: $before
+    ) {
+        pageInfo {
+          endCursor
+          startCursor
+          hasNextPage
+          hasPreviousPage
+        }
+        totalCount
+        edges {
+            cursor
+            node {
+                incrementId
+                id
+                _id
+                status
+                channelName
+                customerEmail
+                customerFirstName
+                customerLastName
+                payment {
+                    method
+                    methodTitle
+                }
+                shippingMethod
+                shippingTitle
+                couponCode
+                totalItemCount
+                totalQtyOrdered
+                grandTotal
+                baseGrandTotal
+                grandTotalInvoiced
+                grandTotalRefunded
+                subTotal
+                baseSubTotal
+                taxAmount
+                baseTaxAmount
+                discountAmount
+                baseDiscountAmount
+                shippingAmount
+                baseShippingAmount
+                vipPlusAmount
+                vipDiscountAmount
+                baseVipPlusAmount
+                baseVipDiscountAmount
+                rewardPointsUsed    
+                rewardPointsAmount
+                baseRewardPointsAmount
+                rewardPointsInvoiced
+                baseRewardPointsInvoiced
+                rewardPointsEarned
+                shippingInsuranceAmount
+                baseShippingInsuranceAmount
+                shippingInsuranceInvoiced
+                baseShippingInsuranceInvoiced
+                giftcardNumber
+                giftcardAmount
+                baseGiftcardAmount
+                baseCurrencyCode
+                channelCurrencyCode
+                orderCurrencyCode
+                items {
+                    edges {
+                        node {
+                            id
+                            sku
+                            type
+                            name
+                            baseImage
+                            additional
+                            qtyOrdered
+                            qtyShipped
+                            qtyInvoiced
+                            qtyCanceled
+                            qtyRefunded
+                            price
+                            basePrice
+                            total
+                            baseTotal
+                            discountPercent
+                            discountAmount
+                            baseDiscountAmount
+                        }
+                    }
+                }
+
+                createdAt
+                updatedAt
+        
+            }
+        }
+    }
+}
+`

+ 1 - 0
src/graphql/customer/query/GetOrderDetails.ts

@@ -61,6 +61,7 @@ export const GET_ORDER_DETAILS: TypedDocumentNode<OrderDetailsData> = gql`
           sku
           type
           name
+          baseImage
           additional
           qtyOrdered
           qtyShipped

+ 2 - 1
src/graphql/customer/query/index.ts

@@ -1,4 +1,5 @@
 export {
     GET_ORDER_DETAILS
 } from "./GetOrderDetails";
-export {GET_CUSTOMER_ADDRESS} from "./GetCustomerAddress";
+export {GET_CUSTOMER_ADDRESS} from "./GetCustomerAddress";
+export {GET_CUSTOMER_ORDERS} from "./GetCustomerOrders";

+ 0 - 47
src/lib/graphql-fetch.ts

@@ -52,31 +52,6 @@ export function getRevalidateTime(
   }
 }
 
-
-
-
-
-export function stableStringify(value: unknown): string {
-  if (value === null || typeof value !== "object") {
-    return JSON.stringify(value);
-  }
-
-  if (Array.isArray(value)) {
-    return `[${value.map(stableStringify).join(",")}]`;
-  }
-
-  const obj = value as Record<string, unknown>;
-  return `{${Object.keys(obj)
-    .sort()
-    .map(
-      (key) => `"${key}":${stableStringify(obj[key])}`
-    )
-    .join(",")}}`;
-}
-
-
-
-
 export interface GraphQLRequestOptions {
   tags?: string[];
   life?: CacheLifeOption;
@@ -153,25 +128,3 @@ export async function graphqlRequest<
   }
 }
 
-
-
-export async function graphqlRequestNoCache<
-  TData = unknown,
-  TVariables extends OperationVariables = OperationVariables
->(
-  query: DocumentNode,
-  variables?: TVariables,
-  options?: Omit<
-    GraphQLRequestOptions,
-    "noCache" | "tags" | "life"
-  >
-): Promise<GraphqlRequestResult<TData>> {
-  return graphqlRequest<TData, TVariables>(
-    query,
-    variables,
-    {
-      ...options,
-      noCache: true,
-    }
-  );
-}

+ 1 - 0
src/types/checkout/type.ts

@@ -345,6 +345,7 @@ export interface CreateCancelOrderData {
 }
 export interface CreateCancelOrderVariables {
   orderId: number;
+  userManual?: boolean; //userManual 区分是否是用户直接主动取消订单
 }
 
 

+ 24 - 0
src/types/customer/order.ts

@@ -3,6 +3,7 @@ export interface ProductsInOrderDetails {
     sku: string;
     type: string;
     name: string;
+    baseImage: string;
     additional: string;
     qtyOrdered: number;
     qtyShipped: number;
@@ -135,4 +136,27 @@ export interface OrderDetails {
 
 export interface OrderDetailsData {
   customerOrder: OrderDetails | null;
+}
+
+export interface OrderListDataVariables {
+    status?: string;
+    first?: number;
+    after?: string;
+    last?: number;
+    before?: string;
+}
+export interface OrderListData {
+    customerOrders: {
+        pageInfo: {
+          endCursor: string;
+          startCursor: string;
+          hasNextPage: boolean;
+          hasPreviousPage: boolean;
+        };
+        totalCount: number;
+        edges: {
+            cursor: string;
+            node: Omit<OrderDetails, 'addresses'> ;
+        }[];
+    }
 }

+ 2 - 0
src/types/customer/type.ts

@@ -4,6 +4,8 @@ export type {
     ProductsInOrderDetails,
     OrderDetails, 
     OrderDetailsData, 
+    OrderListData,
+    OrderListDataVariables,
     ProductItemAdditional,
     AddressListInOrderDetails,
     AddressInOrderDetails,

+ 11 - 1
src/utils/constants.ts

@@ -133,4 +133,14 @@ export const IS_VALID_INPUT = /^[a-zA-Z0-9\s]*$/;
 export const IS_VALID_ADDRESS = /^[a-zA-Z0-9\s,\/-]*$/;
 export const IS_VALID_PHONE = /^[0-9]{10}$/;
 export const IS_VALID_PHONECODE = /^(\+\d+\s|\+\d+\-\d+\s|\+\d+\-\d+and\d+\-\d+\s)/; // +23 / +1-234 / +1-809and1-829
-export const IS_VALID_FULL_PHONE = /^(\+\d+\s|\+\d+\-\d+\s|\+\d+\-\d+and\d+\-\d+\s)\d{9,12}$/;
+export const IS_VALID_FULL_PHONE = /^(\+\d+\s|\+\d+\-\d+\s|\+\d+\-\d+and\d+\-\d+\s)\d{9,12}$/;
+
+
+export const currencyCountryMap: Record<string, string> = {
+    "USD": "US",
+    "GBP": "GB",
+    "CAD": "CA",
+    "EUR": "",
+    "AUD": "AU",
+    "ZAR": "ZA"
+};

+ 5 - 2
src/utils/hooks/usePlaceOrder.ts

@@ -86,11 +86,14 @@ export const usePlaceOrder = () => {
         });
     };
 
-    // 取消订单
+    // 取消订单 userManual 区分是否是用户直接主动取消订单
     const cancelOrder = (params: CreateCancelOrderVariables) => { 
         return client.mutate({
             mutation: CREATE_CANCEL_ORDER,
-            variables: params
+            variables: {
+                orderId: params.orderId,
+                userManual: params.userManual ? true : false,
+            }
         }).then((res) => {
             const resData = res?.data?.createCancelOrder?.cancelOrder ?? null; 
             const result: CreateCancelOrderResult = { 

+ 11 - 1
src/utils/orderDetailTools.ts

@@ -24,4 +24,14 @@ export function getAddressFromOrderDetailAddressList(addressList: AddressListInO
 
     return res;
 
-}
+}
+export function getProductFinalPriceInOrder() {}
+// export function getProductAdditinalInOrder(itemAdditional: string) {
+//     const additinal = JSON.parse(itemAdditional);
+//     let optionsTxt = '';
+//     Object.keys(additinal.attributes).forEach((key) => {
+//         const att = additinal.attributes[key];
+//         optionsTxt = optionsTxt
+//     });
+//     return {}
+// }