You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
850 lines
28 KiB
850 lines
28 KiB
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 {
|
|
transformCouponData,
|
|
calculateCouponDiscount,
|
|
isCouponUsable,
|
|
getCouponUnusableReason,
|
|
sortCoupons,
|
|
filterUsableCoupons,
|
|
filterUnusableCoupons
|
|
} from "@/utils/couponUtils";
|
|
import navTo from "@/utils/common";
|
|
|
|
|
|
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)
|
|
|
|
// 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)
|
|
|
|
const router = Taro.getCurrentInstance().router;
|
|
const goodsId = router?.params?.goodsId;
|
|
|
|
// 计算商品总价
|
|
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 total
|
|
}
|
|
|
|
// 计算优惠券折扣
|
|
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 newQuantity = typeof value === 'string' ? parseInt(value) || 1 : value
|
|
const finalQuantity = Math.max(1, 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 () => {
|
|
try {
|
|
setCouponLoading(true)
|
|
|
|
// 使用新的API获取可用优惠券
|
|
const res = await getMyAvailableCoupons()
|
|
|
|
if (res && res.length > 0) {
|
|
// 转换数据格式
|
|
const transformedCoupons = res.map(transformCouponData)
|
|
|
|
// 按优惠金额排序
|
|
const total = 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 (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: goods.name,
|
|
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({})
|
|
])
|
|
|
|
// 设置商品信息
|
|
if (goodsRes) {
|
|
setGoods(goodsRes)
|
|
}
|
|
|
|
// 设置默认收货地址
|
|
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) {
|
|
await loadUserCoupons()
|
|
}
|
|
} catch (err) {
|
|
console.error('加载数据失败:', err)
|
|
setError('加载数据失败,请重试')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useDidShow(() => {
|
|
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 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={1}
|
|
max={goods.stock || 999}
|
|
disabled={goods.canBuyNumber != 0}
|
|
onChange={handleQuantityChange}
|
|
/>
|
|
</ConfigProvider>
|
|
{goods.stock !== undefined && (
|
|
<Text className={'text-xs text-gray-400'}>
|
|
库存 {goods.stock} 件
|
|
</Text>
|
|
)}
|
|
</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>
|
|
|
|
{/* 支付方式选择 */}
|
|
<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>
|
|
|
|
<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}
|
|
onClick={() => onPay(goods)}
|
|
>
|
|
{payLoading ? '支付中...' : '立即付款'}
|
|
</Button>
|
|
</div>
|
|
</View>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default OrderConfirm;
|