feat(ticket): 实现水票下单功能,支持用水票抵扣送水订单
- 移除原有优惠券和支付方式选择逻辑 - 添加水票相关的API调用和数据管理 - 实现水票消费计划算法,按FIFO原则使用水票 - 添加水票明细弹窗展示用户持有的水票 - 实现下单时自动扣除对应数量水票的功能 - 添加水票核销记录日志功能 - 修改数量选择器以限制在水票可用范围内 - 实现水票下单的完整业务流程验证
This commit is contained in:
@@ -1,58 +1,44 @@
|
|||||||
import {useEffect, useState} from "react";
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
import {
|
import {
|
||||||
Image,
|
|
||||||
Button,
|
Button,
|
||||||
Cell,
|
Cell,
|
||||||
CellGroup,
|
CellGroup,
|
||||||
|
ConfigProvider,
|
||||||
Input,
|
Input,
|
||||||
Space,
|
|
||||||
ActionSheet,
|
|
||||||
Popup,
|
|
||||||
InputNumber,
|
InputNumber,
|
||||||
ConfigProvider
|
Popup,
|
||||||
|
Space
|
||||||
} from '@nutui/nutui-react-taro'
|
} from '@nutui/nutui-react-taro'
|
||||||
import {Location, ArrowRight, Shop} from '@nutui/icons-react-taro'
|
import { ArrowRight, Location, Shop, Ticket } from '@nutui/icons-react-taro'
|
||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
import { getShopGoods } from '@/api/shop/shopGoods'
|
||||||
import {getShopGoods} from "@/api/shop/shopGoods";
|
import { listShopUserAddress } from '@/api/shop/shopUserAddress'
|
||||||
import {View, Text} from '@tarojs/components';
|
import type { ShopUserAddress } from '@/api/shop/shopUserAddress/model'
|
||||||
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
|
|
||||||
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
|
|
||||||
import './use.scss'
|
import './use.scss'
|
||||||
import Gap from "@/components/Gap";
|
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 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";
|
|
||||||
import type {ShopStore} from "@/api/shop/shopStore/model";
|
import type {ShopStore} from "@/api/shop/shopStore/model";
|
||||||
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||||
|
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
|
||||||
|
import { listGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket'
|
||||||
|
import { addGltUserTicketLog } from '@/api/glt/gltUserTicketLog'
|
||||||
|
import { createOrder, listShopOrder } from '@/api/shop/shopOrder'
|
||||||
|
import type { OrderCreateRequest } from '@/api/shop/shopOrder/model'
|
||||||
|
|
||||||
|
// payType=12 in this project is "free order" (no payment). Used for water-ticket orders.
|
||||||
|
const PAY_TYPE_FREE = 12
|
||||||
|
|
||||||
const OrderConfirm = () => {
|
const OrderConfirm = () => {
|
||||||
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
||||||
const [address, setAddress] = useState<ShopUserAddress>()
|
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 [quantity, setQuantity] = useState<number>(1)
|
||||||
const [orderRemark, setOrderRemark] = useState<string>('')
|
const [orderRemark, setOrderRemark] = useState<string>('')
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<string>('')
|
||||||
const [payLoading, setPayLoading] = useState<boolean>(false)
|
const [submitLoading, setSubmitLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
// InputNumber 主题配置
|
// InputNumber 主题配置
|
||||||
const customTheme = {
|
const customTheme = {
|
||||||
@@ -64,20 +50,87 @@ const OrderConfirm = () => {
|
|||||||
nutuiInputnumberButtonBorderRadius: '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)
|
// 门店选择:用于在下单页展示当前“已选门店”,并允许用户切换(写入 SelectedStore Storage)
|
||||||
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
||||||
const [stores, setStores] = useState<ShopStore[]>([])
|
const [stores, setStores] = useState<ShopStore[]>([])
|
||||||
const [storeLoading, setStoreLoading] = useState(false)
|
const [storeLoading, setStoreLoading] = useState(false)
|
||||||
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
||||||
|
|
||||||
|
// 水票:用于“立即送水”下单(用水票抵扣,无需支付)
|
||||||
|
const [tickets, setTickets] = useState<GltUserTicket[]>([])
|
||||||
|
const [ticketPopupVisible, setTicketPopupVisible] = useState(false)
|
||||||
|
const [ticketLoading, setTicketLoading] = useState(false)
|
||||||
|
|
||||||
const router = Taro.getCurrentInstance().router;
|
const router = Taro.getCurrentInstance().router;
|
||||||
const goodsId = router?.params?.goodsId;
|
const goodsId = router?.params?.goodsId;
|
||||||
|
const numericGoodsId = useMemo(() => {
|
||||||
|
const n = goodsId ? Number(goodsId) : undefined
|
||||||
|
return typeof n === 'number' && Number.isFinite(n) ? n : undefined
|
||||||
|
}, [goodsId])
|
||||||
|
|
||||||
|
const userId = useMemo(() => {
|
||||||
|
const raw = Taro.getStorageSync('UserId')
|
||||||
|
const id = Number(raw)
|
||||||
|
return Number.isFinite(id) && id > 0 ? id : undefined
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const usableTickets = useMemo(() => {
|
||||||
|
const list = (tickets || [])
|
||||||
|
.filter(t => t?.deleted !== 1)
|
||||||
|
.filter(t => t?.status !== 1)
|
||||||
|
.filter(t => Number.isFinite(Number(t?.id)) && Number(t.id) > 0)
|
||||||
|
.filter(t => (t.availableQty ?? 0) > 0)
|
||||||
|
.filter(t => (numericGoodsId ? t.goodsId === numericGoodsId : true))
|
||||||
|
// FIFO: use older tickets first (reduce disputes).
|
||||||
|
return list.sort((a, b) => {
|
||||||
|
const ta = new Date(a.createTime || 0).getTime() || 0
|
||||||
|
const tb = new Date(b.createTime || 0).getTime() || 0
|
||||||
|
if (ta !== tb) return ta - tb
|
||||||
|
return (a.id || 0) - (b.id || 0)
|
||||||
|
})
|
||||||
|
}, [tickets, numericGoodsId])
|
||||||
|
|
||||||
|
const availableTicketTotal = useMemo(() => {
|
||||||
|
return usableTickets.reduce((sum, t) => sum + Number(t.availableQty || 0), 0)
|
||||||
|
}, [usableTickets])
|
||||||
|
|
||||||
|
const maxQuantity = useMemo(() => {
|
||||||
|
const stockMax = goods?.stock ?? 999
|
||||||
|
return Math.max(0, Math.min(stockMax, availableTicketTotal))
|
||||||
|
}, [availableTicketTotal, goods?.stock])
|
||||||
|
|
||||||
|
const displayQty = useMemo(() => {
|
||||||
|
if (maxQuantity <= 0) return 0
|
||||||
|
return Math.max(1, Math.min(quantity, maxQuantity))
|
||||||
|
}, [quantity, maxQuantity])
|
||||||
|
|
||||||
|
type ConsumePlanItem = {
|
||||||
|
ticket: GltUserTicket
|
||||||
|
qty: number
|
||||||
|
availableAfter: number
|
||||||
|
usedAfter: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildConsumePlan = (needQty: number): ConsumePlanItem[] => {
|
||||||
|
let remaining = Math.max(0, needQty)
|
||||||
|
const plan: ConsumePlanItem[] = []
|
||||||
|
for (const t of usableTickets) {
|
||||||
|
if (!remaining) break
|
||||||
|
const available = Number(t.availableQty || 0)
|
||||||
|
const used = Number(t.usedQty || 0)
|
||||||
|
if (available <= 0) continue
|
||||||
|
const take = Math.min(available, remaining)
|
||||||
|
remaining -= take
|
||||||
|
plan.push({
|
||||||
|
ticket: t,
|
||||||
|
qty: take,
|
||||||
|
availableAfter: available - take,
|
||||||
|
usedAfter: used + take
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (remaining > 0) return []
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
const loadStores = async () => {
|
const loadStores = async () => {
|
||||||
if (storeLoading) return
|
if (storeLoading) return
|
||||||
@@ -101,463 +154,188 @@ const OrderConfirm = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算商品总价
|
|
||||||
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 handleQuantityChange = (value: string | number) => {
|
||||||
const newQuantity = typeof value === 'string' ? parseInt(value) || 1 : value
|
const parsed = typeof value === 'string' ? parseInt(value) : value
|
||||||
const finalQuantity = Math.max(1, Math.min(newQuantity, goods?.stock || 999))
|
const newQuantity = Number.isFinite(parsed) ? Number(parsed) : 0
|
||||||
setQuantity(finalQuantity)
|
const upper = maxQuantity
|
||||||
|
if (upper <= 0) {
|
||||||
|
setQuantity(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setQuantity(Math.max(1, Math.min(newQuantity || 1, upper)))
|
||||||
|
}
|
||||||
|
|
||||||
// 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用
|
const loadUserTickets = async () => {
|
||||||
if (availableCoupons.length > 0) {
|
if (ticketLoading) return
|
||||||
const newTotal = parseFloat(goods?.price || '0') * finalQuantity
|
if (!userId) {
|
||||||
const sortedCoupons = sortCoupons(availableCoupons, newTotal)
|
setTickets([])
|
||||||
const usableCoupons = filterUsableCoupons(sortedCoupons, newTotal)
|
return
|
||||||
setAvailableCoupons(sortedCoupons)
|
}
|
||||||
|
try {
|
||||||
// 检查当前选中的优惠券是否还可用
|
setTicketLoading(true)
|
||||||
if (selectedCoupon && !isCouponUsable(selectedCoupon, newTotal)) {
|
const list = await listGltUserTicket({ userId, status: 0 })
|
||||||
setSelectedCoupon(null)
|
setTickets(list || [])
|
||||||
Taro.showToast({
|
} catch (e) {
|
||||||
title: '当前优惠券不满足使用条件,已自动取消',
|
console.error('获取水票失败:', e)
|
||||||
icon: 'none'
|
setTickets([])
|
||||||
})
|
Taro.showToast({ title: '获取水票失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
// 🎯 自动推荐新的最优优惠券
|
setTicketLoading(false)
|
||||||
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 findOrderIdByOrderNo = async (orderNo: string): Promise<number | undefined> => {
|
||||||
const handleCouponSelect = (coupon: CouponCardProps) => {
|
try {
|
||||||
const total = getGoodsTotal()
|
const list = await listShopOrder({ orderNo, userId } as any)
|
||||||
|
const first = (list || []).find(o => o?.orderNo === orderNo)
|
||||||
|
return first?.orderId
|
||||||
|
} catch (_e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔍 详细日志记录,用于排查问题
|
const consumeTicketsForOrder = async (
|
||||||
console.log('🎫 手动选择优惠券详细信息:', {
|
needQty: number,
|
||||||
coupon: {
|
orderNo: string,
|
||||||
id: coupon.id,
|
orderId?: number
|
||||||
title: coupon.title,
|
) => {
|
||||||
type: coupon.type,
|
const plan = buildConsumePlan(needQty)
|
||||||
amount: coupon.amount,
|
if (!plan.length) throw new Error('水票可用次数不足')
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 检查是否可用
|
// NOTE: This is a client-side best-effort implementation.
|
||||||
if (!isCouponUsable(coupon, total)) {
|
// For strict consistency (order + ticket deduction + log in one transaction),
|
||||||
const reason = getCouponUnusableReason(coupon, total)
|
// please implement a backend API to do these steps atomically.
|
||||||
|
for (const item of plan) {
|
||||||
|
const t = item.ticket
|
||||||
|
const availableBefore = Number(t.availableQty || 0)
|
||||||
|
|
||||||
// 🚨 记录手动选择失败的详细信息
|
await updateGltUserTicket({
|
||||||
console.error('🚨 手动选择优惠券失败:', {
|
...t,
|
||||||
reason,
|
availableQty: item.availableAfter,
|
||||||
coupon,
|
usedQty: item.usedAfter
|
||||||
total,
|
|
||||||
minAmount: coupon.minAmount,
|
|
||||||
comparison: {
|
|
||||||
totalVsMinAmount: `${total} < ${coupon.minAmount}`,
|
|
||||||
result: total < (coupon.minAmount || 0)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Taro.showToast({
|
// Write-off log (核销记录)
|
||||||
title: reason || '优惠券不可用',
|
await addGltUserTicketLog({
|
||||||
icon: 'none'
|
userTicketId: t.id,
|
||||||
|
changeType: 2, // 约定:2=消费/核销(若后端有枚举,请按后端约定调整)
|
||||||
|
changeAvailable: -item.qty,
|
||||||
|
changeUsed: item.qty,
|
||||||
|
availableAfter: item.availableAfter,
|
||||||
|
usedAfter: item.usedAfter,
|
||||||
|
orderId,
|
||||||
|
orderNo,
|
||||||
|
userId: userId || t.userId,
|
||||||
|
comments: `水票下单核销:${item.qty} 张(${availableBefore}→${item.availableAfter})`
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if (submitLoading) return
|
||||||
|
if (!goods?.goodsId) return
|
||||||
|
|
||||||
|
// 基础校验
|
||||||
|
if (!userId) {
|
||||||
|
Taro.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedStore?.id) {
|
||||||
|
Taro.showToast({ title: '请选择门店', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!address?.id) {
|
||||||
|
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (availableTicketTotal <= 0) {
|
||||||
|
Taro.showToast({ title: '暂无可用水票', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedCoupon(coupon)
|
const finalQty = displayQty
|
||||||
setCouponVisible(false)
|
if (finalQty <= 0) {
|
||||||
Taro.showToast({
|
Taro.showToast({ title: '请选择送水数量', icon: 'none' })
|
||||||
title: '优惠券选择成功',
|
return
|
||||||
icon: 'success'
|
}
|
||||||
})
|
if (finalQty > availableTicketTotal) {
|
||||||
}
|
Taro.showToast({ title: '水票可用次数不足', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (goods.stock !== undefined && finalQty > goods.stock) {
|
||||||
|
Taro.showToast({ title: '商品库存不足', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 取消选择优惠券
|
const confirmRes = await Taro.showModal({
|
||||||
const handleCouponCancel = () => {
|
title: '确认下单',
|
||||||
setSelectedCoupon(null)
|
content: `将使用 ${finalQty} 张水票下单,送水 ${finalQty} 桶,是否确认?`
|
||||||
Taro.showToast({
|
|
||||||
title: '已取消使用优惠券',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
})
|
||||||
}
|
if (!confirmRes.confirm) return
|
||||||
|
|
||||||
// 加载用户优惠券
|
|
||||||
const loadUserCoupons = async () => {
|
|
||||||
try {
|
try {
|
||||||
setCouponLoading(true)
|
setSubmitLoading(true)
|
||||||
|
Taro.showLoading({ title: '提交中...' })
|
||||||
|
|
||||||
// 使用新的API获取可用优惠券
|
const orderData: OrderCreateRequest = {
|
||||||
const res = await getMyAvailableCoupons()
|
goodsItems: [{ goodsId: goods.goodsId, quantity: finalQty }],
|
||||||
|
addressId: address.id,
|
||||||
if (res && res.length > 0) {
|
storeId: selectedStore.id,
|
||||||
// 转换数据格式
|
storeName: selectedStore.name,
|
||||||
const transformedCoupons = res.map(transformCouponData)
|
payType: PAY_TYPE_FREE,
|
||||||
|
deliveryType: 0,
|
||||||
// 按优惠金额排序
|
comments: orderRemark || '水票下单'
|
||||||
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)
|
const res = await createOrder(orderData)
|
||||||
setAvailableCoupons([])
|
const orderNo = res?.orderNo
|
||||||
Taro.showToast({
|
if (!orderNo) throw new Error('下单失败,请稍后重试')
|
||||||
title: '加载优惠券失败',
|
|
||||||
icon: 'none'
|
const orderId = await findOrderIdByOrderNo(orderNo)
|
||||||
})
|
try {
|
||||||
|
await consumeTicketsForOrder(finalQty, orderNo, orderId)
|
||||||
|
} catch (consumeErr: any) {
|
||||||
|
console.error('订单已创建,但水票核销失败:', { orderNo, consumeErr })
|
||||||
|
await Taro.showModal({
|
||||||
|
title: '下单已成功',
|
||||||
|
content: `订单已创建(${orderNo}),但水票扣除/核销记录写入失败,请联系管理员处理。`,
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
// 避免用户重复下单:直接跳转到订单列表查看处理结果
|
||||||
|
Taro.redirectTo({ url: '/user/order/order' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadUserTickets()
|
||||||
|
|
||||||
|
Taro.showToast({ title: '下单成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
// 跳转到“我的送水订单”(当前项目使用“我的订单”页承载)
|
||||||
|
Taro.redirectTo({ url: '/user/order/order' })
|
||||||
|
}, 800)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('水票下单失败:', e)
|
||||||
|
Taro.showToast({ title: e?.message || '下单失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
setCouponLoading(false)
|
Taro.hideLoading()
|
||||||
|
setSubmitLoading(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: '桂乐淘',
|
|
||||||
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 () => {
|
const loadAllData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
// 分别加载数据,避免类型推断问题
|
|
||||||
let goodsRes: ShopGoods | null = null
|
let goodsRes: ShopGoods | null = null
|
||||||
if (goodsId) {
|
if (numericGoodsId) {
|
||||||
goodsRes = await getShopGoods(Number(goodsId))
|
goodsRes = await getShopGoods(numericGoodsId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [addressRes, paymentRes] = await Promise.all([
|
const [addressRes] = await Promise.all([
|
||||||
listShopUserAddress({isDefault: true}),
|
listShopUserAddress({ isDefault: true })
|
||||||
selectPayment({})
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// 设置商品信息
|
// 设置商品信息
|
||||||
@@ -569,20 +347,13 @@ const OrderConfirm = () => {
|
|||||||
if (addressRes && addressRes.length > 0) {
|
if (addressRes && addressRes.length > 0) {
|
||||||
setAddress(addressRes[0])
|
setAddress(addressRes[0])
|
||||||
}
|
}
|
||||||
|
await loadUserTickets()
|
||||||
|
|
||||||
// 设置支付方式
|
// Clamp quantity after loading tickets/stock.
|
||||||
if (paymentRes && paymentRes.length > 0) {
|
setQuantity(prev => {
|
||||||
setPayments(paymentRes.map((d) => ({
|
const upper = maxQuantity > 0 ? maxQuantity : 1
|
||||||
type: d.type,
|
return Math.max(1, Math.min(prev, upper))
|
||||||
name: d.name
|
})
|
||||||
})))
|
|
||||||
setPayment(paymentRes[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载优惠券(在商品信息加载完成后)
|
|
||||||
if (goodsRes) {
|
|
||||||
await loadUserCoupons()
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载数据失败:', err)
|
console.error('加载数据失败:', err)
|
||||||
setError('加载数据失败,请重试')
|
setError('加载数据失败,请重试')
|
||||||
@@ -601,6 +372,15 @@ const OrderConfirm = () => {
|
|||||||
loadAllData()
|
loadAllData()
|
||||||
}, [goodsId]);
|
}, [goodsId]);
|
||||||
|
|
||||||
|
// When tickets/stock change, clamp quantity into [0..maxQuantity].
|
||||||
|
useEffect(() => {
|
||||||
|
setQuantity(prev => {
|
||||||
|
if (maxQuantity <= 0) return 0
|
||||||
|
if (!prev || prev < 1) return 1
|
||||||
|
return Math.min(prev, maxQuantity)
|
||||||
|
})
|
||||||
|
}, [maxQuantity])
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
loadAllData()
|
loadAllData()
|
||||||
@@ -681,10 +461,10 @@ const OrderConfirm = () => {
|
|||||||
extra={(
|
extra={(
|
||||||
<ConfigProvider theme={customTheme}>
|
<ConfigProvider theme={customTheme}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={quantity}
|
value={maxQuantity <= 0 ? 0 : quantity}
|
||||||
min={1}
|
min={maxQuantity <= 0 ? 0 : 1}
|
||||||
max={goods.stock || 999}
|
max={maxQuantity <= 0 ? 0 : maxQuantity}
|
||||||
disabled={goods.canBuyNumber != 0}
|
disabled={maxQuantity <= 0}
|
||||||
onChange={handleQuantityChange}
|
onChange={handleQuantityChange}
|
||||||
/>
|
/>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
@@ -692,31 +472,26 @@ const OrderConfirm = () => {
|
|||||||
/>
|
/>
|
||||||
</CellGroup>
|
</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>
|
<CellGroup>
|
||||||
<Cell
|
<Cell
|
||||||
title={`您还有 ${quantity} 张水票`}
|
title={(
|
||||||
extra={<View className={'font-medium'}>送 {quantity} 桶</View>}
|
<View className="flex items-center gap-2">
|
||||||
|
<Ticket className={'text-gray-500'}/>
|
||||||
|
<Text>可用水票</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
extra={(
|
||||||
|
<View className={'flex items-center gap-2'}>
|
||||||
|
<View className={'text-gray-900'}>{availableTicketTotal} 张</View>
|
||||||
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
onClick={() => setTicketPopupVisible(true)}
|
||||||
|
/>
|
||||||
|
<Cell
|
||||||
|
title={'本次使用'}
|
||||||
|
extra={<View className={'font-medium'}>{displayQty} 张</View>}
|
||||||
/>
|
/>
|
||||||
{/*<Cell title={'配送费'} extra={'¥0.00'}/>*/}
|
|
||||||
<Cell extra={(
|
|
||||||
<View className={'flex items-end gap-2'}>
|
|
||||||
<Text className={'ml-2'}>需使用</Text>
|
|
||||||
<Text className={'text-gray-700 font-bold'} style={{fontSize: '18px'}}>1</Text>张
|
|
||||||
</View>
|
|
||||||
)}/>
|
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
|
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
@@ -731,6 +506,46 @@ const OrderConfirm = () => {
|
|||||||
)}/>
|
)}/>
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
|
|
||||||
|
{/* 水票明细弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={ticketPopupVisible}
|
||||||
|
position="bottom"
|
||||||
|
style={{ height: '70vh' }}
|
||||||
|
onClose={() => setTicketPopupVisible(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={() => setTicketPopupVisible(false)}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{ticketLoading ? (
|
||||||
|
<View className="py-10 text-center text-gray-500">
|
||||||
|
<Text>加载中...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<CellGroup>
|
||||||
|
{usableTickets.map((t) => (
|
||||||
|
<Cell
|
||||||
|
key={t.id}
|
||||||
|
title={<Text>{t.templateName || '水票'}</Text>}
|
||||||
|
description={t.orderNo ? `来源订单:${t.orderNo}` : ''}
|
||||||
|
extra={<Text className="text-gray-700">可用 {t.availableQty ?? 0}</Text>}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!usableTickets.length && (
|
||||||
|
<Cell title={<Text className="text-gray-500">暂无可用水票</Text>} />
|
||||||
|
)}
|
||||||
|
</CellGroup>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
{/* 门店选择弹窗 */}
|
{/* 门店选择弹窗 */}
|
||||||
<Popup
|
<Popup
|
||||||
visible={storePopupVisible}
|
visible={storePopupVisible}
|
||||||
@@ -795,17 +610,21 @@ const OrderConfirm = () => {
|
|||||||
<div className={'flex flex-col justify-center items-start mx-4'}>
|
<div className={'flex flex-col justify-center items-start mx-4'}>
|
||||||
<View className={'flex items-center gap-2'}>
|
<View className={'flex items-center gap-2'}>
|
||||||
<span className={'total-price text-sm text-gray-500'}>使用水票:</span>
|
<span className={'total-price text-sm text-gray-500'}>使用水票:</span>
|
||||||
<span className={'text-red-500 text-xl font-bold'}>{getFinalPrice().toFixed(2)}</span>
|
<span className={'text-red-500 text-xl font-bold'}>
|
||||||
|
{displayQty}
|
||||||
|
</span>
|
||||||
|
<span className={'text-sm text-gray-500'}>张</span>
|
||||||
</View>
|
</View>
|
||||||
</div>
|
</div>
|
||||||
<div className={'buy-btn mx-4'}>
|
<div className={'buy-btn mx-4'}>
|
||||||
<Button
|
<Button
|
||||||
type="success"
|
type="success"
|
||||||
size="large"
|
size="large"
|
||||||
loading={payLoading}
|
loading={submitLoading}
|
||||||
onClick={() => onPay(goods)}
|
disabled={availableTicketTotal <= 0 || maxQuantity <= 0}
|
||||||
|
onClick={onSubmit}
|
||||||
>
|
>
|
||||||
{payLoading ? '支付中...' : '立即付款'}
|
{submitLoading ? '提交中...' : '立即提交'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user