feat(order): 迁移和完善配送方式功能,支持全链路入库与展示

- 迁移配送方式选择功能从 orderConfirm 页至 user/ticket/use 页面
- orderConfirm 页面移除配送方式相关状态、UI与校验,取消配送费计算
- user/ticket/use 页面新增配送方式UI组件,支持配送费计算、楼层选择弹窗和提交校验
- 新增录入deliveryMethod、deliveryFloor、deliveryFee字段至订单模型与后端数据库
- 骑手端订单列表新增配送方式、楼层、配送费的详细展示
- 更新环境配置接口地址到正式API,修正测试及开发环境
- 用户页底部组件UI优化,新增版权icon并重构结构样式
- 使用配送方式字段校验下单逻辑,支持编辑模式配送信息回显与费用显示
- 移除orderConfirm中配送方式相关样式和组件,实现代码回滚清理
This commit is contained in:
2026-04-12 21:57:50 +08:00
parent e0418df018
commit 54404aa48f
11 changed files with 467 additions and 387 deletions

View File

@@ -89,6 +89,22 @@ const OrderConfirm = () => {
// Prevent using stale `inDeliveryRange` from a previous address when user switches addresses.
const [deliveryRangeCheckedAddressId, setDeliveryRangeCheckedAddressId] = useState<number | undefined>(undefined)
// 配送方式elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)
const [deliveryMethod, setDeliveryMethod] = useState<string>('')
// 步梯是否需要送上楼null=未选择)
const [needCarryUpstairs, setNeedCarryUpstairs] = useState<boolean | null>(null)
// 楼层从2开始需要送上楼时选择
const [deliveryFloor, setDeliveryFloor] = useState<number>(2)
// 楼层选择弹窗
const [floorPickerVisible, setFloorPickerVisible] = useState(false)
// 计算配送费每桶每层1元第1层不收费
const getDeliveryFee = () => {
if (deliveryMethod !== 'stairs' || !needCarryUpstairs) return 0
if (deliveryFloor <= 1) return 0
return displayQty * (deliveryFloor - 1)
}
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.goodsId;
const orderId = router?.params?.orderId;
@@ -594,6 +610,19 @@ const OrderConfirm = () => {
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
return
}
// 配送方式校验(必选)
if (!deliveryMethod) {
Taro.showToast({ title: '请选择配送方式', icon: 'none' })
return
}
// 步梯场景:必须选择是否送上楼
if (deliveryMethod === 'stairs' && needCarryUpstairs === null) {
Taro.showToast({ title: '请选择是否需要送上楼', icon: 'none' })
return
}
if (!addressHasCoords) {
Taro.showToast({ title: '该收货地址缺少经纬度,请在地址里选择地图定位后重试', icon: 'none' })
return
@@ -653,11 +682,14 @@ const OrderConfirm = () => {
const ok = await ensureInDeliveryRange()
if (!ok) return
const deliveryFee = getDeliveryFee()
const confirmContent = isEditMode
? `配送时间:${sendTimeText}\n送水数量${finalQty}\n配送方式${deliveryMethod === 'elevator' ? '电梯' : deliveryMethod === 'stairs' ? '步梯' : '一楼商铺/其他'}${deliveryMethod === 'stairs' && needCarryUpstairs && deliveryFloor > 1 ? `${deliveryFloor}楼)` : deliveryMethod === 'stairs' && !needCarryUpstairs ? '(不送上楼)' : ''}\n${deliveryFee > 0 ? `配送费:¥${deliveryFee.toFixed(2)}\n` : ''}是否确认修改?`
: `配送时间:${sendTimeText}\n配送方式${deliveryMethod === 'elevator' ? '电梯' : deliveryMethod === 'stairs' ? '步梯' : '一楼商铺/其他'}${deliveryMethod === 'stairs' && needCarryUpstairs && deliveryFloor > 1 ? `${deliveryFloor}楼)` : deliveryMethod === 'stairs' && !needCarryUpstairs ? '(不送上楼)' : ''}\n${deliveryFee > 0 ? `配送费:¥${deliveryFee.toFixed(2)}\n` : ''}将使用 ${finalQty} 张水票下单(优先使用可用数量少的水票),送水 ${finalQty} 桶,是否确认?`
const confirmRes = await Taro.showModal({
title: isEditMode ? '确认修改' : '确认下单',
content: isEditMode
? `配送时间:${sendTimeText}\n送水数量${finalQty}\n是否确认修改`
: `配送时间:${sendTimeText}\n将使用 ${finalQty} 张水票下单(优先使用可用数量少的水票),送水 ${finalQty} 桶,是否确认?`
content: confirmContent
})
if (!confirmRes.confirm) return
@@ -671,7 +703,10 @@ const OrderConfirm = () => {
addressId: address.id,
totalNum: finalQty,
buyerRemarks: orderRemark,
sendTime: dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss')
sendTime: dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
deliveryMethod,
deliveryFloor: deliveryMethod === 'stairs' && needCarryUpstairs ? deliveryFloor : undefined,
deliveryFee: getDeliveryFee() || undefined
})
} else {
// Best-effort auto dispatch rider. If it fails, backend/manual dispatch can still handle it.
@@ -697,7 +732,11 @@ const OrderConfirm = () => {
riderId: Number.isFinite(Number(autoRider?.userId)) ? Number(autoRider?.userId) : undefined,
riderName: autoRider?.realName,
riderPhone: autoRider?.mobile,
comments: goods?.name ? `立即送水:${goods.name}` : '立即送水'
comments: goods?.name ? `立即送水:${goods.name}` : '立即送水',
// 配送方式信息
deliveryMethod,
deliveryFloor: deliveryMethod === 'stairs' && needCarryUpstairs ? deliveryFloor : undefined,
deliveryFee: getDeliveryFee() || undefined
})
remain -= useQty
}
@@ -785,6 +824,16 @@ const OrderConfirm = () => {
const st = parseTime(editingOrderRes.sendTime)
if (st) setSendTime(clampSendDateToToday(st).toDate())
// 回显配送方式
if (editingOrderRes.deliveryMethod) {
setDeliveryMethod(editingOrderRes.deliveryMethod)
if (editingOrderRes.deliveryMethod === 'stairs') {
const hasFloor = editingOrderRes.deliveryFloor && editingOrderRes.deliveryFloor > 1
setNeedCarryUpstairs(hasFloor)
if (hasFloor) setDeliveryFloor(editingOrderRes.deliveryFloor)
}
}
const addrId = Number(editingOrderRes.addressId)
const addrIdSafe = Number.isFinite(addrId) && addrId > 0 ? addrId : undefined
if (addrIdSafe) {
@@ -1051,6 +1100,85 @@ const OrderConfirm = () => {
)}
</CellGroup>
{/* 配送方式选择(必选) */}
<CellGroup className={'delivery-method-group'}>
<Cell>
<View className={'delivery-method-section'}>
<View className={'delivery-method-label'}>
<Text className={'font-medium text-sm'}></Text>
<Text className={'text-red-500 text-xs ml-1'}>*</Text>
</View>
<View className={'delivery-method-options'}>
{[
{ key: 'elevator', label: '电梯', icon: '🏛️' },
{ key: 'stairs', label: '步梯', icon: '🚶' },
{ key: 'groundFloor', label: '一楼商铺/其他', icon: '🏪' },
].map(item => (
<View
key={item.key}
className={`delivery-method-item ${deliveryMethod === item.key ? 'active' : ''}`}
onClick={() => {
setDeliveryMethod(item.key)
setNeedCarryUpstairs(null)
setDeliveryFloor(2)
}}
>
<Text className={'delivery-method-icon'}>{item.icon}</Text>
<Text className={'text-sm'}>{item.label}</Text>
</View>
))}
</View>
{/* 步梯:是否需要送上楼 */}
{deliveryMethod === 'stairs' && (
<View className={'carry-upstairs-section'}>
<Text className={'text-sm text-gray-600 mb-2'}></Text>
<View className={'carry-upstairs-options'}>
<View
className={`carry-upstairs-item ${needCarryUpstairs === true ? 'active' : ''}`}
onClick={() => setNeedCarryUpstairs(true)}
>
<Text></Text>
</View>
<View
className={`carry-upstairs-item ${needCarryUpstairs === false ? 'active' : ''}`}
onClick={() => {
setNeedCarryUpstairs(false)
setDeliveryFloor(2)
}}
>
<Text></Text>
</View>
</View>
</View>
)}
{/* 步梯+送上楼:选择楼层 */}
{deliveryMethod === 'stairs' && needCarryUpstairs === true && (
<View className={'floor-select-section'}>
<Text className={'text-sm text-gray-600'}></Text>
<View
className={'floor-select-btn'}
onClick={() => setFloorPickerVisible(true)}
>
<Text className={deliveryFloor > 1 ? 'text-gray-900' : 'text-gray-400'}>
{deliveryFloor > 1 ? `${deliveryFloor}` : '请选择楼层'}
</Text>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
{deliveryFloor > 1 && (
<View className={'floor-fee-tip'}>
<Text className={'text-xs text-orange-500'}>
{displayQty} x {deliveryFloor - 1} = {getDeliveryFee().toFixed(2)}
</Text>
</View>
)}
</View>
)}
</View>
</Cell>
</CellGroup>
<CellGroup>
<Cell
title={'配送时间'}
@@ -1297,6 +1425,49 @@ const OrderConfirm = () => {
</View>
</Popup>
{/* 楼层选择弹窗 */}
<Popup
visible={floorPickerVisible}
position="bottom"
onClose={() => setFloorPickerVisible(false)}
style={{height: '40vh'}}
>
<View className="floor-picker-popup">
<View className="floor-picker-popup__header">
<Text className="text-base font-medium"></Text>
<Text
className="text-sm text-gray-500"
onClick={() => setFloorPickerVisible(false)}
>
</Text>
</View>
<View className="floor-picker-popup__content">
<View className="floor-grid">
{Array.from({length: 32}, (_, i) => i + 2).map(f => (
<View
key={f}
className={`floor-grid-item ${deliveryFloor === f ? 'active' : ''}`}
onClick={() => {
setDeliveryFloor(f)
setFloorPickerVisible(false)
}}
>
<Text>{f}</Text>
</View>
))}
</View>
</View>
{deliveryFloor > 1 && (
<View className="floor-picker-popup__footer">
<Text className={'text-sm text-gray-600'}>
{displayQty} x {deliveryFloor - 1} = <Text className={'text-red-500 font-bold'}>{(displayQty * (deliveryFloor - 1)).toFixed(2)}</Text>
</Text>
</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'}>
@@ -1309,6 +1480,11 @@ const OrderConfirm = () => {
</span>
<span className={'text-sm text-gray-500'}></span>
</View>
{getDeliveryFee() > 0 && (
<View className={'text-xs text-orange-500'}>
{getDeliveryFee().toFixed(2)}
</View>
)}
</div>
<div className={'buy-btn mx-4'}>
{noUsableTickets && !isEditMode ? (
@@ -1321,12 +1497,14 @@ const OrderConfirm = () => {
size="large"
loading={submitLoading || deliveryRangeChecking}
disabled={
deliveryRangeChecking ||
deliveryRangeChecking ||
!address?.id ||
!addressHasCoords ||
(deliveryRangeCheckedAddressId === address?.id && inDeliveryRange === false) ||
(!isEditMode && availableTicketTotal <= 0) ||
!canStartOrder
!canStartOrder ||
!deliveryMethod ||
(deliveryMethod === 'stairs' && needCarryUpstairs === null)
}
onClick={onSubmit}
>
@@ -1338,7 +1516,13 @@ const OrderConfirm = () => {
? '地址缺少定位'
: ((deliveryRangeCheckedAddressId === address?.id && inDeliveryRange === false)
? '不在配送范围'
: (submitLoading ? '提交中...' : (isEditMode ? '确定修改' : '立即提交'))
: (!deliveryMethod
? '请选配送方式'
: (deliveryMethod === 'stairs' && needCarryUpstairs === null
? '请选是否送上楼'
: (submitLoading ? '提交中...' : (isEditMode ? '确定修改' : '立即提交'))
)
)
)
)
)