我将电子商务的 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;
请帮忙!真的好久都解决不了
点击添加到购物车。浏览器中显示的购物车商品数量总是比预期的正确数量少一个。