feat(order): 添加配送时间选择功能

- 在订单模型中新增 sendStartTime 字段用于预约配送时间
- 为水票套票商品添加配送时间选择器组件和日期选择逻辑
- 实现配送时间验证确保水票套票商品必须选择配送时间
- 优化支付错误处理增加配送范围提示和地址更换引导
- 调整套票购买注意事项显示动态最低起送量信息
- 移除用户票据页面重复的时间选择相关代码以保持一致性
This commit is contained in:
2026-02-09 16:48:34 +08:00
parent 37c2f030f2
commit a1e1487d42
4 changed files with 108 additions and 51 deletions

View File

@@ -1,4 +1,4 @@
import {useEffect, useState} from "react";
import {useEffect, useMemo, useState} from "react";
import {
Image,
Button,
@@ -9,6 +9,7 @@ import {
ActionSheet,
Popup,
InputNumber,
DatePicker,
ConfigProvider
} from '@nutui/nutui-react-taro'
import {Location, ArrowRight} from '@nutui/icons-react-taro'
@@ -38,6 +39,7 @@ import {
filterUsableCoupons,
filterUnusableCoupons
} from "@/utils/couponUtils";
import dayjs from 'dayjs'
import type {ShopStore} from "@/api/shop/shopStore/model";
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
@@ -54,6 +56,9 @@ const OrderConfirm = () => {
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string>('')
const [payLoading, setPayLoading] = useState<boolean>(false)
// 配送时间(仅水票套票商品需要)
const [sendTime, setSendTime] = useState<Date>(() => dayjs().startOf('day').toDate())
const [sendTimePickerVisible, setSendTimePickerVisible] = useState(false)
// 水票套票活动(若存在则按规则限制最小购买量等)
const [ticketTemplate, setTicketTemplate] = useState<GltTicketTemplate | null>(null)
@@ -88,6 +93,7 @@ const OrderConfirm = () => {
ticketTemplate.enabled !== false &&
ticketTemplate.status !== 1 &&
ticketTemplate.deleted !== 1
const hasTicketTemplate = !!ticketTemplate
// 套票活动最低购买量:优先取模板配置
const ticketMinBuyQty = (() => {
@@ -96,6 +102,10 @@ const OrderConfirm = () => {
})()
const minBuyQty = isTicketTemplateActive ? ticketMinBuyQty : 1
const sendTimeText = useMemo(() => {
return dayjs(sendTime).format('YYYY-MM-DD')
}, [sendTime])
const getGiftTicketQty = (buyQty: number) => {
if (!isTicketTemplateActive) return 0
const multiplier = Number(ticketTemplate?.giftMultiplier || 0)
@@ -420,6 +430,12 @@ const OrderConfirm = () => {
return;
}
// 水票套票商品:保存配送时间到 ShopOrder.sendStartTime
if (hasTicketTemplate && !sendTime) {
Taro.showToast({ title: '请选择配送时间', icon: 'none' })
return
}
// 水票套票活动:最小购买量校验
if (isTicketTemplateActive && quantity < minBuyQty) {
Taro.showToast({
@@ -482,6 +498,9 @@ const OrderConfirm = () => {
comments: goods.name,
deliveryType: 0,
buyerRemarks: orderRemark,
sendStartTime: hasTicketTemplate
? dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss')
: undefined,
couponId: parseInt(String(bestCoupon.id), 10)
}
);
@@ -507,6 +526,9 @@ const OrderConfirm = () => {
comments: '桂乐淘',
deliveryType: 0,
buyerRemarks: orderRemark,
sendStartTime: hasTicketTemplate
? dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss')
: undefined,
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined
}
@@ -549,31 +571,35 @@ const OrderConfirm = () => {
// icon: 'success'
// })
} catch (error: any) {
// return navTo('/user/order/order?statusFilter=0', true)
// console.error('支付失败:', error)
const message = String(error?.message || '')
const isOutOfDeliveryRange =
message.includes('不在配送范围') ||
message.includes('配送范围') ||
message.includes('电子围栏') ||
message.includes('围栏')
// 只处理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('跳去未付款的订单列表页面')
// }
// “配送范围”类错误给出更友好的解释,并提供快捷入口去更换收货地址
if (isOutOfDeliveryRange) {
try {
const res = await Taro.showModal({
title: '暂不支持配送',
content: '当前收货地址超出配送范围。您可以更换收货地址后再下单,或联系门店确认配送范围。',
confirmText: '更换地址',
cancelText: '我知道了'
})
if (res?.confirm) {
Taro.navigateTo({ url: '/user/address/index' })
}
} catch (_e) {
// ignore
}
return
}
// 兜底:仅在 PaymentHandler 未弹过提示时再提示一次
if (!error?.handled) {
Taro.showToast({ title: message || '支付失败,请重试', icon: 'none' })
}
} finally {
setPayLoading(false)
}
@@ -673,6 +699,9 @@ const OrderConfirm = () => {
})
useEffect(() => {
// 切换商品时重置配送时间,避免沿用上一次选择
setSendTime(dayjs().startOf('day').toDate())
setSendTimePickerVisible(false)
loadAllData()
}, [goodsId]);
@@ -731,6 +760,21 @@ const OrderConfirm = () => {
)}
</CellGroup>
{hasTicketTemplate && (
<CellGroup>
<Cell
title={'配送时间'}
extra={(
<View className={'flex items-center gap-2'}>
<View className={'text-gray-900'}>{sendTimeText}</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
)}
onClick={() => setSendTimePickerVisible(true)}
/>
</CellGroup>
)}
{/*<CellGroup>*/}
{/* <Cell*/}
{/* title={(*/}
@@ -874,10 +918,10 @@ const OrderConfirm = () => {
<CellGroup>
<Cell extra={(
<div className={'text-red-500 text-sm'}>
1.20
2.
3.
<Text></Text>
<Text>{ticketTemplate.minBuyQty}</Text>
<Text></Text>
<Text></Text>
</div>
)}/>
</CellGroup>
@@ -1023,6 +1067,23 @@ const OrderConfirm = () => {
<Gap height={50}/>
<DatePicker
visible={sendTimePickerVisible}
title="选择配送时间"
type="date"
startDate={dayjs().startOf('day').toDate()}
endDate={dayjs().add(30, 'day').toDate()}
value={sendTime}
onClose={() => setSendTimePickerVisible(false)}
onCancel={() => setSendTimePickerVisible(false)}
onConfirm={(_options, selectedValue) => {
const [y, m, d] = (selectedValue || []).map(v => Number(v))
const next = new Date(y, (m || 1) - 1, d || 1, 0, 0, 0)
setSendTime(next)
setSendTimePickerVisible(false)
}}
/>
<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'}>