import { useCallback, useEffect, useRef, useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' import { View, Text, Image, Textarea, ScrollView } from '@tarojs/components' import { Button, Empty, Icon, Popup, Stepper } from '@antmjs/vantui' import useAuthStore from '@/stores/auth/useAuthStore' import useCartStore from '@/stores/cart/useCartStore' import { createOrderApi } from '@/services/order' import { getStoreInfoApi } from '@/services/store' import {formatRetailPrice, formatSpec} from '@/utils/format' import type { CartItem } from '@/types/cart' import type { StoreDetail } from '@/types/store' import './index.less' export default function CartPage() { const token = useAuthStore(s => s.token) const items = useCartStore(s => s.items) const totalQuantity = useCartStore(s => s.totalQuantity) const totalAmount = useCartStore(s => s.totalAmount) const loading = useCartStore(s => s.loading) const fetchCart = useCartStore(s => s.fetchCart) const updateQuantity = useCartStore(s => s.updateQuantity) const removeItem = useCartStore(s => s.removeItem) const clearCart = useCartStore(s => s.clearCart) const clearLocal = useCartStore(s => s.clearLocal) /** 本地编辑中的数量(输入即时生效,500ms 防抖后提交服务端) */ const [qtyMap, setQtyMap] = useState>({}) const debounceRef = useRef>>({}) /** 下单弹层 */ const [showOrder, setShowOrder] = useState(false) const [remark, setRemark] = useState('') const [submitting, setSubmitting] = useState(false) /** 配送信息(下单弹层展示,打开时拉取) */ const [storeInfo, setStoreInfo] = useState(null) const [storeLoading, setStoreLoading] = useState(false) /** 可购项(status=1) */ const purchasable = items.filter(item => item.status === 1) const hasInvalid = items.length > 0 && purchasable.length < items.length const loggedIn = !!token const goLogin = useCallback(() => { Taro.navigateTo({ url: '/pages/login/index' }) }, []) /** 拉取门店配送信息 */ const fetchStoreInfo = useCallback(() => { setStoreLoading(true) getStoreInfoApi() .then(res => setStoreInfo(res.data)) .catch(() => setStoreInfo(null)) .finally(() => setStoreLoading(false)) }, []) useDidShow(() => { // 未登录不请求接口,直接展示去登录空态(参考消息页) if (!loggedIn) return fetchCart().catch(() => {}) // 从门店信息编辑页返回且弹层仍打开时 → 刷新配送信息 if (showOrder) fetchStoreInfo() }) /** 同步本地编辑数量:删除不存在的项,保留在编数量 */ useEffect(() => { setQtyMap(prev => { const next: Record = {} for (const item of items) { if (prev[item.id] !== undefined) { next[item.id] = prev[item.id] } } return next }) }, [items]) /** 清理防抖定时器 */ useEffect(() => { const timers = debounceRef.current return () => { Object.values(timers).forEach(t => clearTimeout(t)) } }, []) /** 数量变更:本地即时更新 + 防抖提交 */ const handleQtyChange = useCallback( (item: CartItem, val: string | number) => { const value = Number(val) if (!value || value <= 0) return setQtyMap(prev => ({ ...prev, [item.id]: String(value) })) if (debounceRef.current[item.id]) { clearTimeout(debounceRef.current[item.id]) } debounceRef.current[item.id] = setTimeout(() => { updateQuantity(item.id, value).catch(() => { // 服务端拒绝(超上限等)→ 重新同步展示 fetchCart().catch(() => {}) }) }, 500) }, [updateQuantity, fetchCart], ) /** 删除单项 */ const handleRemove = useCallback( (item: CartItem) => { Taro.showModal({ title: '删除商品', content: `确定删除「${item.name}」吗?`, confirmColor: '#ee0a24', success: res => { if (res.confirm) { removeItem(item.id).catch(() => {}) } }, }) }, [removeItem], ) /** 清空购物车 */ const handleClear = useCallback(() => { Taro.showModal({ title: '清空购物车', content: '确定清空购物车吗?', confirmColor: '#ee0a24', success: res => { if (res.confirm) { clearCart().catch(() => {}) } }, }) }, [clearCart]) /** 打开下单弹层(同时拉取配送信息) */ const handleOrderTap = useCallback(() => { if (!purchasable.length) { Taro.showToast({ title: '没有可购买的商品', icon: 'none' }) return } setShowOrder(true) fetchStoreInfo() }, [purchasable.length, fetchStoreInfo]) /** 配送信息点击:拉取失败时重试,否则前往门店信息编辑页 */ const handleDeliveryTap = useCallback(() => { if (!storeLoading && !storeInfo) { fetchStoreInfo() return } Taro.navigateTo({ url: '/pages/store-info/index' }) }, [storeLoading, storeInfo, fetchStoreInfo]) /** 提交订单(金额一律服务端重算) */ const handleSubmitOrder = useCallback(async () => { if (submitting) return setSubmitting(true) try { await createOrderApi({ items: purchasable.map(item => ({ product_id: item.product_id, quantity: Number(qtyMap[item.id] ?? item.quantity), })), remark: remark.trim() || undefined, }) Taro.showToast({ title: '下单成功', icon: 'success' }) setShowOrder(false) setRemark('') // 本地先清空,再以服务端为准同步(服务端可能保留购物车内容) clearLocal() fetchCart().catch(() => {}) } catch { // 错误(已下架/未设等级价)已由 request 层 toast } finally { setSubmitting(false) } }, [submitting, purchasable, qtyMap, remark, clearLocal, fetchCart]) /** 当前展示的数量 */ const displayQty = (item: CartItem) => qtyMap[item.id] ?? item.quantity return ( {/* ========== 头部 ========== */} 购物车 {loggedIn && items.length > 0 && ( 清空 )} {/* ========== 列表 ========== */} {!loggedIn ? ( 去登录 ) : items.length === 0 ? ( loading ? ( 加载中... ) : ( ) ) : ( items.map(item => ( {item.name} {item.status === 0 && 已失效} {formatSpec(item.spec, item.unit)}{' '} 单价:{formatRetailPrice(item.price, item.spec)} {item.price_unit} ¥{item.price} {item.status === 1 ? ( handleQtyChange(item, e.detail)} /> ) : ( handleRemove(item)}>删除 )} {item.status === 1 && ( ¥{item.amount ?? '0.00'} handleRemove(item)}>删除 )} )) )} {loggedIn && hasInvalid && ( 部分商品已下架或未设置您所在等级的价格,不可下单 )} {/* ========== 底部结算栏 ========== */} {loggedIn && items.length > 0 && ( 合计{totalQuantity}件 ¥{totalAmount} )} {/* ========== 下单确认弹层 ========== */} setShowOrder(false)} > 确认下单 {/* 配送信息(点击前往修改门店信息) */} {storeLoading ? ( 配送信息加载中... ) : storeInfo ? ( {storeInfo.name} {(storeInfo.contact || storeInfo.phone) && ( {storeInfo.contact} {storeInfo.phone} )} {storeInfo.address ? ( {storeInfo.address} ) : ( 收货地址未填写,点击完善配送信息 )} ) : ( 未获取到门店信息,点击重试 )} {purchasable.map(item => ( {item.name} {formatSpec(item.spec, item.unit)} ×{displayQty(item)} ¥{item.amount ?? '0.00'} ))} 订单备注