@@ -1,4 +1,4 @@
import { useEffect , useState } from "react" ;
import { useEffect , useMemo , useState } from "react" ;
import {
Image ,
Button ,
@@ -82,17 +82,62 @@ const OrderConfirm = () => {
const [ selectedStore , setSelectedStore ] = useState < ShopStore | null > ( getSelectedStoreFromStorage ( ) )
const router = Taro . getCurrentInstance ( ) . router ;
const goodsId = router ? . params ? . goodsId ;
const params = router ? . params || ( { } as Record < string , any > )
const goodsIdParam = params ? . goodsId
const orderDataRaw = params ? . orderData
type OrderDataParam = {
goodsId? : number | string
skuId? : number | string
quantity? : number | string
price? : number | string
specInfo? : string
}
const orderDataParam : OrderDataParam | null = useMemo ( ( ) = > {
if ( ! orderDataRaw ) return null
const rawText = String ( orderDataRaw )
try {
return JSON . parse ( decodeURIComponent ( rawText ) ) as OrderDataParam
} catch ( _e1 ) {
try {
return JSON . parse ( rawText ) as OrderDataParam
} catch ( _e2 ) {
console . error ( 'orderData 参数解析失败:' , orderDataRaw )
return null
}
}
} , [ orderDataRaw ] )
const resolvedGoodsId = ( ( ) = > {
const id1 = Number ( goodsIdParam )
if ( Number . isFinite ( id1 ) && id1 > 0 ) return id1
const id2 = Number ( orderDataParam ? . goodsId )
if ( Number . isFinite ( id2 ) && id2 > 0 ) return id2
return undefined
} ) ( )
const resolvedSkuId = ( ( ) = > {
const n = Number ( orderDataParam ? . skuId )
return Number . isFinite ( n ) && n > 0 ? n : undefined
} ) ( )
const quantityFromParam = ( ( ) = > {
const n = Number ( orderDataParam ? . quantity )
return Number . isFinite ( n ) && n > 0 ? Math . floor ( n ) : undefined
} ) ( )
// 页面级兜底:未登录直接进入下单页时,引导去注册/登录并回跳
useEffect ( ( ) = > {
if ( ! goodsId ) {
// 也可能是 orderData 模式;这里只做最小兜底
if ( ! ensureLoggedIn ( '/shop/orderConfirm/index' ) ) return
return
}
if ( ! ensureLoggedIn ( ` /shop/orderConfirm/index?goodsId= ${ g oodsId} ` ) ) return
} , [ goodsId ] )
// 兼容 goodsId / orderData 两种进入方式( goodsDetail 有规格时会走 orderData)
const backUrl =
orderDataRaw
? ` /shop/orderConfirm/index?orderData= ${ orderDataRaw } `
: resolvedGoodsId
? ` /shop/orderConfirm/index?goodsId= ${ resolvedG oodsId} `
: '/shop/orderConfirm/index'
if ( ! ensureLoggedIn ( backUrl ) ) return
} , [ resolvedGoodsId , orderDataRaw ] )
const isTicketTemplateActive =
! ! ticketTemplate &&
@@ -142,7 +187,9 @@ const OrderConfirm = () => {
// 计算商品总价
const getGoodsTotal = ( ) = > {
if ( ! goods ) return 0
const p rice = parseFloat ( goods . price || '0' )
const rawP rice = String ( orderDataParam ? . price ? ? goods . price ? ? '0' )
const priceNum = parseFloat ( rawPrice )
const price = Number . isFinite ( priceNum ) ? priceNum : 0
// const total = price * quantity
// 🔍 详细日志,用于排查数值精度问题
@@ -183,12 +230,21 @@ const OrderConfirm = () => {
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 ) )
const step = goods ? . step || 1
const stockMax = goods ? . stock ? ? 999
const maxMultiple = step > 1 ? Math . floor ( stockMax / step ) * step : stockMax
const maxAllowed = maxMultiple > 0 ? maxMultiple : stockMax
const effectiveMin = Math . min ( fallback , maxAllowed )
const clamped = Math . max ( effectiveMin , Math . min ( Number ( newQuantity ) || fallback , maxAllowed ) )
const snapped = step > 1 ? Math . ceil ( clamped / step ) * step : clamped
const finalQuantity = Math . max ( effectiveMin , Math . min ( snapped , maxAllowed ) )
setQuantity ( finalQuantity )
// 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用
if ( availableCoupons . length > 0 ) {
const newTotal = parseFloat ( goods ? . price || '0' ) * finalQuantity
const priceNum = parseFloat ( String ( orderDataParam ? . price ? ? goods ? . price ? ? '0' ) )
const unitPrice = Number . isFinite ( priceNum ) ? priceNum : 0
const newTotal = unitPrice * finalQuantity
const sortedCoupons = sortCoupons ( availableCoupons , newTotal )
const usableCoupons = filterUsableCoupons ( sortedCoupons , newTotal )
setAvailableCoupons ( sortedCoupons )
@@ -497,7 +553,9 @@ const OrderConfirm = () => {
comments : goods.name ,
deliveryType : 0 ,
buyerRemarks : orderRemark ,
couponId : parseInt ( String ( bestCoupon . id ) , 10 )
couponId : parseInt ( String ( bestCoupon . id ) , 10 ) ,
skuId : resolvedSkuId ,
specInfo : orderDataParam?.specInfo
}
) ;
@@ -548,7 +606,9 @@ const OrderConfirm = () => {
deliveryType : 0 ,
buyerRemarks : orderRemark ,
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
couponId : selectedCoupon ? parseInt ( String ( selectedCoupon . id ) , 10 ) : undefined
couponId : selectedCoupon ? parseInt ( String ( selectedCoupon . id ) , 10 ) : undefined ,
skuId : resolvedSkuId ,
specInfo : orderDataParam?.specInfo
}
) ;
@@ -684,8 +744,8 @@ const OrderConfirm = () => {
// 分别加载数据,避免类型推断问题
let goodsRes : ShopGoods | null = null
if ( g oodsId) {
goodsRes = await getShopGoods ( Number ( g oodsId) )
if ( resolvedG oodsId) {
goodsRes = await getShopGoods ( resolvedG oodsId)
}
const [ addressRes , paymentRes ] = await Promise . all ( [
@@ -696,9 +756,9 @@ const OrderConfirm = () => {
// 设置商品信息
// 查询当前商品是否存在水票套票活动(失败/无数据时不影响正常下单)
let tpl : GltTicketTemplate | null = null
if ( g oodsId) {
if ( resolvedG oodsId) {
try {
tpl = await getGltTicketTemplateByGoodsId ( Number ( g oodsId) )
tpl = await getGltTicketTemplateByGoodsId ( resolvedG oodsId)
} catch ( e ) {
tpl = null
}
@@ -714,18 +774,41 @@ const OrderConfirm = () => {
const n = Number ( tpl ? . minBuyQty )
return Number . isFinite ( n ) && n > 0 ? Math . floor ( n ) : 1
} ) ( )
const tplStep = ( ( ) = > {
const n = Number ( tpl ? . step )
return Number . isFinite ( n ) && n > 0 ? Math . floor ( n ) : undefined
} ) ( )
// 设置商品信息(若存在套票模板,则默认 canBuyNumber 使用模板最小购买量)
if ( goodsRes ) {
const patchedGoods : ShopGoods = { . . . goodsRes }
// 兜底:确保 step 为合法正整数;若存在套票模板则优先使用模板 step
const goodsStepNum = Number ( ( patchedGoods as any ) ? . step )
const goodsStep = Number . isFinite ( goodsStepNum ) && goodsStepNum > 0 ? Math . floor ( goodsStepNum ) : 1
patchedGoods . step = tplActive && tplStep ? tplStep : goodsStep
// 规格商品( orderData 模式)下单时,用 sku 价格覆盖展示与计算金额
if ( orderDataParam ? . price !== undefined && orderDataParam ? . price !== null && orderDataParam ? . price !== '' ) {
patchedGoods . price = String ( orderDataParam . price )
}
if ( tplActive && ( ( patchedGoods . canBuyNumber ? ? 0 ) === 0 ) ) {
patchedGoods . canBuyNumber = tplMinBuyQty
}
setGoods ( patchedGoods )
// 设置默认购买数量:优先使用 canBuyNumber, 否则使用 1
const init Qty = ( patchedGoods . canBuyNumber ? ? 0 ) > 0 ? ( patchedGoods . canBuyNumber as number ) : 1
setQuantity ( initQty )
// 设置默认购买数量:优先使用 canBuyNumber, 其次使用路由参数 quantity, 否则使用 1
const fixed Qty = ( patchedGoods . canBuyNumber ? ? 0 ) > 0 ? Number ( patchedGoods . canBuyNumber ) : undefined
const rawQty = fixedQty ? ? quantityFromParam ? ? 1
const minQty = tplActive ? tplMinBuyQty : 1
const step = patchedGoods . step || 1
const stockMax = patchedGoods . stock ? ? 999
const maxMultiple = step > 1 ? Math . floor ( stockMax / step ) * step : stockMax
const maxAllowed = maxMultiple > 0 ? maxMultiple : stockMax
const effectiveMin = Math . min ( minQty , maxAllowed )
const clamped = Math . max ( effectiveMin , Math . min ( Math . floor ( rawQty ) , maxAllowed ) )
const stepped = step > 1 ? Math . ceil ( clamped / step ) * step : clamped
setQuantity ( Math . min ( maxAllowed , Math . max ( effectiveMin , stepped ) ) )
}
setTicketTemplate ( tpl )
@@ -750,9 +833,20 @@ const OrderConfirm = () => {
const n = Number ( goodsRes ? . canBuyNumber )
if ( Number . isFinite ( n ) && n > 0 ) return Math . floor ( n )
if ( tplActive ) return tplMinBuyQty
return 1
return quantityFromParam || 1
} ) ( )
const total = parseFloat ( goodsRes . price || '0' ) * initQty
const stepForInit = tplActive && tplStep ? tplStep : ( ( ) = > {
const n = Number ( ( goodsRes as any ) ? . step )
return Number . isFinite ( n ) && n > 0 ? Math . floor ( n ) : 1
} ) ( )
const stockMax = goodsRes . stock ? ? 999
const maxMultiple = stepForInit > 1 ? Math . floor ( stockMax / stepForInit ) * stepForInit : stockMax
const maxAllowed = maxMultiple > 0 ? maxMultiple : stockMax
const initQtySnapped = stepForInit > 1 ? Math . ceil ( initQty / stepForInit ) * stepForInit : initQty
const effectiveMin = Math . min ( tplActive ? tplMinBuyQty : 1 , maxAllowed )
const safeInitQty = Math . max ( effectiveMin , Math . min ( initQtySnapped , maxAllowed ) )
const unitPrice = parseFloat ( String ( orderDataParam ? . price ? ? goodsRes . price ? ? '0' ) )
const total = unitPrice * safeInitQty
await loadUserCoupons ( total )
}
} catch ( err ) {
@@ -773,7 +867,7 @@ const OrderConfirm = () => {
useEffect ( ( ) = > {
if ( ! isLoggedIn ( ) ) return
loadAllData ( )
} , [ goodsId ] ) ;
} , [ resolvedGoodsId , orderDataRaw ] ) ;
// 重新加载数据
const handleRetry = ( ) = > {
@@ -863,14 +957,14 @@ const OrderConfirm = () => {
< 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 >
< Text className = { 'text-red-500' } > ¥ { goods . price } * { goods . step } < / Text >
< View className = { 'flex flex-col items-end gap-1' } >
< ConfigProvider theme = { customTheme } >
< InputNumber
value = { quantity }
min = { isTicketTemplateActive ? minBuyQty : 1 }
max = { goods . stock || 999 }
step = { minBuyQty === 1 ? 1 : 10 }
step = { goods . step || 1 }
readOnly
disabled = { ( ( goods . canBuyNumber ? ? 0 ) !== 0 ) && ! isTicketTemplateActive }
onChange = { handleQuantityChange }