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

@@ -63,10 +63,10 @@
"profession": "高级开发工程师", "profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png", "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md", "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775756293122, "usedAt": 1775972794982,
"industryId": "all" "industryId": "all"
} }
] ]
}, },
"lastUpdated": 1775757430292 "lastUpdated": 1775999935033
} }

View File

@@ -69,6 +69,23 @@
- 订单创建接口接收并存储这两个字段 - 订单创建接口接收并存储这两个字段
- 骑手端/后台展示配送方式和楼层信息 - 骑手端/后台展示配送方式和楼层信息
## 配送方式功能迁移orderConfirm → user/ticket/use
### 原因
配送方式选择功能从购买下单页orderConfirm迁移到水票核销/立即送水页面user/ticket/use
### 修改的文件
1. `src/shop/orderConfirm/index.tsx` - 回滚移除配送方式状态变量、UI、校验、楼层选择弹窗、配送费计算
2. `src/shop/orderConfirm/index.scss` - 回滚:移除配送方式/楼层选择相关样式
3. `src/api/glt/gltTicketOrder/model/index.ts` - GltTicketOrder 新增 deliveryMethod、deliveryFloor、deliveryFee 字段
4. `src/user/ticket/use.tsx` - 新增配送方式选择UI、配送费计算、楼层选择弹窗、提交校验、编辑模式回显
5. `src/user/ticket/use.scss` - 新增:配送方式/楼层选择样式
### 关键差异
- orderConfirm 页面是付费购买,配送费加到实付金额中
- use 页面是水票核销(不付费),配送费以"到付"形式展示在底部栏
- use 页面同时支持新建和编辑模式,编辑时回显配送信息
## 微信订阅消息配置(补充) ## 微信订阅消息配置(补充)
### 需要做的配置 ### 需要做的配置

View File

@@ -0,0 +1,25 @@
# 2026-04-12 工作日志
## 配送方式功能完善:全链路入库+展示
### 问题
配送方式deliveryMethod、楼层deliveryFloor、配送费deliveryFee在小程序端用户下单页面已可选择并提交但后端数据库表没有对应字段数据实际没有保存。骑手端和后台管理端也没有展示这些信息。
### 修改的文件
#### 后端(/Users/gxwebsoft/JAVA/java-10584
1. `src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java` - 新增 deliveryMethod(String)、deliveryFloor(Integer)、deliveryFee(BigDecimal) 三个数据库字段
2. `sql/glt_ticket_order_delivery_fields.sql` - 新增 ALTER TABLE SQL给 glt_ticket_order 表添加三个字段
#### 小程序端(/Users/gxwebsoft/VUE/template-10584
3. `src/rider/orders/index.tsx` - 骑手送水订单页面:新增配送方式中文映射函数,订单卡片中展示配送方式、楼层、配送费
#### 后台管理端(/Users/gxwebsoft/VUE/mp-10584
4. `src/api/glt/gltTicketOrder/model/index.ts` - GltTicketOrder 接口新增 deliveryMethod、deliveryFloor、deliveryFee 字段
5. `src/views/glt/gltTicketOrder/index.vue` - 列表页"配送信息"列中展示配送方式、楼层、配送费
6. `src/views/glt/gltTicketOrder/components/gltTicketOrderEdit.vue` - 编辑弹窗中新增配送方式只读展示
### 部署注意事项
- **必须先执行 SQL**`java-10584/sql/glt_ticket_order_delivery_fields.sql`
- MyBatis-Plus 使用驼峰自动映射,`delivery_method``deliveryMethod`,无需修改 Mapper XML
- 后端 save/updateById 自动包含新字段,无需修改 Controller/Service

View File

@@ -8,22 +8,22 @@ const CURRENT_ENV = 'production'
export const ENV_CONFIG = { export const ENV_CONFIG = {
// 开发环境 // 开发环境
development: { development: {
API_BASE_URL: 'https://glt-dev-api.websoft.top/api', API_BASE_URL: 'https://glt-api.websoft.top/api',
SERVER_API_URL: 'https://glt-dev-server.websoft.top/api', SERVER_API_URL: 'https://glt-server.websoft.top/api',
APP_NAME: '开发环境', APP_NAME: '开发环境',
DEBUG: 'true', DEBUG: 'true',
}, },
// 测试环境 // 测试环境
test: { test: {
API_BASE_URL: 'https://glt-dev-api.websoft.top/api', API_BASE_URL: 'https://glt-api.websoft.top/api',
SERVER_API_URL: 'https://glt-dev-server.websoft.top/api', SERVER_API_URL: 'https://glt-server.websoft.top/api',
APP_NAME: '测试环境', APP_NAME: '测试环境',
DEBUG: 'true', DEBUG: 'true',
}, },
// 生产环境 // 生产环境
production: { production: {
API_BASE_URL: 'https://glt-dev-api.websoft.top/api', API_BASE_URL: 'https://glt-api.websoft.top/api',
SERVER_API_URL: 'https://glt-dev-server.websoft.top/api', SERVER_API_URL: 'https://glt-server.websoft.top/api',
APP_NAME: '桂乐淘', APP_NAME: '桂乐淘',
DEBUG: 'false', DEBUG: 'false',
}, },

View File

@@ -76,6 +76,12 @@ export interface GltTicketOrder {
createTime?: string; createTime?: string;
// 修改时间 // 修改时间
updateTime?: string; updateTime?: string;
// 配送方式elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)
deliveryMethod?: string;
// 楼层(步梯+送上楼时有值从2开始
deliveryFloor?: number;
// 配送费(步梯+送上楼时计算:数量 × (楼层-1)
deliveryFee?: number;
} }
/** /**

View File

@@ -1,9 +1,10 @@
import {loginBySms} from "@/api/passport/login"; import {loginBySms} from "@/api/passport/login";
import {useState} from "react"; import {useState} from "react";
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import {View,Text} from '@tarojs/components'
import {Popup} from '@nutui/nutui-react-taro' import {Popup} from '@nutui/nutui-react-taro'
import {UserParam} from "@/api/system/user/model"; import {UserParam} from "@/api/system/user/model";
import {Button} from '@nutui/nutui-react-taro' import {Button, Image} from '@nutui/nutui-react-taro'
import {Form, Input} from '@nutui/nutui-react-taro' import {Form, Input} from '@nutui/nutui-react-taro'
import {Copyright} from "@/config/app"; import {Copyright} from "@/config/app";
const UserFooter = () => { const UserFooter = () => {
@@ -46,11 +47,14 @@ const UserFooter = () => {
return ( return (
<> <>
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}> <View className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
{/*<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>*/} {/*<View className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</View>*/}
{/*<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>*/} {/*<View className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</View>*/}
<div className={'text-xs text-gray-400 py-1'}>{Copyright}</div> <View className={'text-xs text-gray-400 py-1 flex justify-center items-center gap-2'}>
</div> <Image src={'https://oss.wsdns.cn/20260412/7d03ec2a05964c3e926c4eac12ee5835.png'} mode={'aspectFit'} width={20} height={20} />
<Text>{Copyright}</Text>
</View>
</View>
<Popup <Popup
style={{width: '350px', padding: '10px'}} style={{width: '350px', padding: '10px'}}
@@ -66,7 +70,7 @@ const UserFooter = () => {
labelPosition="left" labelPosition="left"
onFinish={(values) => submitByPhone(values)} onFinish={(values) => submitByPhone(values)}
footer={ footer={
<div <View
style={{ style={{
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
@@ -76,7 +80,7 @@ const UserFooter = () => {
<Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}> <Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}>
</Button> </Button>
</div> </View>
} }
> >
<Form.Item <Form.Item

View File

@@ -73,6 +73,14 @@ export default function RiderOrders() {
return '待派单' return '待派单'
} }
// 配送方式中文映射
const getDeliveryMethodText = (method?: string) => {
if (method === 'elevator') return '电梯'
if (method === 'stairs') return '步梯'
if (method === 'groundFloor') return '一楼商铺/其他'
return ''
}
const getOrderStatusColor = (order: GltTicketOrder) => { const getOrderStatusColor = (order: GltTicketOrder) => {
const text = getOrderStatusText(order) const text = getOrderStatusText(order)
if (text === '已完成') return 'text-green-600' if (text === '已完成') return 'text-green-600'
@@ -383,6 +391,10 @@ export default function RiderOrders() {
const pickupName = o.warehouseName || o.storeName const pickupName = o.warehouseName || o.storeName
const pickupAddr = o.warehouseAddress || o.storeAddress const pickupAddr = o.warehouseAddress || o.storeAddress
// 配送方式信息
const deliveryMethodText = getDeliveryMethodText(o.deliveryMethod)
const hasDeliveryInfo = !!deliveryMethodText
return ( return (
<Cell key={String(o.id)} style={{ padding: '16px' }}> <Cell key={String(o.id)} style={{ padding: '16px' }}>
<View className="w-full"> <View className="w-full">
@@ -418,6 +430,24 @@ export default function RiderOrders() {
<Text>{o.price || '-'}</Text> <Text>{o.price || '-'}</Text>
</View> </View>
{hasDeliveryInfo && (
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text className={o.deliveryMethod === 'stairs' ? 'text-orange-500' : ''}>
{deliveryMethodText}
</Text>
{o.deliveryMethod === 'stairs' && o.deliveryFloor && o.deliveryFloor > 1 && (
<Text className="ml-1 text-orange-500">{o.deliveryFloor}</Text>
)}
{o.deliveryMethod === 'stairs' && !o.deliveryFloor && (
<Text className="ml-1 text-gray-400"></Text>
)}
{!!o.deliveryFee && o.deliveryFee > 0 && (
<Text className="ml-3 text-red-500"> {o.deliveryFee}</Text>
)}
</View>
)}
<View className="text-sm text-gray-700 mt-1"> <View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text> <Text className="text-gray-500"></Text>
<Text>{o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}</Text> <Text>{o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}</Text>

View File

@@ -40,188 +40,6 @@
} }
} }
// 配送方式选择
.delivery-method-group {
.delivery-method-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.delivery-method-label {
display: flex;
align-items: center;
}
.delivery-method-options {
display: flex;
gap: 12px;
}
.delivery-method-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 16px 8px;
border-radius: 12px;
border: 2px solid #f0f0f0;
background: #fafafa;
transition: all 0.2s ease;
&.active {
border-color: #07c160;
background: rgba(7, 193, 96, 0.05);
}
&:active {
transform: scale(0.97);
}
}
.delivery-method-icon {
font-size: 24px;
}
.delivery-method-text {
font-size: 13px;
color: #333;
font-weight: 500;
}
.delivery-method-item.active .delivery-method-text {
color: #07c160;
}
}
// 是否送上楼
.carry-upstairs-section {
display: flex;
flex-direction: column;
padding: 12px 0 0;
border-top: 1px dashed #eee;
margin-top: 4px;
}
.carry-upstairs-options {
display: flex;
gap: 12px;
}
.carry-upstairs-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
border-radius: 10px;
border: 2px solid #f0f0f0;
background: #fafafa;
font-size: 18px;
color: #666;
transition: all 0.2s ease;
&.active {
border-color: #07c160;
background: rgba(7, 193, 96, 0.05);
color: #07c160;
font-weight: 500;
}
&:active {
transform: scale(0.97);
}
}
// 楼层选择
.floor-select-section {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0 0;
border-top: 1px dashed #eee;
margin-top: 4px;
}
.floor-select-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 14px;
border-radius: 8px;
background: #f5f5f5;
font-size: 18px;
transition: background 0.2s;
&:active {
background: #e8e8e8;
}
}
.floor-fee-tip {
margin-left: auto;
}
// 楼层选择弹窗
.floor-picker-popup {
height: 100%;
display: flex;
flex-direction: column;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
&__content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
&__footer {
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
text-align: center;
background: #fafafa;
}
}
.floor-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.floor-grid-item {
display: flex;
align-items: center;
justify-content: center;
padding: 14px 0;
border-radius: 10px;
border: 2px solid #f0f0f0;
background: #fff;
font-size: 14px;
color: #333;
transition: all 0.2s ease;
&.active {
border-color: #07c160;
background: rgba(7, 193, 96, 0.08);
color: #07c160;
font-weight: 600;
}
&:active {
transform: scale(0.95);
background: #f5f5f5;
}
}
.address-bottom-line{ .address-bottom-line{
width: 100%; width: 100%;
border-radius: 12rpx 12rpx 0 0; border-radius: 12rpx 12rpx 0 0;

View File

@@ -81,15 +81,6 @@ const OrderConfirm = () => {
const [storeLoading, setStoreLoading] = useState(false) const [storeLoading, setStoreLoading] = useState(false)
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage()) const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
// 配送方式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)
const router = Taro.getCurrentInstance().router; const router = Taro.getCurrentInstance().router;
const params = router?.params || ({} as Record<string, any>) const params = router?.params || ({} as Record<string, any>)
const goodsIdParam = params?.goodsId const goodsIdParam = params?.goodsId
@@ -222,20 +213,11 @@ const OrderConfirm = () => {
return calculateCouponDiscount(selectedCoupon, total) return calculateCouponDiscount(selectedCoupon, total)
} }
// 计算配送费每桶每层1元第1层不收费
const getDeliveryFee = () => {
// 仅步梯 + 需要送上楼 时才有配送费
if (deliveryMethod !== 'stairs' || !needCarryUpstairs) return 0
if (deliveryFloor <= 1) return 0
return quantity * (deliveryFloor - 1)
}
// 计算实付金额 // 计算实付金额
const getFinalPrice = () => { const getFinalPrice = () => {
const total = getGoodsTotal() const total = getGoodsTotal()
const discount = getCouponDiscount() const discount = getCouponDiscount()
const deliveryFee = getDeliveryFee() return Math.max(0, total - discount)
return Math.max(0, total - discount + deliveryFee)
} }
@@ -499,24 +481,6 @@ const OrderConfirm = () => {
return; return;
} }
// 配送方式校验(仅送货上门模式需要选择)
if (goods.deliveryMode !== 1 && !deliveryMethod) {
Taro.showToast({
title: '请选择配送方式',
icon: 'error'
})
return;
}
// 步梯场景:必须选择是否送上楼
if (deliveryMethod === 'stairs' && needCarryUpstairs === null) {
Taro.showToast({
title: '请选择是否需要送上楼',
icon: 'error'
})
return;
}
if (!payment) { if (!payment) {
Taro.showToast({ Taro.showToast({
title: '请选择支付方式', title: '请选择支付方式',
@@ -591,9 +555,7 @@ const OrderConfirm = () => {
buyerRemarks: orderRemark, buyerRemarks: orderRemark,
couponId: parseInt(String(bestCoupon.id), 10), couponId: parseInt(String(bestCoupon.id), 10),
skuId: resolvedSkuId, skuId: resolvedSkuId,
specInfo: orderDataParam?.specInfo, specInfo: orderDataParam?.specInfo
deliveryMethod: deliveryMethod || undefined,
deliveryFloor: needCarryUpstairs ? deliveryFloor : undefined
} }
); );
@@ -646,9 +608,7 @@ const OrderConfirm = () => {
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined // 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined, couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined,
skuId: resolvedSkuId, skuId: resolvedSkuId,
specInfo: orderDataParam?.specInfo, specInfo: orderDataParam?.specInfo
deliveryMethod: deliveryMethod || undefined,
deliveryFloor: needCarryUpstairs ? deliveryFloor : undefined
} }
); );
@@ -1000,88 +960,6 @@ const OrderConfirm = () => {
{/* />*/} {/* />*/}
{/*</CellGroup>*/} {/*</CellGroup>*/}
{/* 配送方式选择(仅送货上门模式显示) */}
{goods.deliveryMode !== 1 && (
<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'}>
{quantity} × {deliveryFloor - 1} = {getDeliveryFee().toFixed(2)}
</Text>
</View>
)}
</View>
)}
</View>
</Cell>
</CellGroup>
)}
<CellGroup> <CellGroup>
<Cell key={goods.goodsId}> <Cell key={goods.goodsId}>
<View className={'flex w-full justify-between gap-3'}> <View className={'flex w-full justify-between gap-3'}>
@@ -1180,14 +1058,7 @@ const OrderConfirm = () => {
)} )}
onClick={() => setCouponVisible(true)} onClick={() => setCouponVisible(true)}
/> />
<Cell title={'配送费'} extra={ <Cell title={'配送费'} extra={'¥0.00'}/>
<View className={'flex items-center gap-1'}>
<Text>{getDeliveryFee().toFixed(2)}</Text>
{deliveryMethod === 'stairs' && needCarryUpstairs && deliveryFloor > 1 && (
<Text className={'text-xs text-gray-400'}>({quantity}×{deliveryFloor - 1})</Text>
)}
</View>
}/>
<Cell extra={( <Cell extra={(
<View className={'flex items-end gap-2'}> <View className={'flex items-end gap-2'}>
<Text></Text> <Text></Text>
@@ -1361,49 +1232,6 @@ const OrderConfirm = () => {
</View> </View>
</Popup> </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'}>
{quantity} × {deliveryFloor - 1} = <Text className={'text-red-500 font-bold'}>{(quantity * (deliveryFloor - 1)).toFixed(2)}</Text>
</Text>
</View>
)}
</View>
</Popup>
<Gap height={50}/> <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'}> <div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
@@ -1418,11 +1246,6 @@ const OrderConfirm = () => {
{getCouponDiscount().toFixed(2)} {getCouponDiscount().toFixed(2)}
</View> </View>
)} )}
{getDeliveryFee() > 0 && (
<View className={'text-xs text-orange-500'}>
{getDeliveryFee().toFixed(2)}
</View>
)}
</div> </div>
<div className={'buy-btn mx-4'}> <div className={'buy-btn mx-4'}>
<Button <Button

View File

@@ -58,6 +58,179 @@
} }
} }
// 配送方式选择
.delivery-method-group {
.delivery-method-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.delivery-method-label {
display: flex;
align-items: center;
}
.delivery-method-options {
display: flex;
gap: 12px;
}
.delivery-method-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 16px 8px;
border-radius: 12px;
border: 2px solid #f0f0f0;
background: #fafafa;
transition: all 0.2s ease;
&.active {
border-color: #07c160;
background: rgba(7, 193, 96, 0.05);
}
&:active {
transform: scale(0.97);
}
}
.delivery-method-icon {
font-size: 24px;
}
}
// 是否送上楼
.carry-upstairs-section {
display: flex;
flex-direction: column;
padding: 12px 0 0;
border-top: 1px dashed #eee;
margin-top: 4px;
}
.carry-upstairs-options {
display: flex;
gap: 12px;
}
.carry-upstairs-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
border-radius: 10px;
border: 2px solid #f0f0f0;
background: #fafafa;
font-size: 18px;
color: #666;
transition: all 0.2s ease;
&.active {
border-color: #07c160;
background: rgba(7, 193, 96, 0.05);
color: #07c160;
font-weight: 500;
}
&:active {
transform: scale(0.97);
}
}
// 楼层选择
.floor-select-section {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0 0;
border-top: 1px dashed #eee;
margin-top: 4px;
}
.floor-select-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 14px;
border-radius: 8px;
background: #f5f5f5;
font-size: 18px;
transition: background 0.2s;
&:active {
background: #e8e8e8;
}
}
.floor-fee-tip {
margin-left: auto;
}
// 楼层选择弹窗
.floor-picker-popup {
height: 100%;
display: flex;
flex-direction: column;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
&__content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
&__footer {
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
text-align: center;
background: #fafafa;
}
}
.floor-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.floor-grid-item {
display: flex;
align-items: center;
justify-content: center;
padding: 14px 0;
border-radius: 10px;
border: 2px solid #f0f0f0;
background: #fff;
font-size: 14px;
color: #333;
transition: all 0.2s ease;
&.active {
border-color: #07c160;
background: rgba(7, 193, 96, 0.08);
color: #07c160;
font-weight: 600;
}
&:active {
transform: scale(0.95);
background: #f5f5f5;
}
}
// 优惠券弹窗样式 // 优惠券弹窗样式
.coupon-popup { .coupon-popup {
height: 100%; height: 100%;

View File

@@ -89,6 +89,22 @@ const OrderConfirm = () => {
// Prevent using stale `inDeliveryRange` from a previous address when user switches addresses. // Prevent using stale `inDeliveryRange` from a previous address when user switches addresses.
const [deliveryRangeCheckedAddressId, setDeliveryRangeCheckedAddressId] = useState<number | undefined>(undefined) 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 router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.goodsId; const goodsId = router?.params?.goodsId;
const orderId = router?.params?.orderId; const orderId = router?.params?.orderId;
@@ -594,6 +610,19 @@ const OrderConfirm = () => {
Taro.showToast({ title: '请选择收货地址', icon: 'none' }) Taro.showToast({ title: '请选择收货地址', icon: 'none' })
return return
} }
// 配送方式校验(必选)
if (!deliveryMethod) {
Taro.showToast({ title: '请选择配送方式', icon: 'none' })
return
}
// 步梯场景:必须选择是否送上楼
if (deliveryMethod === 'stairs' && needCarryUpstairs === null) {
Taro.showToast({ title: '请选择是否需要送上楼', icon: 'none' })
return
}
if (!addressHasCoords) { if (!addressHasCoords) {
Taro.showToast({ title: '该收货地址缺少经纬度,请在地址里选择地图定位后重试', icon: 'none' }) Taro.showToast({ title: '该收货地址缺少经纬度,请在地址里选择地图定位后重试', icon: 'none' })
return return
@@ -653,11 +682,14 @@ const OrderConfirm = () => {
const ok = await ensureInDeliveryRange() const ok = await ensureInDeliveryRange()
if (!ok) return 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({ const confirmRes = await Taro.showModal({
title: isEditMode ? '确认修改' : '确认下单', title: isEditMode ? '确认修改' : '确认下单',
content: isEditMode content: confirmContent
? `配送时间:${sendTimeText}\n送水数量${finalQty}\n是否确认修改`
: `配送时间:${sendTimeText}\n将使用 ${finalQty} 张水票下单(优先使用可用数量少的水票),送水 ${finalQty} 桶,是否确认?`
}) })
if (!confirmRes.confirm) return if (!confirmRes.confirm) return
@@ -671,7 +703,10 @@ const OrderConfirm = () => {
addressId: address.id, addressId: address.id,
totalNum: finalQty, totalNum: finalQty,
buyerRemarks: orderRemark, 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 { } else {
// Best-effort auto dispatch rider. If it fails, backend/manual dispatch can still handle it. // 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, riderId: Number.isFinite(Number(autoRider?.userId)) ? Number(autoRider?.userId) : undefined,
riderName: autoRider?.realName, riderName: autoRider?.realName,
riderPhone: autoRider?.mobile, 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 remain -= useQty
} }
@@ -785,6 +824,16 @@ const OrderConfirm = () => {
const st = parseTime(editingOrderRes.sendTime) const st = parseTime(editingOrderRes.sendTime)
if (st) setSendTime(clampSendDateToToday(st).toDate()) 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 addrId = Number(editingOrderRes.addressId)
const addrIdSafe = Number.isFinite(addrId) && addrId > 0 ? addrId : undefined const addrIdSafe = Number.isFinite(addrId) && addrId > 0 ? addrId : undefined
if (addrIdSafe) { if (addrIdSafe) {
@@ -1051,6 +1100,85 @@ const OrderConfirm = () => {
)} )}
</CellGroup> </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> <CellGroup>
<Cell <Cell
title={'配送时间'} title={'配送时间'}
@@ -1297,6 +1425,49 @@ const OrderConfirm = () => {
</View> </View>
</Popup> </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}/> <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'}> <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>
<span className={'text-sm text-gray-500'}></span> <span className={'text-sm text-gray-500'}></span>
</View> </View>
{getDeliveryFee() > 0 && (
<View className={'text-xs text-orange-500'}>
{getDeliveryFee().toFixed(2)}
</View>
)}
</div> </div>
<div className={'buy-btn mx-4'}> <div className={'buy-btn mx-4'}>
{noUsableTickets && !isEditMode ? ( {noUsableTickets && !isEditMode ? (
@@ -1321,12 +1497,14 @@ const OrderConfirm = () => {
size="large" size="large"
loading={submitLoading || deliveryRangeChecking} loading={submitLoading || deliveryRangeChecking}
disabled={ disabled={
deliveryRangeChecking || deliveryRangeChecking ||
!address?.id || !address?.id ||
!addressHasCoords || !addressHasCoords ||
(deliveryRangeCheckedAddressId === address?.id && inDeliveryRange === false) || (deliveryRangeCheckedAddressId === address?.id && inDeliveryRange === false) ||
(!isEditMode && availableTicketTotal <= 0) || (!isEditMode && availableTicketTotal <= 0) ||
!canStartOrder !canStartOrder ||
!deliveryMethod ||
(deliveryMethod === 'stairs' && needCarryUpstairs === null)
} }
onClick={onSubmit} onClick={onSubmit}
> >
@@ -1338,7 +1516,13 @@ const OrderConfirm = () => {
? '地址缺少定位' ? '地址缺少定位'
: ((deliveryRangeCheckedAddressId === address?.id && inDeliveryRange === false) : ((deliveryRangeCheckedAddressId === address?.id && inDeliveryRange === false)
? '不在配送范围' ? '不在配送范围'
: (submitLoading ? '提交中...' : (isEditMode ? '确定修改' : '立即提交')) : (!deliveryMethod
? '请选配送方式'
: (deliveryMethod === 'stairs' && needCarryUpstairs === null
? '请选是否送上楼'
: (submitLoading ? '提交中...' : (isEditMode ? '确定修改' : '立即提交'))
)
)
) )
) )
) )