Files
template-10584/src/shop/orderConfirm/index.tsx
赵忠林 37c2f030f2 feat(shop): 添加套票活动功能并优化购买数量控制
- 在仓库模型中添加状态字段
- 实现套票活动最低购买量的灵活配置,优先取模板配置值
- 优化数量输入逻辑,支持套票活动下的默认数量设置
- 改进优惠券加载逻辑,使用初始数量对应总价进行推荐
- 修复商品信息加载顺序,确保套票模板数据正确应用
- 更新支付工具类中的仓库类型引用
- 调整数量输入组件的最小值和禁用状态逻辑
2026-02-09 16:02:33 +08:00

1057 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {useEffect, useState} from "react";
import {
Image,
Button,
Cell,
CellGroup,
Input,
Space,
ActionSheet,
Popup,
InputNumber,
ConfigProvider
} from '@nutui/nutui-react-taro'
import {Location, ArrowRight} from '@nutui/icons-react-taro'
import Taro, {useDidShow} from '@tarojs/taro'
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods";
import {View, Text} from '@tarojs/components';
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
import './index.scss'
import Gap from "@/components/Gap";
import {selectPayment} from "@/api/system/payment";
import {Payment} from "@/api/system/payment/model";
import {PaymentHandler, PaymentType, buildSingleGoodsOrder} from "@/utils/payment";
import OrderConfirmSkeleton from "@/components/OrderConfirmSkeleton";
import CouponList from "@/components/CouponList";
import {CouponCardProps} from "@/components/CouponCard";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {getGltTicketTemplateByGoodsId} from "@/api/glt/gltTicketTemplate";
import type {GltTicketTemplate} from "@/api/glt/gltTicketTemplate/model";
import {
transformCouponData,
calculateCouponDiscount,
isCouponUsable,
getCouponUnusableReason,
sortCoupons,
filterUsableCoupons,
filterUnusableCoupons
} from "@/utils/couponUtils";
import type {ShopStore} from "@/api/shop/shopStore/model";
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
const OrderConfirm = () => {
const [goods, setGoods] = useState<ShopGoods | null>(null);
const [address, setAddress] = useState<ShopUserAddress>()
const [payments, setPayments] = useState<any[]>([])
const [payment, setPayment] = useState<Payment>()
const [isVisible, setIsVisible] = useState<boolean>(false)
const [quantity, setQuantity] = useState<number>(1)
const [orderRemark, setOrderRemark] = useState<string>('')
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string>('')
const [payLoading, setPayLoading] = useState<boolean>(false)
// 水票套票活动(若存在则按规则限制最小购买量等)
const [ticketTemplate, setTicketTemplate] = useState<GltTicketTemplate | null>(null)
// InputNumber 主题配置
const customTheme = {
nutuiInputnumberButtonWidth: '28px',
nutuiInputnumberButtonHeight: '28px',
nutuiInputnumberInputWidth: '40px',
nutuiInputnumberInputHeight: '28px',
nutuiInputnumberInputBorderRadius: '4px',
nutuiInputnumberButtonBorderRadius: '4px',
}
// 优惠券相关状态
const [selectedCoupon, setSelectedCoupon] = useState<CouponCardProps | null>(null)
const [couponVisible, setCouponVisible] = useState<boolean>(false)
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
const [couponLoading, setCouponLoading] = useState<boolean>(false)
// 门店选择:用于在下单页展示当前“已选门店”,并允许用户切换(写入 SelectedStore Storage
const [storePopupVisible, setStorePopupVisible] = useState(false)
const [stores, setStores] = useState<ShopStore[]>([])
const [storeLoading, setStoreLoading] = useState(false)
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.goodsId;
const isTicketTemplateActive =
!!ticketTemplate &&
ticketTemplate.enabled !== false &&
ticketTemplate.status !== 1 &&
ticketTemplate.deleted !== 1
// 套票活动最低购买量:优先取模板配置
const ticketMinBuyQty = (() => {
const n = Number(ticketTemplate?.minBuyQty)
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1
})()
const minBuyQty = isTicketTemplateActive ? ticketMinBuyQty : 1
const getGiftTicketQty = (buyQty: number) => {
if (!isTicketTemplateActive) return 0
const multiplier = Number(ticketTemplate?.giftMultiplier || 0)
const startSend = Number(ticketTemplate?.startSendQty || 0)
if (multiplier > 0) return Math.max(0, buyQty) * multiplier
return Math.max(0, startSend)
}
const loadStores = async () => {
if (storeLoading) return
try {
setStoreLoading(true)
const list = await listShopStore()
setStores((list || []).filter(s => s?.isDelete !== 1))
} catch (e) {
console.error('获取门店列表失败:', e)
setStores([])
Taro.showToast({title: '获取门店列表失败', icon: 'none'})
} finally {
setStoreLoading(false)
}
}
// @ts-ignore
const openStorePopup = async () => {
setStorePopupVisible(true)
if (!stores.length) {
await loadStores()
}
}
// 计算商品总价
const getGoodsTotal = () => {
if (!goods) return 0
const price = parseFloat(goods.price || '0')
// const total = price * quantity
// 🔍 详细日志,用于排查数值精度问题
// console.log('💵 商品总价计算:', {
// goodsPrice: goods.price,
// goodsPriceType: typeof goods.price,
// parsedPrice: price,
// quantity: quantity,
// total: total,
// totalFixed2: total.toFixed(2),
// totalString: total.toString()
// })
return price * quantity
}
// 计算优惠券折扣
const getCouponDiscount = () => {
if (!selectedCoupon || !goods) return 0
const total = getGoodsTotal()
return calculateCouponDiscount(selectedCoupon, total)
}
// 计算实付金额
const getFinalPrice = () => {
const total = getGoodsTotal()
const discount = getCouponDiscount()
return Math.max(0, total - discount)
}
const handleSelect = (item: any) => {
setPayment(payments.find(payment => payment.name === item.name))
setIsVisible(false)
}
// 处理数量变化
const handleQuantityChange = (value: string | number) => {
const fallback = isTicketTemplateActive ? minBuyQty : 1
const newQuantity = typeof value === 'string' ? parseInt(value, 10) || fallback : value
const finalQuantity = Math.max(fallback, Math.min(newQuantity, goods?.stock || 999))
setQuantity(finalQuantity)
// 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用
if (availableCoupons.length > 0) {
const newTotal = parseFloat(goods?.price || '0') * finalQuantity
const sortedCoupons = sortCoupons(availableCoupons, newTotal)
const usableCoupons = filterUsableCoupons(sortedCoupons, newTotal)
setAvailableCoupons(sortedCoupons)
// 检查当前选中的优惠券是否还可用
if (selectedCoupon && !isCouponUsable(selectedCoupon, newTotal)) {
setSelectedCoupon(null)
Taro.showToast({
title: '当前优惠券不满足使用条件,已自动取消',
icon: 'none'
})
// 🎯 自动推荐新的最优优惠券
if (usableCoupons.length > 0) {
const bestCoupon = usableCoupons[0]
const discount = calculateCouponDiscount(bestCoupon, newTotal)
if (discount > 0) {
setSelectedCoupon(bestCoupon)
Taro.showToast({
title: `已为您重新推荐最优优惠券,可省¥${discount.toFixed(2)}`,
icon: 'success',
duration: 3000
})
}
}
} else if (!selectedCoupon && usableCoupons.length > 0) {
// 🔔 如果没有选中优惠券但有可用的,推荐最优的
const bestCoupon = usableCoupons[0]
const discount = calculateCouponDiscount(bestCoupon, newTotal)
if (discount > 0) {
setSelectedCoupon(bestCoupon)
Taro.showToast({
title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`,
icon: 'success',
duration: 3000
})
}
} else if (selectedCoupon && usableCoupons.length > 0) {
// 🔍 检查是否有更好的优惠券
const bestCoupon = usableCoupons[0]
const currentDiscount = calculateCouponDiscount(selectedCoupon, newTotal)
const bestDiscount = calculateCouponDiscount(bestCoupon, newTotal)
// 如果有更好的优惠券优惠超过0.01元)
if (bestDiscount > currentDiscount + 0.01 && bestCoupon.id !== selectedCoupon.id) {
Taro.showModal({
title: '发现更优惠的优惠券',
content: `有更好的优惠券可用,额外节省¥${(bestDiscount - currentDiscount).toFixed(2)},是否更换?`,
success: (res) => {
if (res.confirm) {
setSelectedCoupon(bestCoupon)
Taro.showToast({
title: '优惠券已更换',
icon: 'success'
})
}
}
})
}
}
}
}
// 处理优惠券选择
const handleCouponSelect = (coupon: CouponCardProps) => {
const total = getGoodsTotal()
// 🔍 详细日志记录,用于排查问题
console.log('🎫 手动选择优惠券详细信息:', {
coupon: {
id: coupon.id,
title: coupon.title,
type: coupon.type,
amount: coupon.amount,
minAmount: coupon.minAmount,
status: coupon.status
},
orderInfo: {
goodsPrice: goods?.price,
quantity: quantity,
total: total,
totalFixed: total.toFixed(2)
},
validation: {
isUsable: isCouponUsable(coupon, total),
discount: calculateCouponDiscount(coupon, total),
reason: getCouponUnusableReason(coupon, total)
}
})
// 检查是否可用
if (!isCouponUsable(coupon, total)) {
const reason = getCouponUnusableReason(coupon, total)
// 🚨 记录手动选择失败的详细信息
console.error('🚨 手动选择优惠券失败:', {
reason,
coupon,
total,
minAmount: coupon.minAmount,
comparison: {
totalVsMinAmount: `${total} < ${coupon.minAmount}`,
result: total < (coupon.minAmount || 0)
}
})
Taro.showToast({
title: reason || '优惠券不可用',
icon: 'none'
})
return
}
setSelectedCoupon(coupon)
setCouponVisible(false)
Taro.showToast({
title: '优惠券选择成功',
icon: 'success'
})
}
// 取消选择优惠券
const handleCouponCancel = () => {
setSelectedCoupon(null)
Taro.showToast({
title: '已取消使用优惠券',
icon: 'success'
})
}
// 加载用户优惠券
const loadUserCoupons = async (totalOverride?: number) => {
try {
setCouponLoading(true)
// 使用新的API获取可用优惠券
const res = await getMyAvailableCoupons()
if (res && res.length > 0) {
// 转换数据格式
const transformedCoupons = res.map(transformCouponData)
// 按优惠金额排序
const total = totalOverride ?? getGoodsTotal()
const sortedCoupons = sortCoupons(transformedCoupons, total)
const usableCoupons = filterUsableCoupons(sortedCoupons, total)
setAvailableCoupons(sortedCoupons)
// 🎯 智能推荐:自动应用最优惠的可用优惠券
if (usableCoupons.length > 0 && !selectedCoupon) {
const bestCoupon = usableCoupons[0] // 已经按优惠金额排序,第一个就是最优的
const discount = calculateCouponDiscount(bestCoupon, total)
// 🔍 详细日志记录自动推荐的信息
console.log('🤖 自动推荐优惠券详细信息:', {
coupon: {
id: bestCoupon.id,
title: bestCoupon.title,
type: bestCoupon.type,
amount: bestCoupon.amount,
minAmount: bestCoupon.minAmount,
status: bestCoupon.status
},
orderInfo: {
goodsPrice: goods?.price,
quantity: quantity,
total: total,
totalFixed: total.toFixed(2)
},
validation: {
isUsable: isCouponUsable(bestCoupon, total),
discount: discount,
reason: getCouponUnusableReason(bestCoupon, total)
}
})
if (discount > 0) {
setSelectedCoupon(bestCoupon)
// 显示智能推荐提示
Taro.showToast({
title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`,
icon: 'success',
duration: 3000
})
}
}
// 🔔 优惠券提示:如果有可用优惠券,显示提示
if (usableCoupons.length > 0) {
console.log(`发现${usableCoupons.length}张可用优惠券,已为您推荐最优惠券`)
}
console.log('加载优惠券成功:', {
originalData: res,
transformedData: transformedCoupons,
sortedData: sortedCoupons,
usableCoupons: usableCoupons,
recommendedCoupon: usableCoupons[0] || null
})
} else {
setAvailableCoupons([])
console.log('暂无可用优惠券')
}
} catch (error) {
console.error('加载优惠券失败:', error)
setAvailableCoupons([])
Taro.showToast({
title: '加载优惠券失败',
icon: 'none'
})
} finally {
setCouponLoading(false)
}
}
/**
* 统一支付入口
*/
const onPay = async (goods: ShopGoods) => {
try {
setPayLoading(true)
// 基础校验
if (!address) {
Taro.showToast({
title: '请选择收货地址',
icon: 'error'
})
return;
}
if (!payment) {
Taro.showToast({
title: '请选择支付方式',
icon: 'error'
})
return;
}
// 水票套票活动:最小购买量校验
if (isTicketTemplateActive && quantity < minBuyQty) {
Taro.showToast({
title: `最低购买量:${minBuyQty}`,
icon: 'none'
})
return
}
// 库存校验
if (goods.stock !== undefined && quantity > goods.stock) {
Taro.showToast({
title: '商品库存不足',
icon: 'error'
})
return;
}
// 优惠券校验
if (selectedCoupon) {
const total = getGoodsTotal()
if (!isCouponUsable(selectedCoupon, total)) {
const reason = getCouponUnusableReason(selectedCoupon, total)
Taro.showToast({
title: reason || '优惠券不可用',
icon: 'error'
})
return;
}
} else {
// 🔔 支付前最后一次检查:提醒用户是否有可用优惠券
const total = getGoodsTotal()
const usableCoupons = filterUsableCoupons(availableCoupons, total)
if (usableCoupons.length > 0) {
const bestCoupon = usableCoupons[0]
const discount = calculateCouponDiscount(bestCoupon, total)
if (discount > 0) {
// 用模态框提醒用户
const confirmResult = await new Promise<boolean>((resolve) => {
Taro.showModal({
title: '发现可用优惠券',
content: `您有优惠券可使用,可省¥${discount.toFixed(2)},是否使用?`,
success: (res) => resolve(res.confirm),
fail: () => resolve(false)
})
})
if (confirmResult) {
setSelectedCoupon(bestCoupon)
// 🔄 使用优惠券后需要重新构建订单数据,这里直接递归调用支付函数
// 但要确保传递最新的优惠券信息
const currentPaymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
const updatedOrderData = buildSingleGoodsOrder(
goods.goodsId!,
quantity,
address.id,
{
comments: goods.name,
deliveryType: 0,
buyerRemarks: orderRemark,
couponId: parseInt(String(bestCoupon.id), 10)
}
);
console.log('🎯 使用推荐优惠券的订单数据:', updatedOrderData);
// 执行支付
await PaymentHandler.pay(updatedOrderData, currentPaymentType);
return; // 提前返回,避免重复执行支付
} else {
// 用户选择不使用优惠券,继续支付
}
}
}
}
// 构建订单数据
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
quantity,
address.id,
{
comments: '桂乐淘',
deliveryType: 0,
buyerRemarks: orderRemark,
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined
}
);
// 根据支付方式选择支付类型
const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
// 🔍 支付前的详细信息记录
console.log('💰 开始支付 - 详细信息:', {
orderData,
paymentType,
selectedCoupon: selectedCoupon ? {
id: selectedCoupon.id,
title: selectedCoupon.title,
type: selectedCoupon.type,
amount: selectedCoupon.amount,
minAmount: selectedCoupon.minAmount,
discount: getCouponDiscount()
} : null,
priceCalculation: {
goodsPrice: goods?.price,
quantity: quantity,
goodsTotal: getGoodsTotal(),
couponDiscount: getCouponDiscount(),
finalPrice: getFinalPrice()
},
couponValidation: selectedCoupon ? {
isUsable: isCouponUsable(selectedCoupon, getGoodsTotal()),
reason: getCouponUnusableReason(selectedCoupon, getGoodsTotal())
} : null
});
// 执行支付 - 移除这里的成功提示让PaymentHandler统一处理
await PaymentHandler.pay(orderData, paymentType);
// ✅ 移除双重成功提示 - PaymentHandler会处理成功提示
// Taro.showToast({
// title: '支付成功',
// icon: 'success'
// })
} catch (error: any) {
// return navTo('/user/order/order?statusFilter=0', true)
// console.error('支付失败:', error)
// 只处理PaymentHandler未处理的错误
// if (!error.handled) {
// let errorMessage = '支付失败,请重试';
//
// // 根据错误类型提供具体提示
// if (error.message?.includes('余额不足')) {
// errorMessage = '账户余额不足,请充值后重试';
// } else if (error.message?.includes('优惠券')) {
// errorMessage = '优惠券使用失败,请重新选择';
// } else if (error.message?.includes('库存')) {
// errorMessage = '商品库存不足,请减少购买数量';
// } else if (error.message?.includes('地址')) {
// errorMessage = '收货地址信息有误,请重新选择';
// } else if (error.message) {
// errorMessage = error.message;
// }
// Taro.showToast({
// title: errorMessage,
// icon: 'error'
// })
// console.log('跳去未付款的订单列表页面')
// }
} finally {
setPayLoading(false)
}
};
// 统一的数据加载函数
const loadAllData = async () => {
try {
setLoading(true)
setError('')
// 分别加载数据,避免类型推断问题
let goodsRes: ShopGoods | null = null
if (goodsId) {
goodsRes = await getShopGoods(Number(goodsId))
}
const [addressRes, paymentRes] = await Promise.all([
listShopUserAddress({isDefault: true}),
selectPayment({})
])
// 设置商品信息
// 查询当前商品是否存在水票套票活动(失败/无数据时不影响正常下单)
let tpl: GltTicketTemplate | null = null
if (goodsId) {
try {
tpl = await getGltTicketTemplateByGoodsId(Number(goodsId))
} catch (e) {
tpl = null
}
}
const tplActive =
!!tpl &&
tpl.enabled !== false &&
tpl.status !== 1 &&
tpl.deleted !== 1
const tplMinBuyQty = (() => {
const n = Number(tpl?.minBuyQty)
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1
})()
// 设置商品信息(若存在套票模板,则默认 canBuyNumber 使用模板最小购买量)
if (goodsRes) {
const patchedGoods: ShopGoods = { ...goodsRes }
if (tplActive && ((patchedGoods.canBuyNumber ?? 0) === 0)) {
patchedGoods.canBuyNumber = tplMinBuyQty
}
setGoods(patchedGoods)
// 设置默认购买数量:优先使用 canBuyNumber否则使用 1
const initQty = (patchedGoods.canBuyNumber ?? 0) > 0 ? (patchedGoods.canBuyNumber as number) : 1
setQuantity(initQty)
}
setTicketTemplate(tpl)
// 设置默认收货地址
if (addressRes && addressRes.length > 0) {
setAddress(addressRes[0])
}
// 设置支付方式
if (paymentRes && paymentRes.length > 0) {
setPayments(paymentRes.map((d) => ({
type: d.type,
name: d.name
})))
setPayment(paymentRes[0])
}
// 加载优惠券:使用“初始数量”对应的总价做推荐,避免默认数量变化导致推荐不准
if (goodsRes) {
const initQty = (() => {
const n = Number(goodsRes?.canBuyNumber)
if (Number.isFinite(n) && n > 0) return Math.floor(n)
if (tplActive) return tplMinBuyQty
return 1
})()
const total = parseFloat(goodsRes.price || '0') * initQty
await loadUserCoupons(total)
}
} catch (err) {
console.error('加载数据失败:', err)
setError('加载数据失败,请重试')
} finally {
setLoading(false)
}
}
useDidShow(() => {
// 返回/切换到该页面时,刷新一下当前已选门店
setSelectedStore(getSelectedStoreFromStorage())
loadAllData()
})
useEffect(() => {
loadAllData()
}, [goodsId]);
// 重新加载数据
const handleRetry = () => {
loadAllData()
}
// 错误状态
if (error) {
return (
<View className="order-confirm-page">
<View className="error-state">
<Text className="error-text">{error}</Text>
<Button onClick={handleRetry}></Button>
</View>
</View>
)
}
// 加载状态
if (loading || !goods) {
return <OrderConfirmSkeleton/>
}
return (
<div className={'order-confirm-page'}>
<CellGroup>
{
address && (
<Cell className={'address-bottom-line'} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
<Space>
<Location className={'text-gray-500'}/>
<View className={'flex flex-col w-full justify-between items-start'}>
<Space className={'flex flex-row w-full'}>
<View className={'flex-wrap text-nowrap whitespace-nowrap text-gray-500'}></View>
<View className={'font-medium text-sm flex items-center w-full'}>
<View
style={{width: '64%'}}>{address.province} {address.city} {address.region} {address.address}</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
</Space>
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
</View>
</Space>
</Cell>
)
}
{!address && (
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
<Space>
<Location/>
</Space>
</Cell>
)}
</CellGroup>
{/*<CellGroup>*/}
{/* <Cell*/}
{/* title={(*/}
{/* <View className="flex items-center gap-2">*/}
{/* <Shop className={'text-gray-500'}/>*/}
{/* <Text>门店</Text>*/}
{/* </View>*/}
{/* )}*/}
{/* extra={(*/}
{/* <View className={'flex items-center gap-2'}>*/}
{/* <View className={'text-gray-900'}>*/}
{/* {selectedStore?.name || '请选择门店'}*/}
{/* </View>*/}
{/* <ArrowRight className={'text-gray-400'} size={14}/>*/}
{/* </View>*/}
{/* )}*/}
{/* onClick={openStorePopup}*/}
{/* />*/}
{/*</CellGroup>*/}
<CellGroup>
<Cell key={goods.goodsId}>
<View className={'flex w-full justify-between gap-3'}>
<View>
<Image src={goods.image} mode={'aspectFill'} style={{
width: '80px',
height: '80px',
}} lazyLoad={false}/>
</View>
<View className={'flex flex-col w-full ml-2'} style={{width: '100%'}}>
<Text className={'font-medium w-full'}>{goods.name}</Text>
<Text className={'number text-gray-400 text-sm py-2'}>80g/</Text>
<View className={'flex justify-between items-center'}>
<Text className={'text-red-500'}>{goods.price}</Text>
<View className={'flex flex-col items-end gap-1'}>
<ConfigProvider theme={customTheme}>
<InputNumber
value={quantity}
min={isTicketTemplateActive ? minBuyQty : 1}
max={goods.stock || 999}
disabled={((goods.canBuyNumber ?? 0) !== 0) && !isTicketTemplateActive}
onChange={handleQuantityChange}
/>
</ConfigProvider>
{goods.stock !== undefined && (
<Text className={'text-xs text-gray-400'}>
{goods.stock}
</Text>
)}
{isTicketTemplateActive && (
<View className={'text-xs text-gray-500'}>
<Text>{minBuyQty}</Text>
<Text className={'ml-2'}>{getGiftTicketQty(quantity)}</Text>
</View>
)}
</View>
</View>
</View>
</View>
</Cell>
</CellGroup>
<CellGroup>
<Cell
title={'支付方式'}
extra={(
<View className={'flex items-center gap-2'}>
<View className={'text-gray-900'}>{payment?.name}</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
)}
onClick={() => setIsVisible(true)}
/>
</CellGroup>
<CellGroup>
<Cell
title={`商品总价(共${quantity}件)`}
extra={<View className={'font-medium'}>{getGoodsTotal().toFixed(2)}</View>}
/>
<Cell
title={'优惠券'}
extra={(
<View className={'flex justify-between items-center'}>
<View className={'text-red-500 text-sm mr-1'}>
{selectedCoupon ? `-¥${getCouponDiscount().toFixed(2)}` : '暂未使用'}
</View>
{(() => {
const usableCoupons = filterUsableCoupons(availableCoupons, getGoodsTotal())
if (usableCoupons.length > 0 && !selectedCoupon) {
return (
<View className={'flex items-center'}>
<View className={'bg-red-500 text-white text-xs px-2 py-1 rounded mr-2'}>
{usableCoupons.length}
</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
)
} else if (usableCoupons.length > 0) {
return (
<View className={'flex items-center'}>
<View className={'bg-green-500 text-white text-xs px-2 py-1 rounded mr-2'}>
</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
)
} else {
return <ArrowRight className={'text-gray-400'} size={14}/>
}
})()
}
</View>
)}
onClick={() => setCouponVisible(true)}
/>
<Cell title={'配送费'} extra={'¥0.00'}/>
<Cell extra={(
<View className={'flex items-end gap-2'}>
<Text></Text>
<Text className={'text-red-500 text-sm'}>{getCouponDiscount().toFixed(2)}</Text>
<Text className={'ml-2'}></Text>
<Text className={'text-gray-700 font-bold'} style={{fontSize: '18px'}}>{getFinalPrice().toFixed(2)}</Text>
</View>
)}/>
</CellGroup>
<CellGroup>
<Cell title={'订单备注'} extra={(
<Input
placeholder={'选填,请先和商家协商一致'}
style={{padding: '0'}}
value={orderRemark}
onChange={(value) => setOrderRemark(value)}
maxLength={100}
/>
)}/>
</CellGroup>
{ticketTemplate && (
<CellGroup>
<Cell extra={(
<div className={'text-red-500 text-sm'}>
1.20
2.
3.
</div>
)}/>
</CellGroup>
)}
{/* 支付方式选择 */}
<ActionSheet
visible={isVisible}
options={payments}
onSelect={handleSelect}
onCancel={() => setIsVisible(false)}
/>
{/* 优惠券选择弹窗 */}
<Popup
visible={couponVisible}
position="bottom"
onClose={() => setCouponVisible(false)}
style={{height: '60vh'}}
>
<View className="coupon-popup">
<View className="coupon-popup__header">
<Text className="text-sm"></Text>
<Button
size="small"
fill="none"
onClick={() => setCouponVisible(false)}
>
</Button>
</View>
<View className="coupon-popup__content">
{couponLoading ? (
<View className="coupon-popup__loading">
<Text>...</Text>
</View>
) : (
<>
{selectedCoupon && (
<View className="coupon-popup__current">
<Text className="coupon-popup__current-title font-medium">使</Text>
<View className="coupon-popup__current-item">
<Text>{selectedCoupon.title} -{calculateCouponDiscount(selectedCoupon, getGoodsTotal()).toFixed(2)}</Text>
<Button size="small" onClick={handleCouponCancel}>使</Button>
</View>
</View>
)}
{(() => {
const total = getGoodsTotal()
const usableCoupons = filterUsableCoupons(availableCoupons, total)
const unusableCoupons = filterUnusableCoupons(availableCoupons, total)
return (
<>
<CouponList
title={`可用优惠券 (${usableCoupons.length})`}
coupons={usableCoupons}
layout="vertical"
onCouponClick={handleCouponSelect}
showEmpty={usableCoupons.length === 0}
emptyText="暂无可用优惠券"
/>
{unusableCoupons.length > 0 && (
<CouponList
title={`不可用优惠券 (${unusableCoupons.length})`}
coupons={unusableCoupons.map(coupon => ({
...coupon,
status: 2 as const
}))}
layout="vertical"
showEmpty={false}
/>
)}
</>
)
})()}
</>
)}
</View>
</View>
</Popup>
{/* 门店选择弹窗 */}
<Popup
visible={storePopupVisible}
position="bottom"
style={{height: '70vh'}}
onClose={() => setStorePopupVisible(false)}
>
<View className="p-4">
<View className="flex justify-between items-center mb-3">
<Text className="text-base font-medium"></Text>
<Text
className="text-sm text-gray-500"
onClick={() => setStorePopupVisible(false)}
>
</Text>
</View>
{storeLoading ? (
<View className="py-10 text-center text-gray-500">
<Text>...</Text>
</View>
) : (
<CellGroup>
{stores.map((s) => {
const isActive = !!selectedStore?.id && selectedStore.id === s.id
return (
<Cell
key={s.id}
title={<Text className={isActive ? 'text-green-600' : ''}>{s.name || `门店${s.id}`}</Text>}
description={s.address || ''}
onClick={async () => {
let storeToSave: ShopStore = s
if (s?.id) {
try {
const full = await getShopStore(s.id)
if (full) storeToSave = full
} catch (_e) {
// keep base item
}
}
setSelectedStore(storeToSave)
saveSelectedStoreToStorage(storeToSave)
setStorePopupVisible(false)
Taro.showToast({title: '门店已切换', icon: 'success'})
}}
/>
)
})}
{!stores.length && (
<Cell title={<Text className="text-gray-500"></Text>} />
)}
</CellGroup>
)}
</View>
</Popup>
<Gap height={50}/>
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
<View className={'btn-bar flex justify-between items-center'}>
<div className={'flex flex-col justify-center items-start mx-4'}>
<View className={'flex items-center gap-2'}>
<span className={'total-price text-sm text-gray-500'}></span>
<span className={'text-red-500 text-xl font-bold'}>{getFinalPrice().toFixed(2)}</span>
</View>
{selectedCoupon && (
<View className={'text-xs text-gray-400'}>
{getCouponDiscount().toFixed(2)}
</View>
)}
</div>
<div className={'buy-btn mx-4'}>
<Button
type="success"
size="large"
loading={payLoading}
disabled={isTicketTemplateActive && quantity < minBuyQty}
onClick={() => onPay(goods)}
>
{payLoading ? '支付中...' : '立即付款'}
</Button>
</div>
</View>
</div>
</div>
);
};
export default OrderConfirm;