CartContext.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'
  2. import Taro from '@tarojs/taro'
  3. export interface CartItem {
  4. id: number // 商品ID(当前子商品ID,或父商品ID如果无规格)
  5. parentGoodsId: number // 父商品ID,0表示无父商品(单规格商品)
  6. name: string // 商品名称(包含规格信息的完整名称)
  7. price: number // 商品价格
  8. image: string // 商品图片
  9. stock: number // 商品库存
  10. quantity: number // 购买数量
  11. }
  12. export interface CartState {
  13. items: CartItem[]
  14. totalAmount: number
  15. totalCount: number
  16. }
  17. interface CartContextType {
  18. cart: CartState
  19. addToCart: (item: CartItem) => void
  20. removeFromCart: (id: number) => void
  21. updateQuantity: (id: number, quantity: number) => void
  22. switchSpec: (cartItemId: number, newChildGoods: { id: number; name: string; price: number; stock: number; image?: string }, quantity?: number) => void
  23. clearCart: () => void
  24. isInCart: (id: number) => boolean
  25. getItemQuantity: (id: number) => number
  26. isLoading: boolean
  27. }
  28. const CartContext = createContext<CartContextType | undefined>(undefined)
  29. const CART_STORAGE_KEY = 'mini_cart'
  30. export const CartProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  31. const [cart, setCart] = useState<CartState>({
  32. items: [],
  33. totalAmount: 0,
  34. totalCount: 0
  35. })
  36. const [isLoading, setIsLoading] = useState(true)
  37. // 从本地存储加载购物车
  38. useEffect(() => {
  39. const loadCart = () => {
  40. try {
  41. const savedCart = Taro.getStorageSync(CART_STORAGE_KEY)
  42. if (savedCart && Array.isArray(savedCart.items)) {
  43. // 数据迁移:确保每个购物车项都有parentGoodsId字段
  44. const migratedItems = savedCart.items.map((item: any) => ({
  45. ...item,
  46. parentGoodsId: item.parentGoodsId !== undefined ? item.parentGoodsId : 0 // 旧数据默认为0(单规格商品)
  47. }))
  48. const totalAmount = migratedItems.reduce((sum: number, item: CartItem) =>
  49. sum + (item.price * item.quantity), 0)
  50. const totalCount = migratedItems.reduce((sum: number, item: CartItem) =>
  51. sum + item.quantity, 0)
  52. setCart({
  53. items: migratedItems,
  54. totalAmount,
  55. totalCount
  56. })
  57. }
  58. } catch (error) {
  59. console.error('加载购物车失败:', error)
  60. } finally {
  61. setIsLoading(false)
  62. }
  63. }
  64. loadCart()
  65. }, [])
  66. // 保存购物车到本地存储
  67. const saveCart = (items: CartItem[]) => {
  68. const totalAmount = items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
  69. const totalCount = items.reduce((sum, item) => sum + item.quantity, 0)
  70. const newCart = {
  71. items,
  72. totalAmount,
  73. totalCount
  74. }
  75. setCart(newCart)
  76. try {
  77. Taro.setStorageSync(CART_STORAGE_KEY, { items })
  78. } catch (error) {
  79. console.error('保存购物车失败:', error)
  80. }
  81. }
  82. // 添加商品到购物车,支持父商品和子商品
  83. // 注意:父子商品的租户一致性验证在API层面进行
  84. const addToCart = (item: CartItem) => {
  85. const existingItem = cart.items.find(cartItem => cartItem.id === item.id)
  86. if (existingItem) {
  87. // 如果商品已存在,增加数量
  88. const newQuantity = Math.min(existingItem.quantity + item.quantity, item.stock)
  89. if (newQuantity === existingItem.quantity) {
  90. Taro.showToast({
  91. title: '库存不足',
  92. icon: 'none'
  93. })
  94. return
  95. }
  96. const newItems = cart.items.map(cartItem =>
  97. cartItem.id === item.id
  98. ? { ...cartItem, quantity: newQuantity }
  99. : cartItem
  100. )
  101. saveCart(newItems)
  102. Taro.showToast({
  103. title: '已更新购物车',
  104. icon: 'success'
  105. })
  106. } else {
  107. // 添加新商品
  108. if (item.quantity > item.stock) {
  109. Taro.showToast({
  110. title: '库存不足',
  111. icon: 'none'
  112. })
  113. return
  114. }
  115. saveCart([...cart.items, item])
  116. }
  117. }
  118. // 从购物车移除商品
  119. const removeFromCart = (id: number) => {
  120. const newItems = cart.items.filter(item => item.id !== id)
  121. saveCart(newItems)
  122. Taro.showToast({
  123. title: '已移除商品',
  124. icon: 'success'
  125. })
  126. }
  127. // 更新商品数量
  128. const updateQuantity = (id: number, quantity: number) => {
  129. const item = cart.items.find(item => item.id === id)
  130. if (!item) return
  131. // 当数量小于等于0时,设为1而不是删除商品
  132. if (quantity <= 0) {
  133. quantity = 1
  134. }
  135. if (quantity > item.stock) {
  136. Taro.showToast({
  137. title: '库存不足',
  138. icon: 'none'
  139. })
  140. return
  141. }
  142. const newItems = cart.items.map(item =>
  143. item.id === id ? { ...item, quantity } : item
  144. )
  145. saveCart(newItems)
  146. }
  147. // 清空购物车
  148. const clearCart = () => {
  149. saveCart([])
  150. Taro.showToast({
  151. title: '已清空购物车',
  152. icon: 'success'
  153. })
  154. }
  155. // 检查商品是否在购物车中
  156. const isInCart = (id: number) => {
  157. return cart.items.some(item => item.id === id)
  158. }
  159. // 获取购物车中商品数量
  160. const getItemQuantity = (id: number) => {
  161. const item = cart.items.find(item => item.id === id)
  162. return item ? item.quantity : 0
  163. }
  164. // 切换购物车项规格
  165. const switchSpec = (
  166. cartItemId: number,
  167. newChildGoods: { id: number; name: string; price: number; stock: number; image?: string },
  168. quantity?: number
  169. ) => {
  170. try {
  171. const item = cart.items.find(item => item.id === cartItemId)
  172. if (!item) {
  173. console.error('切换规格失败:购物车项不存在', cartItemId)
  174. Taro.showToast({
  175. title: '商品不存在',
  176. icon: 'none'
  177. })
  178. return
  179. }
  180. // 检查是否是父商品(允许切换规格)
  181. if (item.parentGoodsId === 0) {
  182. console.error('切换规格失败:单规格商品不支持切换', cartItemId)
  183. Taro.showToast({
  184. title: '该商品不支持切换规格',
  185. icon: 'none'
  186. })
  187. return
  188. }
  189. // 检查新规格库存是否足够
  190. if (newChildGoods.stock <= 0) {
  191. console.error('切换规格失败:规格无库存', { newStock: newChildGoods.stock })
  192. Taro.showToast({
  193. title: '该规格已售罄',
  194. icon: 'none'
  195. })
  196. return
  197. }
  198. // 确定要使用的数量:如果提供了quantity参数则使用,否则使用原有数量
  199. const finalQuantity = quantity !== undefined ? quantity : item.quantity
  200. // 验证数量有效性
  201. if (finalQuantity <= 0) {
  202. console.error('切换规格失败:数量无效', { finalQuantity })
  203. Taro.showToast({
  204. title: '数量不能小于1',
  205. icon: 'none'
  206. })
  207. return
  208. }
  209. if (finalQuantity > newChildGoods.stock) {
  210. console.error('切换规格失败:库存不足', { finalQuantity, newStock: newChildGoods.stock })
  211. Taro.showToast({
  212. title: `规格库存不足,仅剩${newChildGoods.stock}件`,
  213. icon: 'none'
  214. })
  215. return
  216. }
  217. // 验证新规格数据完整性
  218. if (!newChildGoods.id || !newChildGoods.name || newChildGoods.price < 0) {
  219. console.error('切换规格失败:规格数据不完整', newChildGoods)
  220. Taro.showToast({
  221. title: '规格数据错误',
  222. icon: 'none'
  223. })
  224. return
  225. }
  226. // 创建更新后的购物车项
  227. const updatedItem: CartItem = {
  228. ...item,
  229. id: newChildGoods.id,
  230. name: newChildGoods.name,
  231. price: newChildGoods.price,
  232. stock: newChildGoods.stock,
  233. image: newChildGoods.image || item.image,
  234. quantity: finalQuantity
  235. }
  236. // 更新购物车
  237. const newItems = cart.items.map(cartItem =>
  238. cartItem.id === cartItemId ? updatedItem : cartItem
  239. )
  240. saveCart(newItems)
  241. Taro.showToast({
  242. title: '已切换规格',
  243. icon: 'success'
  244. })
  245. } catch (error) {
  246. console.error('切换规格时发生异常:', error)
  247. Taro.showToast({
  248. title: '切换规格失败,请重试',
  249. icon: 'none'
  250. })
  251. }
  252. }
  253. const value = {
  254. cart,
  255. addToCart,
  256. removeFromCart,
  257. updateQuantity,
  258. switchSpec,
  259. clearCart,
  260. isInCart,
  261. getItemQuantity,
  262. isLoading
  263. }
  264. return (
  265. <CartContext.Provider value={value}>
  266. {children}
  267. </CartContext.Provider>
  268. )
  269. }
  270. export const useCart = () => {
  271. const context = useContext(CartContext)
  272. if (context === undefined) {
  273. throw new Error('useCart must be used within a CartProvider')
  274. }
  275. return context
  276. }