购物车商品数量总是比 React 模板中的正确数量少一个

问题描述 投票:0回答:0

我将电子商务的 Django Rest Framework 后端连接到购买的 React 模板。

后端运行良好。

前端有一个我无法摆脱的错误。第一次点击添加到购物车时,前端显示购物车为空。第二次点击加入购物车时,购物车不为空,购物车商品数量为1(正确数量为2,后台正确)。前端购物车的商品数量总是比正确的少一个。

这是我使用的代码。

api.js(直接与后端交互,这些功能已经过测试并且运行良好):

import axios from "axios";

const API_URL = "http://localhost:8000/";

const api = axios.create({
  baseURL: API_URL,
  timeout: 5000,
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
});

const getProducts = async () => {
  try {
    const response = await api.get("shop/products/");
    const products = response.data.map((product) => {
      const category = Array.isArray(product.category)
        ? product.category
        : [product.category];
      return {
        ...product,
        category,
      };
    });
    return products;
  } catch (error) {
    console.error(error);
  }
};

const getProduct = async (productId) => {
  try {
    const response = await axios.get(`${API_URL}/shop/products/${productId}`);
    const product = response.data;

    const category = Array.isArray(product.category)
      ? product.category
      : [product.category];

    return {
      ...product,
      category,
    };
  } catch (error) {
    console.error(error);
  }
};

const createCart = async () => {
  try {
    const response = await api.post("shop/carts/");
    const cart = response.data;
    return cart;
  } catch (error) {
    console.error(error);
  }
};

const getCart = async (cartId) => {
  try {
    const response = await api.get(`shop/carts/${cartId}/`);
    const cart = response.data;
    return cart;
  } catch (error) {
    console.error(error);
  }
};

const deleteCart = async (cartId) => {
  try {
    const response = await api.delete(`shop/carts/${cartId}/`);
    return response.data;
  } catch (error) {
    console.error(error);
  }
};

const createCartItem = async (cartId, productId, quantity) => {
  try {
    const response = await api.post(`shop/carts/${cartId}/items/`, {
      product_id: productId,
      quantity: quantity,
    });
    const createdCartItem = response.data;

    return createdCartItem;
  } catch (error) {
    console.error(error);
  }
};

const getCartItems = async (cartId) => {
  try {
    const response = await api.get(`shop/carts/${cartId}/items/`);
    const cartItems = response.data;
    return cartItems;
  } catch (error) {
    console.error(error);
  }
};

const getCartItem = async (cartId, cartItemId) => {
  try {
    const response = await api.get(`shop/carts/${cartId}/items/${cartItemId}/`);
    const cartItem = response.data;
    return cartItem;
  } catch (error) {
    console.error(error);
  }
};

const updateCartItem = async (cartId, cartItemId, newQuantity) => {
  try {
    const response = await api.patch(
      `shop/carts/${cartId}/items/${cartItemId}/`,
      { quantity: newQuantity }
    );
    const updatedCartItem = response.data;
    return updatedCartItem;
  } catch (error) {
    console.error(error);
  }
};

const deleteCartItem = async (cartId, cartItemId) => {
  try {
    const response = await api.delete(
      `shop/carts/${cartId}/items/${cartItemId}/`
    );
    return response.data;
  } catch (error) {
    console.error(error);
  }
};

async function clearCart(cartId) {
  try {
    const response = await api.post(`shop/carts/${cartId}/clear/`);

    if (response.status === 204) {
      return true;
    } else {
      throw new Error("Failed to clear the cart");
    }
  } catch (error) {
    console.error("Error clearing cart:", error);
    throw error;
  }
}

export {
  getProducts,
  getProduct,
  createCart,
  getCart,
  deleteCart,
  createCartItem,
  getCartItems,
  getCartItem,
  updateCartItem,
  deleteCartItem,
  clearCart,
};

ShoppingCart.js(api.js 和其他与购物车相关的模板文件之间的中介):

import {
  createCart,
  getCart,
  createCartItem,
  getCartItems,
  updateCartItem,
  deleteCartItem,
  clearCart,
} from "../api";

class ShoppingCart {
  constructor(options) {
    this.currency = options.currency;
    this.currency = options.currency;
    this.storage = options.storage;
    this.tax = options.tax;
    this.shippingFlatRate = options.shippingFlatRate;
    this.shippingQuantityRate = options.shippingQuantityRate;
    this.idKey = options.idKey;
    this.itemsKey = options.itemsKey;
    this.totalPriceKey = options.totalPriceKey;
    this.cartId = null;
    this.items = [];
    this.totalPrice = 0;
  }

  async load() {
    const cartId = window[this.storage].getItem(this.idKey);
    if (cartId) {
      const cart = await getCart(cartId);
      this.cartId = cart.id;
      this.items = cart.items;
      this.totalPrice = cart.total_price;
    } else {
      const cart = await createCart();
      this.cartId = cart.id;
      window[this.storage].setItem(this.idKey, this.cartId);
    }
  }

  async save() {
    if (!this.cartId) {
      const cart = await createCart();
      this.cartId = cart.id;
      window[this.storage].setItem(this.idKey, this.cartId);
    }

    const itemsCopy = [...this.items];
    const promises = itemsCopy.map((item) => {
      const quantity = item.quantity;
      if (item.deleted) {
        return Promise.resolve();
      }
      return updateCartItem(this.cartId, item.id, quantity);
    });

    await Promise.all(promises);

    await this.getItems();
    this.totalPrice = this.items.reduce(
      (total, item) => total + item.product.price * item.quantity,
      0
    );
  }

  async addItem(productId, quantity = 1) {
    const item = this.items.find((item) => item.product_id === productId);
    if (item) {
      item.quantity += quantity;
      await this.updateItemQuantity(item.id, item.quantity);
    } else {
      try {
        const cartItem = await createCartItem(this.cartId, productId, quantity);
        this.items = [...this.items, cartItem];
      } catch (error) {
        console.error("Error in createCartItem:", error);
      }
    }

    await this.save();
  }

  async removeItem(cartItemId) {
    try {
      await deleteCartItem(this.cartId, cartItemId);
      this.items = this.items.filter((item) => item.id !== cartItemId);
      await this.save();
    } catch (error) {
      console.error("Error in removeItem:", error);
    }
  }

  async updateItemQuantity(cartItemId, quantity) {
    const item = this.items.find((item) => item.id === cartItemId);
    if (item) {
      item.quantity = quantity;
      await updateCartItem(this.cartId, cartItemId, quantity);
      await this.save();
    }
  }

  async getItems() {
    this.items = await getCartItems(this.cartId);
  }

  async clearItems() {
    this.items = [];
    await clearCart(this.cartId);
  }
}

export default ShoppingCart;


cart-slice.js (edited template file):

import cogoToast from "cogo-toast";
import ShoppingCart from "../../redux/ShoppingCart";

const { createSlice } = require("@reduxjs/toolkit");

const cart = new ShoppingCart({
  currency: "EUR",
  storage: "localStorage",
  tax: 0.2,
  shippingFlatRate: 5,
  shippingQuantityRate: 2,
  idKey: "cartId",
  itemsKey: "cartItems",
  totalPriceKey: "cartTotalPrice",
});

cart.load(); 

const cartSlice = createSlice({
  name: "cart",
  initialState: {
    cartItems: [],
  },
  reducers: {
    addToCart(state, action) {
      const product = action.payload;
      const quantity = product.quantity ? product.quantity : 1;
      cart.addItem(product.id, quantity);
      state.cartItems = cart.items;
      cogoToast.success("Added To Cart", { position: "bottom-left" });
    },
    deleteFromCart(state, action) {
      const cartItemId = action.payload;
      cart.removeItem(cartItemId);
      state.cartItems = cart.items;
      cogoToast.error("Removed From Cart", { position: "bottom-left" });
    },
    decreaseQuantity(state, action) {
      const cartItemId = action.payload;
      const item = cart.getItem(cartItemId);
      if (item.quantity === 1) {
        cart.removeItem(cartItemId);
        cogoToast.error("Removed From Cart", { position: "bottom-left" });
      } else {
        cart.updateItemQuantity(cartItemId, item.quantity - 1);
      }
      state.cartItems = cart.items;
    },
    deleteAllFromCart(state) {
      const newState = { ...state };
      cart.clearItems();
      newState.cartItems = [];
      cogoToast.error("Cart Cleared", { position: "bottom-left" });
      return newState;
    },
  },
});

export const {
  addToCart,
  deleteFromCart,
  decreaseQuantity,
  deleteAllFromCart,
} = cartSlice.actions;

export default cartSlice.reducer;

Cart.js(编辑模板文件):

import { Fragment, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useLocation } from "react-router-dom";
import SEO from "../../components/seo";
import { getDiscountPrice } from "../../helpers/product";
import LayoutOne from "../../layouts/LayoutOne";
import Breadcrumb from "../../wrappers/breadcrumb/Breadcrumb";
import {
  addToCart,
  decreaseQuantity,
  deleteFromCart,
  deleteAllFromCart,
} from "../../store/slices/cart-slice";
import { cartItemStock } from "../../helpers/product";

const Cart = () => {
  let cartTotalPrice = 0;

  const [quantityCount] = useState(1);
  const dispatch = useDispatch();
  let { pathname } = useLocation();

  const currency = useSelector((state) => state.currency);
  const { cartItems } = useSelector((state) => state.cart);

  return (
    <Fragment>
      <SEO
        titleTemplate="Cart"
        description="Cart page of flone react minimalist eCommerce template."
      />

      <LayoutOne headerTop="visible">
        {/* breadcrumb */}
        <Breadcrumb
          pages={[
            { label: "Home", path: process.env.PUBLIC_URL + "/" },
            { label: "Cart", path: process.env.PUBLIC_URL + pathname },
          ]}
        />
        <div className="cart-main-area pt-90 pb-100">
          <div className="container">
            {cartItems && cartItems.length >= 1 ? (
              <Fragment>
                <h3 className="cart-page-title">Your cart items</h3>
                <div className="row">
                  <div className="col-12">
                    <div className="table-content table-responsive cart-table-content">
                      <table>
                        <thead>
                          <tr>
                            <th>Image</th>
                            <th>Product Name</th>
                            <th>Unit Price</th>
                            <th>Qty</th>
                            <th>Subtotal</th>
                            <th>action</th>
                          </tr>
                        </thead>
                        <tbody>
                          {cartItems.map((cartItem, key) => {
                            // console.log(cartItem);
                            const discountedPrice = getDiscountPrice(
                              cartItem.product.price,
                              cartItem.discount
                            );
                            const finalProductPrice = (
                              cartItem.product.price * currency.currencyRate
                            ).toFixed(2);
                            const finalDiscountedPrice = (
                              discountedPrice * currency.currencyRate
                            ).toFixed(2);

                            discountedPrice != null
                              ? (cartTotalPrice +=
                                  finalDiscountedPrice * cartItem.quantity)
                              : (cartTotalPrice +=
                                  finalProductPrice * cartItem.quantity);
                            return (
                              <tr key={key}>
                                <td className="product-thumbnail">
                                  <Link
                                    to={
                                      process.env.PUBLIC_URL +
                                      "/product/" +
                                      cartItem.product.id
                                    }
                                  >
                                    <img
                                      className="img-fluid"
                                      src={
                                        process.env.PUBLIC_URL +
                                        cartItem.product.images.image
                                      }
                                      alt=""
                                    />
                                  </Link>
                                </td>

                                <td className="product-name">
                                  <Link
                                    to={
                                      process.env.PUBLIC_URL +
                                      "/product/" +
                                      cartItem.product.id
                                    }
                                  >
                                    {cartItem.product.title}
                                  </Link>
                                  {cartItem.selectedProductColor &&
                                  cartItem.selectedProductSize ? (
                                    <div className="cart-item-variation">
                                      <span>
                                        Color: {cartItem.selectedProductColor}
                                      </span>
                                      <span>
                                        Size: {cartItem.selectedProductSize}
                                      </span>
                                    </div>
                                  ) : (
                                    ""
                                  )}
                                </td>

                                <td className="product-price-cart">
                                  {discountedPrice !== null ? (
                                    <Fragment>
                                      <span className="amount old">
                                        {currency.currencySymbol +
                                          finalProductPrice}
                                      </span>
                                      <span className="amount">
                                        {currency.currencySymbol +
                                          finalDiscountedPrice}
                                      </span>
                                    </Fragment>
                                  ) : (
                                    <span className="amount">
                                      {currency.currencySymbol +
                                        finalProductPrice}
                                    </span>
                                  )}
                                </td>

                                <td className="product-quantity">
                                  <div className="cart-plus-minus">
                                    <button
                                      className="dec qtybutton"
                                      onClick={() =>
                                        dispatch(decreaseQuantity(cartItem))
                                      }
                                    >
                                      -
                                    </button>
                                    <input
                                      className="cart-plus-minus-box"
                                      type="text"
                                      value={cartItem.quantity}
                                      readOnly
                                    />
                                    <button
                                      className="inc qtybutton"
                                      onClick={() =>
                                        dispatch(
                                          addToCart({
                                            ...cartItem,
                                            quantity: quantityCount,
                                          })
                                        )
                                      }
                                      disabled={
                                        cartItem !== undefined &&
                                        cartItem.quantity &&
                                        cartItem.quantity >=
                                          cartItemStock(
                                            cartItem,
                                            cartItem.selectedProductColor,
                                            cartItem.selectedProductSize
                                          )
                                      }
                                    >
                                      +
                                    </button>
                                  </div>
                                </td>
                                <td className="product-subtotal">
                                  {discountedPrice !== null
                                    ? currency.currencySymbol +
                                      (
                                        finalDiscountedPrice * cartItem.quantity
                                      ).toFixed(2)
                                    : currency.currencySymbol +
                                      (
                                        finalProductPrice * cartItem.quantity
                                      ).toFixed(2)}
                                </td>

                                <td className="product-remove">
                                  <button
                                    onClick={() =>
                                      dispatch(deleteFromCart(cartItem.id))
                                    }
                                  >
                                    <i className="fa fa-times"></i>
                                  </button>
                                </td>
                              </tr>
                            );
                          })}
                        </tbody>
                      </table>
                    </div>
                  </div>
                </div>
                <div className="row">
                  <div className="col-lg-12">
                    <div className="cart-shiping-update-wrapper">
                      <div className="cart-shiping-update">
                        <Link
                          to={process.env.PUBLIC_URL + "/shop-grid-standard"}
                        >
                          Continue Shopping
                        </Link>
                      </div>
                      <div className="cart-clear">
                        <button onClick={() => dispatch(deleteAllFromCart())}>
                          Clear Shopping Cart
                        </button>
                      </div>
                    </div>
                  </div>
                </div>

                <div className="row">
                  <div className="col-lg-4 col-md-6">
                    <div className="cart-tax">
                      <div className="title-wrap">
                        <h4 className="cart-bottom-title section-bg-gray">
                          Estimate Shipping And Tax
                        </h4>
                      </div>
                      <div className="tax-wrapper">
                        <p>
                          Enter your destination to get a shipping estimate.
                        </p>
                        <div className="tax-select-wrapper">
                          <div className="tax-select">
                            <label>* Country</label>
                            <select className="email s-email s-wid">
                              <option>Latvia</option>
                            </select>
                          </div>
                          <div className="tax-select">
                            <label>* Region / State</label>
                            <select className="email s-email s-wid">
                              <option>Latvia</option>
                            </select>
                          </div>
                          <div className="tax-select">
                            <label>* Zip/Postal Code</label>
                            <input type="text" />
                          </div>
                          <button className="cart-btn-2" type="submit">
                            Get A Quote
                          </button>
                        </div>
                      </div>
                    </div>
                  </div>

                  <div className="col-lg-4 col-md-6">
                    <div className="discount-code-wrapper">
                      <div className="title-wrap">
                        <h4 className="cart-bottom-title section-bg-gray">
                          Use Coupon Code
                        </h4>
                      </div>
                      <div className="discount-code">
                        <p>Enter your coupon code if you have one.</p>
                        <form>
                          <input type="text" required name="name" />
                          <button className="cart-btn-2" type="submit">
                            Apply Coupon
                          </button>
                        </form>
                      </div>
                    </div>
                  </div>

                  <div className="col-lg-4 col-md-12">
                    <div className="grand-totall">
                      <div className="title-wrap">
                        <h4 className="cart-bottom-title section-bg-gary-cart">
                          Cart Total
                        </h4>
                      </div>
                      <h5>
                        Total products{" "}
                        <span>
                          {currency.currencySymbol + cartTotalPrice.toFixed(2)}
                        </span>
                      </h5>

                      <h4 className="grand-totall-title">
                        Grand Total{" "}
                        <span>
                          {currency.currencySymbol + cartTotalPrice.toFixed(2)}
                        </span>
                      </h4>
                      <Link to={process.env.PUBLIC_URL + "/checkout"}>
                        Proceed to Checkout
                      </Link>
                    </div>
                  </div>
                </div>
              </Fragment>
            ) : (
              <div className="row">
                <div className="col-lg-12">
                  <div className="item-empty-area text-center">
                    <div className="item-empty-area__icon mb-30">
                      <i className="pe-7s-cart"></i>
                    </div>
                    <div className="item-empty-area__text">
                      No items found in cart <br />{" "}
                      <Link to={process.env.PUBLIC_URL + "/shop-grid-standard"}>
                        Shop Now
                      </Link>
                    </div>
                  </div>
                </div>
              </div>
            )}
          </div>
        </div>
      </LayoutOne>
    </Fragment>
  );
};

export default Cart;

MenuCart.js(编辑模板文件):

import { Fragment } from "react";
import { Link } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { getDiscountPrice } from "../../../helpers/product";
import { deleteFromCart } from "../../../store/slices/cart-slice";

const MenuCart = () => {
  const dispatch = useDispatch();
  const currency = useSelector((state) => state.currency);
  const { cartItems } = useSelector((state) => state.cart);
  let cartTotalPrice = 0;

  return (
    <div className="shopping-cart-content">
      {cartItems && cartItems.length > 0 ? (
        <Fragment>
          <ul>
            {cartItems.map((item, index) => {
              const discountedPrice = getDiscountPrice(
                item.price,
                item.discount
              );
              const finalProductPrice = (
                item.price * currency.currencyRate
              ).toFixed(2);
              const finalDiscountedPrice = (
                discountedPrice * currency.currencyRate
              ).toFixed(2);

              discountedPrice != null
                ? (cartTotalPrice += finalDiscountedPrice * item.quantity)
                : (cartTotalPrice += finalProductPrice * item.quantity);

              return (
                <li className="single-shopping-cart" key={index}>
                  <div className="shopping-cart-img">
                    <Link to={process.env.PUBLIC_URL + "/product/" + item.id}>
                      <img
                        alt=""
                        src={process.env.PUBLIC_URL + item.product.images.image}
                        className="img-fluid"
                      />
                    </Link>
                  </div>
                  <div className="shopping-cart-title">
                    <h4>
                      <Link to={process.env.PUBLIC_URL + "/product/" + item.id}>
                        {" "}
                        {item.name}{" "}
                      </Link>
                    </h4>
                    <h6>Qty: {item.quantity}</h6>
                    <span>
                      {discountedPrice !== null
                        ? currency.currencySymbol + finalDiscountedPrice
                        : currency.currencySymbol + finalProductPrice}
                    </span>
                    {item.selectedProductColor && item.selectedProductSize ? (
                      <div className="cart-item-variation">
                        <span>Color: {item.selectedProductColor}</span>
                        <span>Size: {item.selectedProductSize}</span>
                      </div>
                    ) : (
                      ""
                    )}
                  </div>
                  <div className="shopping-cart-delete">
                    <button
                      onClick={() => dispatch(deleteFromCart(item.cartItemId))}
                    >
                      <i className="fa fa-times-circle" />
                    </button>
                  </div>
                </li>
              );
            })}
          </ul>
          <div className="shopping-cart-total">
            <h4>
              Total :{" "}
              <span className="shop-total">
                {currency.currencySymbol + cartTotalPrice.toFixed(2)}
              </span>
            </h4>
          </div>
          <div className="shopping-cart-btn btn-hover text-center">
            <Link className="default-btn" to={process.env.PUBLIC_URL + "/cart"}>
              view cart
            </Link>
            <Link
              className="default-btn"
              to={process.env.PUBLIC_URL + "/checkout"}
            >
              checkout
            </Link>
          </div>
        </Fragment>
      ) : (
        <p className="text-center">No items added to cart</p>
      )}
    </div>
  );
};

export default MenuCart;

请帮忙!真的好久都解决不了

点击添加到购物车。浏览器中显示的购物车商品数量总是比预期的正确数量少一个。

reactjs templates frontend e-commerce cart
© www.soinside.com 2019 - 2024. All rights reserved.