Parcourir la source

引入big.js包用于计算;支付方式展示图标和文案修改

fogwind il y a 2 jours
Parent
commit
7fad20f4b3

+ 5 - 1
README.md

@@ -486,4 +486,8 @@ Hook 是 React 在 render 过程中注册状态依赖的函数调用点,而不
 ## 关于set-state-in-effect
 1. https://yceffort.kr/en/2025/12/react-set-state-in-effect-lint-rule
 2. 使用useReducer规避待验证:https://stackoverflow.com/questions/79856121/calling-setstate-synchronously-within-an-effect-can-trigger-cascading-renders
-3. https://github.com/react/react/issues/34743
+3. https://github.com/react/react/issues/34743
+
+
+## 关于数学加减乘除等计算
+本项目中使用[big.js](https://mikemcl.github.io/big.js)进行数字的数学计算

+ 2 - 0
package.json

@@ -36,6 +36,7 @@
     "@react-aria/visually-hidden": "^3.8.31",
     "@react-types/shared": "^3.33.1",
     "@reduxjs/toolkit": "^2.10.1",
+    "big.js": "^7.0.1",
     "clsx": "^2.1.1",
     "framer-motion": "^12.23.24",
     "graphql": "^16.12.0",
@@ -52,6 +53,7 @@
   },
   "devDependencies": {
     "@tailwindcss/postcss": "^4",
+    "@types/big.js": "^7.0.0",
     "@types/node": "^20",
     "@types/react": "19.2.14",
     "@types/react-dom": "19.2.3",

+ 16 - 0
pnpm-lock.yaml

@@ -87,6 +87,9 @@ importers:
       '@reduxjs/toolkit':
         specifier: ^2.10.1
         version: 2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)
+      big.js:
+        specifier: ^7.0.1
+        version: 7.0.1
       clsx:
         specifier: ^2.1.1
         version: 2.1.1
@@ -130,6 +133,9 @@ importers:
       '@tailwindcss/postcss':
         specifier: ^4
         version: 4.3.0
+      '@types/big.js':
+        specifier: ^7.0.0
+        version: 7.0.0
       '@types/node':
         specifier: ^20
         version: 20.19.41
@@ -1680,6 +1686,9 @@ packages:
   '@tybys/wasm-util@0.10.2':
     resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
 
+  '@types/big.js@7.0.0':
+    resolution: {integrity: sha512-WfAGp7IbJvyB8EmWK4tJD24rJRAL6uVbw3LV/hJntFNam+os9KWKj0PzXo8rRRpjupYK8U0M8FoBB8dBhWF2dg==}
+
   '@types/estree@1.0.9':
     resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
 
@@ -1996,6 +2005,9 @@ packages:
     engines: {node: '>=6.0.0'}
     hasBin: true
 
+  big.js@7.0.1:
+    resolution: {integrity: sha512-iFgV784tD8kq4ccF1xtNMZnXeZzVuXWWM+ERFzKQjv+A5G9HC8CY3DuV45vgzFFcW+u2tIvmF95+AzWgs6BjCg==}
+
   brace-expansion@1.1.14:
     resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
 
@@ -5475,6 +5487,8 @@ snapshots:
       tslib: 2.8.1
     optional: true
 
+  '@types/big.js@7.0.0': {}
+
   '@types/estree@1.0.9': {}
 
   '@types/json-schema@7.0.15': {}
@@ -5788,6 +5802,8 @@ snapshots:
 
   baseline-browser-mapping@2.10.31: {}
 
+  big.js@7.0.1: {}
+
   brace-expansion@1.1.14:
     dependencies:
       balanced-match: 1.0.2

BIN
public/image/payment/applepay.png


BIN
public/image/payment/cash-icon.png


BIN
public/image/payment/credit-card.png


+ 1 - 0
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -414,6 +414,7 @@ export default function CheckoutWrapper({
                 {(!paymentMethodLoading && !paymentMethodError) &&
                 <PaymentMethodCheckout 
                     ref={methodPaymentRef}
+                    grandTotal={cartData.grandTotal}
                     paymentMethods={paymentMethodDatas} 
                     selectedPaymentMethod={selectedPaymentMethod}
                     onPaymentMethodChange={handlePaymentMethodChange}

+ 13 - 5
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -11,6 +11,7 @@ import {
     airwallexManager,
     useAirwallexCardState
 } from "@/lib/Airwallex/useAirwallexCard";
+import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
 import {RepayOrderPaymentMethodsData} from "@/types/checkout/type";
 import { OrderDetails } from "@/types/customer/type";
 import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
@@ -44,11 +45,11 @@ export function PaymentMethodContinueTpPay({
     const { paymentRepay, createPaymentCallback } = usePlaceOrder();
 
     const {loadSdk} = usePaymentSDKContext();
-    const {store,currencies} = useConfig();
-    const currentCurrencyCode  = store.currentCurrency;
+    const {currencies, getCurrentCurrencyItem} = useConfig();
+    const currentCurrency = getCurrentCurrencyItem();
 
     const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
-    const orderCurrencyCode = orderCurrency?.code || currentCurrencyCode;
+    const orderCurrencyCode = orderCurrency?.code || currentCurrency.code;
 
 
     const airwallexCardState = useAirwallexCardState();
@@ -422,6 +423,13 @@ export function PaymentMethodContinueTpPay({
             loadSdk(method);
         }
     };
+    const genetePaymentTips = (payment: string) => {
+        return getPaymentDescription({
+            payment: payment,
+            grandTotal: orderDetail.grandTotal,
+            symble: orderCurrency?.symbol ?? currentCurrency.symbol
+        })
+    };
 
 
     const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
@@ -440,12 +448,12 @@ export function PaymentMethodContinueTpPay({
                                 >
                                     <p className="pr-2 flex-none relative">
                                         <Image className="block h-8 w-auto"
-                                            src={item.icon || ''} alt={"Payment Method " + item.title}
+                                            src={getPaymentMethodLogo(item.method)} alt={"Payment Method " + item.title}
                                             width={32}
                                             height={32}
                                         />
                                     </p>
-                                    <p className="text-ly-12 text-[#666666] ml-1">{item.description}</p>
+                                    <p className="text-ly-12 text-[#666666] ml-1">{genetePaymentTips(item.method)}</p>
 
                                     <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
                                         <svg className="check w-4 h-4 hidden group-has-[:checked]:block" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect  x="1" y="1" width="14" height="14" rx="1"    fill="#000000" ></rect><path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5"  strokeLinecap="square"  d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>

+ 24 - 3
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -3,6 +3,8 @@
 import Image from "next/image";
 import { Ref, useEffect, useImperativeHandle } from "react";
 import { useForm, get } from "react-hook-form";
+import {useConfig} from "@/utils/hooks/useConfig";
+import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
 import { CheckoutPaymentMethod } from "@/types/checkout/type";
 import AirwallexCardInput from "./PaymentMethodAdditional/AirwallexCardInput";
 import {useAirwallexCardState} from "@/lib/Airwallex/useAirwallexCard";
@@ -20,14 +22,19 @@ export function PaymentMethodCheckout({
     ref,
     paymentMethods,
     selectedPaymentMethod,
+    grandTotal,
     onPaymentMethodChange,
 }: {
     ref: Ref<RefPaymentMethodsHandle>;
     paymentMethods: CheckoutPaymentMethod[];
     selectedPaymentMethod: string;
+    grandTotal: number;
     onPaymentMethodChange: (value: string) => Promise<boolean>;
 }) {
 
+    const {getCurrentCurrencyItem} = useConfig();
+    const currentCurrency = getCurrentCurrencyItem();
+    const currencySymbol = currentCurrency.symbol;
 
     const airwallexCardState = useAirwallexCardState();
   
@@ -91,7 +98,13 @@ export function PaymentMethodCheckout({
 
     const airwallexOtherInfoShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
 
-    
+    const genetePaymentTips = (payment: string) => {
+        return getPaymentDescription({
+            payment: payment,
+            grandTotal: grandTotal,
+            symble: currencySymbol
+        })
+    };
     return (<>
 
    
@@ -107,9 +120,17 @@ export function PaymentMethodCheckout({
                                     className="group box-border flex items-center relative bg-ly-lightgray p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
                                 >
                                     <p className="pr-2 flex-none relative">
-                                        <Image className="block h-8 w-auto" width={32} height={32} src={item.icon || ''} alt={"Payment Method " + item.title} />
+                                        {/**这里的width=32 height=32仅仅是让next.js有一个值计算图片的比例,并不能决定图片的渲染尺寸 */}
+                                        <Image className="block h-8 w-auto" 
+                                            width={32} 
+                                            height={32} 
+                                            src={getPaymentMethodLogo(item.method)} 
+                                            alt={"Payment Method " + item.title} 
+                                        />
+                                    </p>
+                                    <p className="text-ly-12 text-[#666666] ml-1">
+                                        {genetePaymentTips(item.method)}
                                     </p>
-                                    <p className="text-ly-12 text-[#666666] ml-1">{item.description}</p>
 
                                     <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
                                         <svg className="check w-4 h-4 hidden group-has-[:checked]:block" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect  x="1" y="1" width="14" height="14" rx="1"    fill="#000000" ></rect><path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5"  strokeLinecap="square"  d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>

+ 10 - 24
src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx

@@ -4,6 +4,7 @@ import {useState, useRef, useCallback, useLayoutEffect } from "react";
 import clsx from "clsx";
 import Image from "next/image";
 import { useRouter } from 'next/navigation';
+import Big from 'big.js';
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useAppDispatch } from "@/store/hooks";
 import { useAddProduct } from "@utils/hooks/useAddToCart";
@@ -12,7 +13,6 @@ import {useConfig} from "@/utils/hooks/useConfig";
 import {useLoginModal} from "@/providers/LoginModalProvider";
 import { CartDetail, CartItem } from "@/types/cart/type";
 import { formatCartDetail } from "@/utils/cartDetailTools";
-import { lyCompute } from "@/utils/helper";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 // import {BonusBuy} from "./BonusBuy";
@@ -23,34 +23,20 @@ import CommonModal from "@/components/theme/ui/CommonModal";
 import Portal from "@/components/common/portal/Portal";
 
 function computeGrandTotal(cartData: CartDetail) {
+
     // 剔除 运费,丢件险,积分抵扣金额 rewardPointsAmount 
-    const excludeAmount = lyCompute(cartData.shippingAmount, cartData.shippingInsuranceAmount, 'addition');
-    const grandAmount = lyCompute(
-        cartData.grandTotal, 
-        cartData.rewardPointsAmount, 
-        'addition'
-    );
-
-    return lyCompute(
-        grandAmount, 
-        excludeAmount, 
-        'subtract'
-    );
+    const excludeAmount = new Big(cartData.shippingAmount).plus(cartData.shippingInsuranceAmount);
+
+    const grandAmount = new Big(cartData.grandTotal).plus(cartData.rewardPointsAmount);
+
+    return Big(grandAmount).minus(excludeAmount).toFixed(2);
 }
 
 function computeSaveAmount(cartData: CartDetail) {
     // 优惠金额 礼品卡,plus
-    const x = lyCompute(
-        cartData.discountAmount, 
-        cartData.giftcardAmount, 
-        'addition'
-    );
-
-    return lyCompute(
-        cartData.vipPlusAmount, 
-        x, 
-        'addition'
-    );
+    const x = Big(cartData.discountAmount).plus(cartData.giftcardAmount);
+
+    return Big(cartData.vipPlusAmount).plus(x).toFixed(2);
 }
 
 /**

+ 11 - 0
src/utils/constants.ts

@@ -143,4 +143,15 @@ export const currencyCountryMap: Record<string, string> = {
     "EUR": "",
     "AUD": "AU",
     "ZAR": "ZA"
+};
+
+export const paymentMethodImageMap:Record<string, string> = {
+    klarna: '/image/payment/klarna.png',
+    afterpay: '/image/payment/afterpay.png',
+    applepay: '/image/payment/applepay.png',
+    paypal_smart_button: '/image/payment/paypal.png',
+    awxafterpay: '/image/payment/afterpay.png',
+    awxklarna: '/image/payment/klarna.png',
+    airwallex: '/image/payment/credit-card.png',
+    default: '/image/payment/cash-icon.png'
 };

+ 0 - 62
src/utils/helper.ts

@@ -297,68 +297,6 @@ export function buildProductFilters(params: {
     isFilterApplied,
   };
 }
-/**
- * 
- * @param flag addition x+y; subtract x-y; multiply x*y
- * @param x 
- * @param y 
- */
-export function lyCompute(
-    x: string | number, 
-    y: string | number,
-    flag: 'addition' | 'subtract' | 'multiply' = 'addition',
-) {
-    let res: number, final: string;
-    let xx = String(x).replaceAll(/\,/g,'');
-    let yy = String(y).replaceAll(/\,/g,'');
-  
-    const xxArr = xx.split('.');
-    const yyArr = yy.split('.');
-
-    let decimalPlaces = Math.max(
-        xxArr[1] ? xxArr[1].length :  0, 
-        yyArr[1] ? yyArr[1].length : 0
-    ); 
-    if(flag === 'multiply') {
-       decimalPlaces = (xxArr[1] ? xxArr[1].length :  0) + (yyArr[1] ? yyArr[1].length : 0);
-    }
-    if(flag === 'addition' || flag === 'subtract') {
-        if(xxArr[1]) {
-            xxArr[1] = xxArr[1].padEnd(decimalPlaces,'0');
-        } else {
-            xxArr[1] = '0'.repeat(decimalPlaces);
-        }
-
-        if(yyArr[1]) {
-            yyArr[1] = yyArr[1].padEnd(decimalPlaces,'0');
-        } else {
-            yyArr[1] = '0'.repeat(decimalPlaces);
-        }
-    }
-    
-
-    xx = xxArr.join('');
-    yy = yyArr.join('');
-
-    if(flag === 'multiply') {
-        res = Number(xx) * Number(yy);
-    } else if(flag === 'subtract') {
-        res = Number(xx) - Number(yy);
-    } else {
-        res = Number(xx) + Number(yy);
-    }
-
-    const isMinus = res < 0 ? true : false;
-    const resStr = String(Math.abs(res));
-
-    if(decimalPlaces >= resStr.length) {
-        final = '0.' + resStr.padStart(decimalPlaces,'0');
-    } else {
-        final = decimalPlaces ? (resStr.substring(0,resStr.length - decimalPlaces) + '.' + resStr.substring(resStr.length - decimalPlaces)) : resStr;
-    }
-    
-    return isMinus ? '-' + final : final;
-}
 
 // 获取/生成访客UUID
 export function getUuId() {

+ 25 - 0
src/utils/paymentTools.ts

@@ -0,0 +1,25 @@
+import Big from 'big.js';
+import {paymentMethodImageMap} from "@/utils/constants";
+
+export function getPaymentMethodLogo(payment: string) {
+    return paymentMethodImageMap[payment] || paymentMethodImageMap.default;
+}
+
+export function getPaymentDescription(param: {payment: string; grandTotal: number; symble: string;}) {
+    switch (param.payment) {
+        case 'klarna':
+        case 'awxklarna':
+        case 'awxafterpay':
+        case 'afterpay':
+            const n = Big(param.grandTotal).div(4).toFixed(2);
+            return `4 interest-free payments of ${param.symble}${n}`;
+        case 'applepay':
+            return 'Apple Pay';
+        case 'paypal_smart_button':
+            return 'Paypal';
+        case 'airwallex':
+            return '';
+        default:
+            return '';
+    }
+}