| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- "use client";
- import { useState, useMemo } from "react";
- import { useRouter } from "next/navigation";
- import { Swiper, SwiperSlide } from 'swiper/react';
- import Big from 'big.js';
- import { useAppDispatch, useAppSelector } from "@/store/hooks";
- import { useCustomToast } from "@utils/hooks/useToast";
- import {
- closeAddToCartDialog,
- clearAddToCartProduct
- } from '@/store/slices/addToCartDialogSlice';
- import { useAddProduct } from "@utils/hooks/useAddToCart";
- import {useConfig} from "@/utils/hooks/useConfig";
- import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
- import type {
- ProductOption,
- ResolvedVariant
- } from "@/components/catalog/type";
- import CommonModal from "@/components/theme/ui/CommonModal";
- import ProductOptionsInAddToCartModal from "./ProductOptionsInAddToCartModal";
- import { Price } from "@components/theme/ui/Price";
- import {
- isValueAvailable,
- isOptionValueAvailable,
- getFirstAvailable,
- getAvailableVariants,
- isCombinationAvailable,
- formatFlexibleVariants,
- findNearestAvailableVariant
- } from "@/utils/variantTools";
- export default function AddToCartModal() {
- const router = useRouter();
- const dispatch = useAppDispatch();
- const {getCurrentCurrencyItem} = useConfig();
- const currentCurrency = getCurrentCurrencyItem();
- const {isOpen, product} = useAppSelector((state) => state.addToCartDialog);
- const { appendProductToCart } = useAddProduct();
- const { showToast } = useCustomToast();
- const images = useMemo(() => {
- return product?.images?.edges.map((item) => {
- return item.node;
- }) ?? [];
- },[product?.images]);
- const flexibleVariants = useMemo(() => {
- return formatFlexibleVariants(product?.flexibleVariants);
- },[product?.flexibleVariants]);
- const productOptions: ProductOption[] = useMemo(() => {
- const opts = product?.productOptions;
- return opts ? JSON.parse(opts) : [];
- },[product?.productOptions]);
- // 获取所有可用变体
- const availableVariants = useMemo(() => {
- return getAvailableVariants(flexibleVariants);
- },[flexibleVariants]);
- const isSaleable = product?.isSaleable;
- const [productQty] = useState(1);
- // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
- const [lastClickedOptionId, setLastClickedOptionId] = useState<number>(productOptions[0]?.id );
-
- // 当前选中的选项值映射:option_id -> value_id
- // useEffect和useMemo的执行时机不同,useMemo早于useEffect(useEffect在浏览器绘制后执行)执行,
- // useLayoutEffect早于useEffect执行,useLayoutEffect在浏览器绘制之前执行(官方说会阻塞浏览器绘制)
- const [selected, setSelected] = useState<Record<number, number>>(() => {
- // if (Object.keys(selected).length === 0) {
- let res: Record<number, number> = {};
- const defaultSelected = getFirstAvailable(productOptions, flexibleVariants);
- if (defaultSelected) {
- res = defaultSelected;
- } else {
- // 极端情况:没有任何可用组合,默认全选每组第一个(但仍需标记为不可用,由禁用逻辑处理)
- productOptions.forEach((opt) => {
- res[opt.id] = opt.values[0]?.id;
- });
- }
- console.log('set default selected ---- ', res);
- return res;
- // }
- });
- // 计算每个选项值的禁用状态
- const disabledMap = useMemo(() => {
- const map: Record<number, boolean> = {};
- if (Object.keys(selected).length === 0) return map; // 没有选中的项
- productOptions.forEach((option) => {
- option.values.forEach((value) => {
- // 规则:如果该选项组是最近点击的组,校验组中的值是否存在可用变体
- if (option.id === lastClickedOptionId) {
- // map[value.id] = false;
- map[value.id] = !isValueAvailable(value.id,availableVariants);
- } else {
- // 校验其他组的选项值是否可用
- map[value.id] = !isOptionValueAvailable(
- option.id,
- value.id,
- selected,
- productOptions,
- availableVariants
- );
- }
- });
- });
- return map;
- }, [selected, lastClickedOptionId, productOptions, availableVariants]);
-
- // 判断当前选中的组合是否可购买(用于控制购买按钮等)
- const isCurrentSelectionAvailable = useMemo(() => {
- let res = true;
- if(isSaleable !== '1') {
- res = false;
- } else {
- const selectedIds = Object.values(selected);
- if (selectedIds.length !== productOptions.length) {
- res = false
- } else {
- res = isCombinationAvailable(selectedIds, availableVariants);
- }
- }
- return res;
- }, [selected, productOptions, availableVariants, isSaleable]);
-
- // 获取当前选中变体和价格等信息
- const currentVariantInfo: {variant:ResolvedVariant | null; totalLinePrice: number; totalNowPrice: number; save: number;} = useMemo(() => {
- const selectedIds = Object.values(selected);
- let totalLinePrice = 0;
- let totalNowPrice = 0;
- let save = 0;
- const variant = flexibleVariants.find((v) => {
- const vIds = v.optionValues.map((ov) => ov.id);
- return selectedIds.length === vIds.length && selectedIds.every((id) => vIds.includes(id));
- });
- if (selectedIds.length === 0 || variant === undefined) {
- // 尚未初始化,返回一个安全占位对象
- return { variant: null, totalLinePrice: 0, totalNowPrice: 0, save: 0 };
- }
- if(variant.priceIndices && variant.priceIndices.length > 0) {
- totalLinePrice = Big(variant.priceIndices[0].regular_min_price).times(productQty).toNumber();
- totalNowPrice = Big(variant.priceIndices[0].min_price).times(productQty).toNumber();
- }
- if(totalLinePrice !== totalNowPrice) {
- save = Big(totalNowPrice).minus(totalLinePrice).toNumber();
- }
- return {
- variant,
- totalLinePrice: totalLinePrice,
- totalNowPrice: totalNowPrice,
- save: save
- };
- }, [selected, flexibleVariants, productQty]);
- const handleOptionClick = (result:{
- clickOptionId: number;
- clickValueId: number;
- }) => {
- // 更新前需要判断当前选中的组合是否可以购买,如果可以购买正常更新;如果不能购买查找可以购买的组合然后更新
- const {clickOptionId: optionId, clickValueId: valueId} = result;
- let newSelected: Record<number, number> = {...selected,[optionId]: valueId};
- const selectedValueIds = Object.values(newSelected);
- if(!isCombinationAvailable(selectedValueIds, availableVariants)) {
- newSelected = findNearestAvailableVariant(optionId,valueId,{...newSelected},productOptions,availableVariants);
- }
- setSelected(newSelected);
- // 记录当前点击的选项组
- setLastClickedOptionId(optionId);
- };
- async function addProductToCart() {
- if(!product) {
- showToast("Get product detail failed!", "danger");
- return;
- }
- overlayLoading.start();
- const params = {
- productId: product._id,
- quantity: productQty,
- variantId: currentVariantInfo.variant?._id,
- };
- const res = await appendProductToCart({
- productId: params.productId,
- quantity: params.quantity,
- variantId: params.variantId
- });
- console.log('onAddToCart run ----- 1',res);
- overlayLoading.stop();
- return res;
- }
- const addToCartHandler = async () => {
- if(!isCurrentSelectionAvailable) {
- showToast("The selected options are not available!", "warning");
- return;
- }
- if(currentVariantInfo.variant) {
- await addProductToCart();
- closeModal();
- }
- };
- const buyNowHandler = async () => {
- if(!isCurrentSelectionAvailable) {
- showToast("The selected options are not available!", "warning");
- return;
- }
- if(currentVariantInfo.variant) {
- const res = await addProductToCart();
- closeModal();
- if(res && !res.error) {
- const responseData = res.data;
- if(responseData && responseData.success) {
- router.push('/checkout');
- }
- }
- }
- };
- const closeModal = () => {
- dispatch(closeAddToCartDialog());
- setTimeout(() => {
- dispatch(clearAddToCartProduct());
- }, 300);
- };
- return (<>
- <CommonModal
- isOpen={isOpen}
- onClose={closeModal}
- fullyCustomer={true}
- contentClassName="absolute box-border transition-transform duration-300 ease-out bottom-0 left-0 w-full p-4 bg-white rounded-t-xl"
- body={<>
- <div className="flex justify-between items-center text-ly-16">
- Select Options
- <button className="flex-none w-6 h-6" onClick={closeModal} type="button">
- <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>
-
- {images.length > 0 && (
- <div className="w-full mt-4">
- <Swiper
- spaceBetween={10}
- slidesPerView={3}
- >
- {images.map((img) => {
- return (
- <SwiperSlide key={img.id} className="w-24 h-32">
- <img className="block h-full w-full object-cover" src={img.publicPath} />
- </SwiperSlide>
- );
- })}
- </Swiper>
- </div>
- )}
-
- <div className="box-border w-full mt-6 h-73.75 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-100">
- <div className="box-border w-full">
- <ProductOptionsInAddToCartModal
- productOptions={productOptions}
- selected={selected}
- disabledMap={disabledMap}
- onOptionClick={handleOptionClick}
- />
- </div>
- </div>
-
- <div className="box-border w-full border-t-1 border-[#f2f2f2] mt-1.5 pt-3">
- <div className="flex justify-between items-center">
- <Price
- className="text-ly-14 text-ly-gray line-through"
- amount={String(currentVariantInfo.totalLinePrice)}
- currencyCode={currentCurrency.code}
- />
- <div className="flex items-center">
- <Price
- className="text-ly-14 bg-ly-gold py-0.5 px-1"
- amount={String(currentVariantInfo.save)}
- currencyCode={currentCurrency.code}
- />
- <Price
- className="text-ly-20 font-bold ml-1.5"
- amount={String(currentVariantInfo.totalNowPrice)}
- currencyCode={currentCurrency.code}
- />
- </div>
- </div>
- {isCurrentSelectionAvailable ?
- <>
- <div className="flex justify-between gap-3 mt-3">
- <button className="flex-1 flex justify-center items-center h-12 text-ly-16 font-bold text-white rounded-3xl bg-ly-deepgreen"
- type="button"
- onClick={buyNowHandler}
- >
- Buy now
- </button>
- <button className="flex-1 flex justify-center items-center h-12 text-ly-16 font-bold text-white rounded-3xl bg-ly-middlegreen"
- type="button"
- onClick={addToCartHandler}
- >
- Add to bag
- </button>
- </div>
- {/* <button className="flex justify-center items-center w-full mt-3 h-12 text-ly-16 font-bold text-white rounded-3xl bg-[#ffc43a]" type="button">
- <img className="h-6 w-18.75" src="/image/payment/paypal-logo.svg" />
- </button> */}
- </>
- :
- <p className="text-center text-ly-14">The selected options are not available!</p>
- }
-
- </div>
-
-
- </>}
- />
- </>);
- }
|