feat(ticket): 完善水票配送订单功能

- 优化导入路径,修复 PageParam 类型引用
- 新增 DeliverConfirmMode 类型定义,支持拍照完成和等待客户确认两种模式
- 实现配送确认的双模式功能,支持直接完成和等待确认流程
- 重构订单状态判断逻辑,完善配送流程状态管理
- 新增用户端确认收货功能,支持手动确认收货操作
- 优化订单列表展示,增加票号、取货点、门店电话等详细信息
- 添加地址复制和联系门店功能按钮
- 实现补传照片完成订单功能
- 更新订单流程状态显示,提供更准确的状态标识
- 添加配送确认模式切换的单选框界面
- 优化下单成功后的页面跳转逻辑
- 新增水票配送订单后端接口设计文档
This commit is contained in:
2026-02-06 20:33:56 +08:00
parent 661e7574ef
commit 25177d724e
5 changed files with 268 additions and 28 deletions

View File

@@ -8,6 +8,8 @@ import {
Space,
Button,
Dialog,
Radio,
RadioGroup,
Image,
Empty,
InfiniteLoading,
@@ -22,6 +24,8 @@ import { uploadFile } from '@/api/system/file'
const PAGE_SIZE = 10
type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm'
export default function TicketOrdersPage() {
const riderId = useMemo(() => {
const raw = Taro.getStorageSync('UserId')
@@ -42,6 +46,7 @@ export default function TicketOrdersPage() {
const [deliverSubmitting, setDeliverSubmitting] = useState(false)
const [deliverOrder, setDeliverOrder] = useState<GltTicketOrder | null>(null)
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
const [deliverConfirmMode, setDeliverConfirmMode] = useState<DeliverConfirmMode>('photoComplete')
const riderTabs = useMemo(
() => [
@@ -95,7 +100,21 @@ export default function TicketOrdersPage() {
if (!riderId || order.riderId !== riderId) return false
if (order.receiveConfirmTime) return false
if (order.deliveryStatus === 40) return false
return !order.sendEndTime
if (order.sendEndTime) return false
// 只允许在“配送中”阶段确认送达
if (typeof order.deliveryStatus === 'number') return order.deliveryStatus === 20
return !!order.sendStartTime
}
const canCompleteByPhoto = (order: GltTicketOrder) => {
if (!order.id) return false
if (order.status === 1) return false
if (!riderId || order.riderId !== riderId) return false
if (order.receiveConfirmTime) return false
if (order.deliveryStatus === 40) return false
// 已送达但未完成:允许补传照片并直接完成
return !!order.sendEndTime
}
const filterByTab = useCallback(
@@ -173,9 +192,10 @@ export default function TicketOrdersPage() {
await reload(false)
}, [hasMore, loading, reload])
const openDeliverDialog = (order: GltTicketOrder) => {
const openDeliverDialog = (order: GltTicketOrder, opts?: { mode?: DeliverConfirmMode }) => {
setDeliverOrder(order)
setDeliverImg(order.sendEndImg)
setDeliverConfirmMode(opts?.mode || (order.sendEndImg ? 'photoComplete' : 'waitCustomerConfirm'))
setDeliverDialogVisible(true)
}
@@ -210,22 +230,41 @@ export default function TicketOrdersPage() {
const handleConfirmDelivered = async () => {
if (!deliverOrder?.id) return
if (deliverSubmitting) return
if (deliverConfirmMode === 'photoComplete' && !deliverImg) {
Taro.showToast({ title: '请先拍照/上传送达照片', icon: 'none' })
return
}
setDeliverSubmitting(true)
try {
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
// 送达时间:首次“确认送达”写入;补传照片时不要覆盖原送达时间
const deliveredAt = deliverOrder.sendEndTime || now
// 说明:
// - sendEndImg送达照片留档可选/必填由后端策略决定
// - sendEndTime配送员确认送达时间
// - deliveryStatus建议后端设置为 30待客户确认
await updateGltTicketOrder({
id: deliverOrder.id,
deliveryStatus: 30,
sendEndTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
sendEndImg: deliverImg
})
// - waitCustomerConfirm只标记“已送达”进入待客户确认客户点击确认收货后完成
// - photoComplete拍照留档后可直接完成由后端策略决定是否允许
const payload: GltTicketOrder =
deliverConfirmMode === 'photoComplete'
? {
id: deliverOrder.id,
deliveryStatus: 40,
sendEndTime: deliveredAt,
sendEndImg: deliverImg,
receiveConfirmTime: now,
receiveConfirmType: 20
}
: {
id: deliverOrder.id,
deliveryStatus: 30,
sendEndTime: deliveredAt,
sendEndImg: deliverImg
}
await updateGltTicketOrder(payload)
Taro.showToast({ title: '已确认送达', icon: 'success' })
setDeliverDialogVisible(false)
setDeliverOrder(null)
setDeliverImg(undefined)
setDeliverConfirmMode('photoComplete')
pageRef.current = 1
await reload(true)
} catch (e) {
@@ -320,13 +359,19 @@ export default function TicketOrdersPage() {
const timeText = o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'
const addr = o.address || (o.addressId ? `地址ID${o.addressId}` : '-')
const remark = o.buyerRemarks || o.comments || ''
const ticketNo = o.userTicketId || '-'
const flow1Done = !!o.riderId
const flow2Done = !!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20)
const flow3Done = !!o.sendEndTime
const flow2Done =
!!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20)
const flow3Done =
!!o.sendEndTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 30)
const flow4Done = !!o.receiveConfirmTime || o.deliveryStatus === 40
const phoneToCall = o.phone
const storePhone = o.storePhone
const pickupName = o.warehouseName || o.storeName
const pickupAddr = o.warehouseAddress || o.storeAddress
return (
<Cell key={String(o.id)} style={{ padding: '16px' }}>
@@ -337,6 +382,7 @@ export default function TicketOrdersPage() {
</View>
<View className="text-gray-400 text-xs mt-1">{timeText}</View>
<View className="text-gray-400 text-xs mt-1">{ticketNo}</View>
<View className="mt-3 bg-white rounded-lg">
<View className="text-sm text-gray-700">
@@ -349,6 +395,16 @@ export default function TicketOrdersPage() {
{o.nickname || '-'} {o.phone ? `(${o.phone})` : ''}
</Text>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{pickupName || '-'}</Text>
</View>
{pickupAddr ? (
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{pickupAddr}</Text>
</View>
) : null}
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}</Text>
@@ -359,6 +415,12 @@ export default function TicketOrdersPage() {
<Text className="text-gray-500 ml-3"></Text>
<Text>{o.storeName || '-'}</Text>
</View>
{o.storePhone ? (
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{o.storePhone}</Text>
</View>
) : null}
{remark ? (
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
@@ -402,7 +464,7 @@ export default function TicketOrdersPage() {
<Text className="mx-1 text-gray-400">{'>'}</Text>
<Text className={flow3Done ? 'text-purple-600 font-medium' : 'text-gray-400'}>3 </Text>
<Text className="mx-1 text-gray-400">{'>'}</Text>
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 </Text>
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 </Text>
</View>
<View className="mt-3 flex justify-end">
@@ -418,6 +480,29 @@ export default function TicketOrdersPage() {
</Button>
)}
{!!addr && addr !== '-' && (
<Button
size="small"
onClick={e => {
e.stopPropagation()
void Taro.setClipboardData({ data: addr })
Taro.showToast({ title: '地址已复制', icon: 'none' })
}}
>
</Button>
)}
{!!storePhone && (
<Button
size="small"
onClick={e => {
e.stopPropagation()
Taro.makePhoneCall({ phoneNumber: storePhone })
}}
>
</Button>
)}
{canStartDeliver(o) && (
<Button
size="small"
@@ -435,12 +520,24 @@ export default function TicketOrdersPage() {
type="primary"
onClick={e => {
e.stopPropagation()
openDeliverDialog(o)
openDeliverDialog(o, { mode: 'waitCustomerConfirm' })
}}
>
</Button>
)}
{canCompleteByPhoto(o) && (
<Button
size="small"
type="primary"
onClick={e => {
e.stopPropagation()
openDeliverDialog(o, { mode: 'photoComplete' })
}}
>
</Button>
)}
</Space>
</View>
</View>
@@ -455,7 +552,13 @@ export default function TicketOrdersPage() {
<Dialog
title="确认送达"
visible={deliverDialogVisible}
confirmText={deliverSubmitting ? '提交中...' : '确认送达'}
confirmText={
deliverSubmitting
? '提交中...'
: deliverConfirmMode === 'photoComplete'
? '拍照完成'
: '确认送达'
}
cancelText="取消"
onConfirm={handleConfirmDelivered}
onCancel={() => {
@@ -463,10 +566,18 @@ export default function TicketOrdersPage() {
setDeliverDialogVisible(false)
setDeliverOrder(null)
setDeliverImg(undefined)
setDeliverConfirmMode('photoComplete')
}}
>
<View className="text-sm text-gray-700">
<View>/</View>
<View></View>
<View className="mt-3">
<RadioGroup value={deliverConfirmMode} onChange={v => setDeliverConfirmMode(v as DeliverConfirmMode)}>
<Radio value="photoComplete"></Radio>
<Radio value="waitCustomerConfirm"></Radio>
</RadioGroup>
</View>
<View className="mt-3">
<Button size="small" onClick={handleChooseDeliverImg}>
{deliverImg ? '重新拍照/上传' : '拍照/上传'}
@@ -483,7 +594,7 @@ export default function TicketOrdersPage() {
</View>
)}
<View className="mt-3 text-xs text-gray-500">
</View>
</View>
</Dialog>