Browse Source

完成购物车功能

master
科技小王子 1 month ago
parent
commit
796b5af67d
  1. 12
      src/api/system/userVerify/index.ts
  2. 60
      src/components/CartIcon.tsx
  3. 151
      src/hooks/useCart.ts
  4. 1
      src/pages/cart/cart.config.ts
  5. 270
      src/pages/cart/cart.tsx
  6. 6
      src/pages/index/Header.tsx
  7. 2
      src/pages/index/MySearch.tsx
  8. 23
      src/shop/goodsDetail/index.tsx
  9. 3
      tsconfig.json

12
src/api/system/userVerify/index.ts

@ -128,15 +128,3 @@ export async function submit(data: UserVerify) {
}
return Promise.reject(new Error(res.message));
}
export async function myTenantList(params: any) {
const res = await request.get<ApiResult<UserVerify>>(
'http://127.0.0.1:8080/api/v1/tenants',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

60
src/components/CartIcon.tsx

@ -0,0 +1,60 @@
import React from 'react';
import { Badge } from "@nutui/nutui-react-taro";
import { Cart } from "@nutui/icons-react-taro";
import Taro from '@tarojs/taro';
import { useCart } from "@/hooks/useCart";
interface CartIconProps {
style?: React.CSSProperties;
className?: string;
size?: number;
showBadge?: boolean;
onClick?: () => void;
}
const CartIcon: React.FC<CartIconProps> = ({
style,
className = '',
size = 16,
showBadge = true,
onClick
}) => {
const { cartCount } = useCart();
const handleClick = () => {
if (onClick) {
onClick();
} else {
// 默认跳转到购物车页面
Taro.switchTab({ url: '/pages/cart/cart' });
}
};
if (showBadge) {
return (
<div
className={className}
style={style}
onClick={handleClick}
>
<Badge value={cartCount} top="-2" right="2">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Cart size={size} />
</div>
</Badge>
</div>
);
}
return (
<div
className={className}
style={style}
onClick={handleClick}
>
<Cart size={size} />
</div>
);
};
export default CartIcon;

151
src/hooks/useCart.ts

@ -0,0 +1,151 @@
import { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';
// 购物车商品接口
export interface CartItem {
goodsId: number;
name: string;
price: string;
image: string;
quantity: number;
addTime: number;
}
// 购物车Hook
export const useCart = () => {
const [cartItems, setCartItems] = useState<CartItem[]>([]);
const [cartCount, setCartCount] = useState(0);
// 从本地存储加载购物车数据
const loadCartFromStorage = () => {
try {
const cartData = Taro.getStorageSync('cart_items');
if (cartData) {
const items = JSON.parse(cartData) as CartItem[];
setCartItems(items);
updateCartCount(items);
}
} catch (error) {
console.error('加载购物车数据失败:', error);
}
};
// 保存购物车数据到本地存储
const saveCartToStorage = (items: CartItem[]) => {
try {
Taro.setStorageSync('cart_items', JSON.stringify(items));
} catch (error) {
console.error('保存购物车数据失败:', error);
}
};
// 更新购物车数量
const updateCartCount = (items: CartItem[]) => {
const count = items.reduce((total, item) => total + item.quantity, 0);
setCartCount(count);
};
// 添加商品到购物车
const addToCart = (goods: {
goodsId: number;
name: string;
price: string;
image: string;
}, quantity: number = 1) => {
const newItems = [...cartItems];
const existingItemIndex = newItems.findIndex(item => item.goodsId === goods.goodsId);
if (existingItemIndex >= 0) {
// 如果商品已存在,增加数量
newItems[existingItemIndex].quantity += quantity;
} else {
// 如果商品不存在,添加新商品
const newItem: CartItem = {
goodsId: goods.goodsId,
name: goods.name,
price: goods.price,
image: goods.image,
quantity,
addTime: Date.now()
};
newItems.push(newItem);
}
setCartItems(newItems);
updateCartCount(newItems);
saveCartToStorage(newItems);
// 显示成功提示
Taro.showToast({
title: '加入购物车成功',
icon: 'success',
duration: 1500
});
};
// 从购物车移除商品
const removeFromCart = (goodsId: number) => {
const newItems = cartItems.filter(item => item.goodsId !== goodsId);
setCartItems(newItems);
updateCartCount(newItems);
saveCartToStorage(newItems);
};
// 更新商品数量
const updateQuantity = (goodsId: number, quantity: number) => {
if (quantity <= 0) {
removeFromCart(goodsId);
return;
}
const newItems = cartItems.map(item =>
item.goodsId === goodsId ? { ...item, quantity } : item
);
setCartItems(newItems);
updateCartCount(newItems);
saveCartToStorage(newItems);
};
// 清空购物车
const clearCart = () => {
setCartItems([]);
setCartCount(0);
Taro.removeStorageSync('cart_items');
};
// 获取购物车总价
const getTotalPrice = () => {
return cartItems.reduce((total, item) => {
return total + (parseFloat(item.price) * item.quantity);
}, 0).toFixed(2);
};
// 检查商品是否在购物车中
const isInCart = (goodsId: number) => {
return cartItems.some(item => item.goodsId === goodsId);
};
// 获取商品在购物车中的数量
const getItemQuantity = (goodsId: number) => {
const item = cartItems.find(item => item.goodsId === goodsId);
return item ? item.quantity : 0;
};
// 初始化时加载购物车数据
useEffect(() => {
loadCartFromStorage();
}, []);
return {
cartItems,
cartCount,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
getTotalPrice,
isInCart,
getItemQuantity,
loadCartFromStorage
};
};

1
src/pages/cart/cart.config.ts

@ -1,5 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '购物车',
navigationBarTextStyle: 'black',
navigationStyle: 'custom'
})

270
src/pages/cart/cart.tsx

@ -1,21 +1,40 @@
import {useEffect, useState} from "react"; // 添加 useCallback 引入
import {useEffect, useState} from "react";
import Taro, {useShareAppMessage, useShareTimeline} from '@tarojs/taro';
import {Space, NavBar} from '@nutui/nutui-react-taro'
import {Search, Received, Scan} from '@nutui/icons-react-taro'
import GoodsList from "@/components/GoodsList";
import {
NavBar,
Checkbox,
Image,
InputNumber,
Button,
Empty,
Divider
} from '@nutui/nutui-react-taro';
import {ArrowLeft, Del, Shopping} from '@nutui/icons-react-taro';
import {View} from '@tarojs/components';
import {CartItem, useCart} from "@/hooks/useCart";
function Cart() {
const [statusBarHeight, setStatusBarHeight] = useState<number>()
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
const [selectedItems, setSelectedItems] = useState<number[]>([]);
const [isAllSelected, setIsAllSelected] = useState(false);
const {
cartItems,
cartCount,
updateQuantity,
removeFromCart,
clearCart
} = useCart();
useShareTimeline(() => {
return {
title: '注册即可开通 - webSoft云应用'
title: '购物车 - 云上商店'
};
});
useShareAppMessage(() => {
return {
title: '注册即可开通 - webSoft云应用',
title: '购物车 - 云上商店',
success: function (res) {
console.log('分享成功', res);
},
@ -28,38 +47,235 @@ function Cart() {
useEffect(() => {
Taro.getSystemInfo({
success: (res) => {
setStatusBarHeight(res.statusBarHeight)
setStatusBarHeight(res.statusBarHeight || 0);
},
})
// 设置导航栏背景色(含状态栏)
});
// 设置导航栏背景色
Taro.setNavigationBarColor({
backgroundColor: '#ffffff', // 状态栏+导航栏背景色
frontColor: 'black', // 状态栏文字颜色(仅支持 black/white)
backgroundColor: '#ffffff',
frontColor: 'black',
});
}, []);
// 处理单个商品选择
const handleItemSelect = (goodsId: number, checked: boolean) => {
if (checked) {
setSelectedItems([...selectedItems, goodsId]);
} else {
setSelectedItems(selectedItems.filter(id => id !== goodsId));
setIsAllSelected(false);
}
};
// 处理全选
const handleSelectAll = (checked: boolean) => {
setIsAllSelected(checked);
if (checked) {
setSelectedItems(cartItems.map((item: CartItem) => item.goodsId));
} else {
setSelectedItems([]);
}
};
// 更新商品数量
const handleQuantityChange = (goodsId: number, value: number) => {
updateQuantity(goodsId, value);
};
// 删除商品
const handleRemoveItem = (goodsId: number) => {
Taro.showModal({
title: '确认删除',
content: '确定要删除这个商品吗?',
success: (res) => {
if (res.confirm) {
removeFromCart(goodsId);
setSelectedItems(selectedItems.filter(id => id !== goodsId));
}
}
});
};
// 计算选中商品的总价
const getSelectedTotalPrice = () => {
return cartItems
.filter((item: CartItem) => selectedItems.includes(item.goodsId))
.reduce((total: number, item: CartItem) => total + (parseFloat(item.price) * item.quantity), 0)
.toFixed(2);
};
// 去结算
const handleCheckout = () => {
if (selectedItems.length === 0) {
Taro.showToast({
title: '请选择要结算的商品',
icon: 'none'
});
return;
}
// 这里可以跳转到结算页面
Taro.showToast({
title: '跳转到结算页面',
icon: 'success'
});
}, []); // 新增: 添加滚动事件监听
};
// 检查是否全选
useEffect(() => {
if (cartItems.length > 0 && selectedItems.length === cartItems.length) {
setIsAllSelected(true);
} else {
setIsAllSelected(false);
}
}, [selectedItems, cartItems]);
return (
<>
<View style={{backgroundColor: '#f6f6f6', height: `${statusBarHeight}px`}}
className="fixed z-10 top-0 w-full"></View>
<NavBar
fixed={true}
style={{marginTop: `${statusBarHeight}px`}}
onBackClick={() => {
}}
left={
<>
<div className={'flex justify-between items-center w-full'}>
<Space>
<Search size={18} className={'mx-1'}/>
<Received size={18} className={'mx-1'}/>
<Scan size={18} className={'mx-1'}/>
</Space>
</div>
</>
left={<ArrowLeft onClick={() => Taro.navigateBack()}/>}
right={
cartItems.length > 0 && (
<Button
size="small"
type="primary"
fill="none"
onClick={() => {
Taro.showModal({
title: '确认清空',
content: '确定要清空购物车吗?',
success: (res) => {
if (res.confirm) {
clearCart();
setSelectedItems([]);
}
}
});
}}
>
</Button>
)
}
>
<span></span>
<span className="text-lg">({cartCount})</span>
</NavBar>
<GoodsList/>
{/* 购物车内容 */}
<View className="pt-24">
{cartItems.length === 0 ? (
// 空购物车
<View className="flex flex-col items-center justify-center h-96">
<Empty
image={<Shopping size={80}/>}
description="购物车空空如也"
>
<Button
type="primary"
size="small"
onClick={() => Taro.switchTab({url: '/pages/index/index'})}
>
</Button>
</Empty>
</View>
) : (
<>
{/* 商品列表 */}
<View className="bg-white">
{cartItems.map((item: CartItem, index: number) => (
<View key={item.goodsId}>
<View className="bg-white px-4 py-3 flex items-center gap-3">
{/* 选择框 */}
<Checkbox
checked={selectedItems.includes(item.goodsId)}
onChange={(checked) => handleItemSelect(item.goodsId, checked)}
/>
{/* 商品图片 */}
<Image
src={item.image}
width="80"
height="80"
radius="8"
className="flex-shrink-0"
/>
{/* 商品信息 */}
<View className="flex-1 min-w-0">
<View className="text-lg font-bold text-gray-900 truncate mb-1">
{item.name}
</View>
<View className="flex items-center justify-between">
<View className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-lg'}>{item.price}</span>
</View>
<View className="flex items-center gap-2">
<InputNumber
value={item.quantity}
min={1}
max={99}
onChange={(value) => handleQuantityChange(item.goodsId, Number(value))}
className="w-24"
/>
<Del className={'text-red-500'} size={16} onClick={() => handleRemoveItem(item.goodsId)}/>
</View>
</View>
</View>
</View>
{index < cartItems.length - 1 && <Divider/>}
</View>
))}
</View>
{/* 底部结算栏 */}
<View
className="fixed z-50 bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3 safe-area-bottom">
<View className="flex items-center justify-between">
<View className="flex items-center gap-3">
<Checkbox
checked={isAllSelected}
onChange={handleSelectAll}
>
</Checkbox>
<View className="text-sm text-gray-600">
{selectedItems.length}
</View>
</View>
<View className="flex items-center gap-4">
<View className="text-right">
<View className="text-xs text-gray-500">:</View>
<div className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-lg'}>{getSelectedTotalPrice()}</span>
</div>
</View>
<Button
type="primary"
size="large"
disabled={selectedItems.length === 0}
onClick={handleCheckout}
className="px-6"
>
({selectedItems.length})
</Button>
</View>
</View>
</View>
{/* 底部安全区域占位 */}
<View className="h-20"></View>
</>
)}
</View>
</>
);
}

6
src/pages/index/Header.tsx

@ -6,7 +6,7 @@ import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro'
import {getSiteInfo, getUserInfo, getWxOpenId} from "@/api/layout";
import {TenantId} from "@/utils/config";
import {getOrganization} from "@/api/system/organization";
import {myTenantList, myUserVerify} from "@/api/system/userVerify";
import {myUserVerify} from "@/api/system/userVerify";
import {CmsWebsite} from "@/api/cms/cmsWebsite/model";
import {User} from "@/api/system/user/model";
import './Header.scss';
@ -84,10 +84,6 @@ const Header = () => {
Taro.setStorageSync('RealName', data.realName)
}
})
//
myTenantList({page: 2, page_size: 50}).then(res => {
console.log(res, '...res...lei')
})
}
/* 获取用户手机号 */

2
src/pages/index/MySearch.tsx

@ -29,7 +29,7 @@ function MySearch(props) {
display: 'flex',
alignItems: 'center',
background: '#ffffff',
padding: '0 7px',
padding: '0 5px',
borderRadius: '20px',
marginTop: '100px',
}}

23
src/shop/goodsDetail/index.tsx

@ -8,6 +8,7 @@ import {getShopGoods} from "@/api/shop/shopGoods";
import {Swiper} from '@nutui/nutui-react-taro'
import {wxParse} from "@/utils/common";
import "./index.scss";
import {useCart} from "@/hooks/useCart";
const GoodsDetail = () => {
const [goods, setGoods] = useState<ShopGoods | null>(null);
@ -15,6 +16,21 @@ const GoodsDetail = () => {
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.id;
// 使用购物车Hook
const { cartCount, addToCart } = useCart();
// 处理加入购物车
const handleAddToCart = () => {
if (!goods) return;
addToCart({
goodsId: goods.goodsId!,
name: goods.name || '',
price: goods.price || '0',
image: goods.image || ''
});
};
useEffect(() => {
if (goodsId) {
getShopGoods(Number(goodsId))
@ -100,8 +116,9 @@ const GoodsDetail = () => {
height: "32px",
top: "50px",
right: "110px",
}}>
<Badge value={8} top="-2" right="2">
}}
onClick={() => Taro.navigateTo({url: '/pages/cart/cart'})}>
<Badge value={cartCount} top="-2" right="2">
<div style={{display: 'flex', alignItems: 'center'}}>
<Cart size={16}/>
</div>
@ -181,7 +198,7 @@ const GoodsDetail = () => {
</div>
<div className={'buy-btn mx-4'}>
<div className={'cart-add px-4 text-sm'}
onClick={() => Taro.showToast({title: '加入购物车成功', icon: 'success'})}>
onClick={() => handleAddToCart()}>
</div>
<div className={'cart-buy pl-4 pr-5 text-sm'}
onClick={() => Taro.navigateTo({url: '/shop/orderConfirm/index'})}>

3
tsconfig.json

@ -25,7 +25,8 @@
"@/components/*": ["./src/components/*"],
"@/utils/*": ["./src/utils/*"],
"@/assets/*": ["./src/assets/*"],
"@/api/*": ["./src/api/*"]
"@/api/*": ["./src/api/*"],
"@/hooks/*": ["./src/hooks/*"]
}
},
"include": ["./src", "./types"],

Loading…
Cancel
Save