import { useCallback, useEffect, useRef, useState } from 'react' import { View, Text } from '@tarojs/components' import { addCartApi, deleteCartItemApi, updateCartItemApi } from '@/services/cart' import useCartStore from '@/stores/cart/useCartStore' import { formatQuantity } from '@/utils/format' import type { Product, ProductCartPatch } from '@/types/product' import './index.less' /** 加减防抖间隔(ms):连续点击合并为一次提交 */ const DEBOUNCE_MS = 400 interface CartStepperProps { /** 商品行(使用 id / price / cart_id / cart_quantity) */ product: Product /** 服务端确认后的行数据回写(父组件更新列表项的 cart_id/cart_quantity) */ onSync: (productId: number, patch: ProductCartPatch) => void } /** * 商品行内购物车加减(商品列表 / 首页推荐共用,仅在 cart_quantity > 0 时由父组件渲染): * - 点击即时更新本地数量与悬浮球(乐观展示),防抖后提交服务端 * - 不在购物车(cart_id=0)→ POST /mini/cart 合并加购;已存在 → PUT 绝对数量;减到 0 → DELETE * (数量为 0 不能调 PUT,后端校验数量必须 > 0) * - 同一商品的提交串行执行,避免并发导致数量错乱 * - 失败回滚本地数量(请求层已 toast),并立即整体校准悬浮球 */ export default function CartStepper({ product, onSync }: CartStepperProps) { const applyDelta = useCartStore(s => s.applyDelta) const fetchSummary = useCartStore(s => s.fetchSummary) /** 本地编辑数量(乐观值;null = 展示服务端确认值) */ const [draft, setDraft] = useState(null) /** 最新待提交的目标数量 */ const targetRef = useRef(null) const timerRef = useRef | null>(null) /** 提交串行队列 */ const chainRef = useRef>(Promise.resolve()) /** 最新商品行快照(供防抖/串行回调读取服务端确认值,避免闭包过期) */ const productRef = useRef(product) productRef.current = product /** 卸载时清理防抖定时器 */ useEffect( () => () => { if (timerRef.current) clearTimeout(timerRef.current) }, [], ) /** 提交目标数量(串行执行;与服务器一致时跳过) */ const runSubmit = useCallback( async (target: number) => { const p = productRef.current const confirmed = Number(p.cart_quantity ?? 0) if (target === confirmed) return try { if (target <= 0) { if (p.cart_id) await deleteCartItemApi(p.cart_id) onSync(p.id, { cart_id: 0, cart_quantity: '0.00' }) } else if (p.cart_id) { const res = await updateCartItemApi(p.cart_id, target) onSync(p.id, { cart_id: p.cart_id, cart_quantity: res.data.quantity }) } else { // 未加购过:POST 合并加购,用返回的行 id 回写本地 cart_id const res = await addCartApi({ product_id: p.id, quantity: target }) onSync(p.id, { cart_id: res.data.id, cart_quantity: res.data.quantity }) } // 提交期间用户未再改动 → 本地数量落回服务端确认值(onSync 已回写,展示不变) setDraft(prev => (prev === target ? null : prev)) } catch { // 失败(超上限等,请求层已 toast):放弃后续目标,回滚本地展示并校准悬浮球 targetRef.current = null setDraft(null) fetchSummary().catch(() => {}) } }, [onSync, fetchSummary], ) /** 点击加/减:乐观更新本地数量与悬浮球,防抖后入队提交 */ const handleTap = useCallback( (delta: 1 | -1) => { const before = draft ?? Number(productRef.current.cart_quantity ?? 0) const after = Math.round(Math.max(0, before + delta) * 100) / 100 if (after === before) return setDraft(after) targetRef.current = after // 悬浮球乐观增减(金额按行内售价估算,防抖结束后由服务端汇总校准); // 数量跨过 0 时同步增减商品种数 const price = Number(productRef.current.price ?? 0) applyDelta({ quantity: delta, amount: Math.round(price * delta * 100) / 100, count: before === 0 && after > 0 ? 1 : before > 0 && after === 0 ? -1 : 0, }) if (timerRef.current) clearTimeout(timerRef.current) timerRef.current = setTimeout(() => { timerRef.current = null const target = targetRef.current if (target === null) return targetRef.current = null chainRef.current = chainRef.current.then(() => runSubmit(target)) }, DEBOUNCE_MS) }, [draft, applyDelta, runSubmit], ) const shown = draft ?? Number(product.cart_quantity ?? 0) return ( e.stopPropagation()}> handleTap(-1)}> {formatQuantity(shown)} handleTap(1)}> ) }