From f20c8b0961c499265fc28d75bcb9baee3fe2895a Mon Sep 17 00:00:00 2001 From: gxwebsoft <170083662@qq.com> Date: Fri, 6 Feb 2026 23:08:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(rider):=20=E6=9B=B4=E6=96=B0=E9=85=8D?= =?UTF-8?q?=E9=80=81=E8=AE=A2=E5=8D=95=E5=8A=9F=E8=83=BD=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=96=B0=E7=9A=84=E9=80=81=E6=B0=B4=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 ShopOrder 替换为 GltTicketOrder 类型 - 更新 API 调用从 pageShopOrder/updateShopOrder 到 pageGltTicketOrder/updateGltTicketOrder - 重构配送状态管理逻辑,使用 deliveryStatus 字段替代原有状态判断 - 添加新的配送流程按钮:开始配送、补传照片完成等功能 - 实现配送确认模式选择(拍照完成或等待客户确认) - 集成 PullToRefresh 下拉刷新组件提升用户体验 - 添加 Loading 组件优化加载状态显示 - 重构订单列表展示界面适配新订单类型和字段结构 - 实现订单状态颜色标识和流程进度显示 - 添加地址复制和联系门店功能按钮 --- src/rider/orders/index.tsx | 646 +++++++++++++++++++++++++------------ 1 file changed, 433 insertions(+), 213 deletions(-) diff --git a/src/rider/orders/index.tsx b/src/rider/orders/index.tsx index 80b53a5..284d6b1 100644 --- a/src/rider/orders/index.tsx +++ b/src/rider/orders/index.tsx @@ -1,13 +1,28 @@ -import {useCallback, useEffect, useMemo, useRef, useState} from 'react' -import Taro from '@tarojs/taro' -import { Tabs, TabPane, Cell, Space, Button, Dialog, Image, Empty, InfiniteLoading} from '@nutui/nutui-react-taro' -import {View, Text} from '@tarojs/components' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import Taro, { useDidShow } from '@tarojs/taro' +import { + Tabs, + TabPane, + Cell, + Space, + Button, + Dialog, + Radio, + RadioGroup, + Image, + Empty, + InfiniteLoading, + PullToRefresh, + Loading +} from '@nutui/nutui-react-taro' +import { View, Text } from '@tarojs/components' import dayjs from 'dayjs' -import {pageShopOrder, updateShopOrder} from '@/api/shop/shopOrder' -import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model' -import {uploadFile} from '@/api/system/file' +import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder' +import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model' +import { uploadFile } from '@/api/system/file' export default function RiderOrders() { + const PAGE_SIZE = 10 const riderId = useMemo(() => { const v = Number(Taro.getStorageSync('UserId')) @@ -15,110 +30,149 @@ export default function RiderOrders() { }, []) const pageRef = useRef(1) + const listRef = useRef([]) const [tabIndex, setTabIndex] = useState(0) - const [list, setList] = useState([]) + const [list, setList] = useState([]) const [hasMore, setHasMore] = useState(true) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [deliverDialogVisible, setDeliverDialogVisible] = useState(false) const [deliverSubmitting, setDeliverSubmitting] = useState(false) - const [deliverOrder, setDeliverOrder] = useState(null) + const [deliverOrder, setDeliverOrder] = useState(null) const [deliverImg, setDeliverImg] = useState(undefined) - // 前端展示用:后台可配置实际自动确认收货时长 - const AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK = 24 + type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm' + const [deliverConfirmMode, setDeliverConfirmMode] = useState('photoComplete') const riderTabs = useMemo( () => [ - {index: 0, title: '全部', statusFilter: -1}, - {index: 1, title: '配送中', statusFilter: 3}, // 后端:deliveryStatus=20 - {index: 2, title: '待客户确认', statusFilter: 3}, // 同上,前端再按 sendEndTime 细分 - {index: 3, title: '已完成', statusFilter: 5}, // 后端:orderStatus=1 + { index: 0, title: '全部' }, + { index: 1, title: '待配送', deliveryStatus: 10 }, + { index: 2, title: '配送中', deliveryStatus: 20 }, + { index: 3, title: '待确认', deliveryStatus: 30 }, + { index: 4, title: '已完成', deliveryStatus: 40 } ], [] ) - const isAbnormalOrder = (order: ShopOrder) => { - const s = order.orderStatus - return s === 2 || s === 3 || s === 4 || s === 5 || s === 6 || s === 7 + const getOrderStatusText = (order: GltTicketOrder) => { + if (order.status === 1) return '已冻结' + + const deliveryStatus = order.deliveryStatus + if (deliveryStatus === 40) return '已完成' + if (deliveryStatus === 30) return '待客户确认' + if (deliveryStatus === 20) return '配送中' + if (deliveryStatus === 10) return '待配送' + + // 兼容:如果后端暂未下发 deliveryStatus,就用时间字段推断 + if (order.receiveConfirmTime) return '已完成' + if (order.sendEndTime) return '待客户确认' + if (order.sendStartTime) return '配送中' + if (order.riderId) return '待配送' + return '待派单' } - const getOrderStatusText = (order: ShopOrder) => { - if (order.orderStatus === 2) return '已取消' - if (order.orderStatus === 3) return '取消中' - if (order.orderStatus === 4) return '退款申请中' - if (order.orderStatus === 5) return '退款被拒绝' - if (order.orderStatus === 6) return '退款成功' - if (order.orderStatus === 7) return '客户申请退款' - if (!order.payStatus) return '未付款' - if (order.orderStatus === 1) return '已完成' - - // 配送员页:用 sendEndTime 表示“已送达收货点” - if (order.deliveryStatus === 20) { - if (order.sendEndTime) return '待客户确认收货' - return '配送中' - } - if (order.deliveryStatus === 10) return '待发货' - if (order.deliveryStatus === 30) return '部分发货' - return '处理中' + const getOrderStatusColor = (order: GltTicketOrder) => { + const text = getOrderStatusText(order) + if (text === '已完成') return 'text-green-600' + if (text === '待客户确认') return 'text-purple-600' + if (text === '配送中') return 'text-blue-600' + if (text === '待配送') return 'text-amber-600' + if (text === '已冻结') return 'text-orange-600' + return 'text-gray-500' } - const getOrderStatusColor = (order: ShopOrder) => { - if (isAbnormalOrder(order)) return 'text-orange-500' - if (order.orderStatus === 1) return 'text-green-600' - if (order.sendEndTime) return 'text-purple-600' - return 'text-blue-600' + const canStartDeliver = (order: GltTicketOrder) => { + if (!order.id) return false + if (order.status === 1) return false + if (!riderId || order.riderId !== riderId) return false + if (order.deliveryStatus && order.deliveryStatus !== 10) return false + return !order.sendStartTime && !order.sendEndTime } - const canConfirmDelivered = (order: ShopOrder) => { - if (!order.payStatus) return false - if (order.orderStatus === 1) return false - if (isAbnormalOrder(order)) return false + const canConfirmDelivered = (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 + if (order.sendEndTime) return false + // 只允许在“配送中”阶段确认送达 - if (order.deliveryStatus !== 20) return false - return !order.sendEndTime + 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( - (orders: ShopOrder[]) => { - if (tabIndex === 1) { - // 配送中:未确认送达 - return orders.filter(o => o.deliveryStatus === 20 && !o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1) - } - if (tabIndex === 2) { - // 待客户确认:已确认送达 - return orders.filter(o => o.deliveryStatus === 20 && !!o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1) - } - if (tabIndex === 3) { - return orders.filter(o => o.orderStatus === 1) - } - return orders + (orders: GltTicketOrder[]) => { + if (tabIndex === 0) return orders + + const current = riderTabs.find(t => t.index === tabIndex) + const status = current?.deliveryStatus + if (!status) return orders + + // 如果后端已实现 deliveryStatus 筛选,这里基本不会再过滤;否则用兼容逻辑兜底。 + return orders.filter(o => { + const ds = o.deliveryStatus + if (typeof ds === 'number') return ds === status + if (status === 10) return !!o.riderId && !o.sendStartTime && !o.sendEndTime + if (status === 20) return !!o.sendStartTime && !o.sendEndTime + if (status === 30) return !!o.sendEndTime && !o.receiveConfirmTime + if (status === 40) return !!o.receiveConfirmTime + return true + }) }, - [tabIndex] + [riderTabs, tabIndex] ) const reload = useCallback( async (resetPage = false) => { if (!riderId) return + if (loading) return setLoading(true) setError(null) const currentPage = resetPage ? 1 : pageRef.current - const currentTab = riderTabs.find(t => t.index === tabIndex) || riderTabs[0] - - const params: ShopOrderParam = { + const currentTab = riderTabs.find(t => t.index === tabIndex) + const params: GltTicketOrderParam = { page: currentPage, + limit: PAGE_SIZE, riderId, - statusFilter: currentTab.statusFilter, + deliveryStatus: currentTab?.deliveryStatus } try { - const res = await pageShopOrder(params) - const incoming = (res?.list || []) as ShopOrder[] - setList(prev => (resetPage ? incoming : prev.concat(incoming))) - setHasMore(incoming.length >= 10) - pageRef.current = currentPage + const res = await pageGltTicketOrder(params as any) + const incomingAll = (res?.list || []) as GltTicketOrder[] + + // 兼容:后端若暂未实现 riderId 过滤,前端兜底过滤掉非本人的订单 + const incoming = incomingAll.filter(o => o?.deleted !== 1 && o?.riderId === riderId) + + const prev = resetPage ? [] : listRef.current + const next = resetPage ? incoming : prev.concat(incoming) + listRef.current = next + setList(next) + + const total = typeof res?.count === 'number' ? res.count : undefined + const filteredOut = incomingAll.length - incoming.length + if (typeof total === 'number' && filteredOut === 0) { + setHasMore(next.length < total) + } else { + setHasMore(incomingAll.length >= PAGE_SIZE) + } + + pageRef.current = currentPage + 1 } catch (e) { console.error('加载配送订单失败:', e) setError('加载失败,请重试') @@ -127,18 +181,18 @@ export default function RiderOrders() { setLoading(false) } }, - [riderId, riderTabs, tabIndex] + [PAGE_SIZE, loading, riderId, riderTabs, tabIndex] ) const reloadMore = useCallback(async () => { if (loading || !hasMore) return - pageRef.current += 1 await reload(false) }, [hasMore, loading, reload]) - const openDeliverDialog = (order: ShopOrder) => { + const openDeliverDialog = (order: GltTicketOrder, opts?: { mode?: DeliverConfirmMode }) => { setDeliverOrder(order) setDeliverImg(order.sendEndImg) + setDeliverConfirmMode(opts?.mode || (order.sendEndImg ? 'photoComplete' : 'waitCustomerConfirm')) setDeliverDialogVisible(true) } @@ -148,40 +202,100 @@ export default function RiderOrders() { setDeliverImg(file?.url) } catch (e) { console.error('上传送达照片失败:', e) - Taro.showToast({title: '上传失败,请重试', icon: 'none'}) + Taro.showToast({ title: '上传失败,请重试', icon: 'none' }) + } + } + + const handleStartDeliver = async (order: GltTicketOrder) => { + if (!order?.id) return + if (!canStartDeliver(order)) return + try { + await updateGltTicketOrder({ + id: order.id, + deliveryStatus: 20, + sendStartTime: dayjs().format('YYYY-MM-DD HH:mm:ss') + }) + Taro.showToast({ title: '已开始配送', icon: 'success' }) + pageRef.current = 1 + listRef.current = [] + setList([]) + setHasMore(true) + await reload(true) + } catch (e) { + console.error('开始配送失败:', e) + Taro.showToast({ title: '开始配送失败', icon: 'none' }) } } const handleConfirmDelivered = async () => { - if (!deliverOrder?.orderId) return + if (!deliverOrder?.id) return if (deliverSubmitting) return + if (deliverConfirmMode === 'photoComplete' && !deliverImg) { + Taro.showToast({ title: '请先拍照/上传送达照片', icon: 'none' }) + return + } setDeliverSubmitting(true) try { - await updateShopOrder({ - orderId: deliverOrder.orderId, - // 用于前端/后端识别“配送员已送达收货点” - sendEndTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), - sendEndImg: deliverImg, - }) - Taro.showToast({title: '已确认送达', icon: 'success'}) + const now = dayjs().format('YYYY-MM-DD HH:mm:ss') + // 送达时间:首次“确认送达”写入;补传照片时不要覆盖原送达时间 + const deliveredAt = deliverOrder.sendEndTime || now + // - 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 + listRef.current = [] + setList([]) + setHasMore(true) await reload(true) } catch (e) { console.error('确认送达失败:', e) - Taro.showToast({title: '确认送达失败', icon: 'none'}) + Taro.showToast({ title: '确认送达失败', icon: 'none' }) } finally { setDeliverSubmitting(false) } } useEffect(() => { - }, []) + listRef.current = list + }, [list]) + + useDidShow(() => { + pageRef.current = 1 + listRef.current = [] + setList([]) + setHasMore(true) + void reload(true) + // eslint-disable-next-line react-hooks/exhaustive-deps + }) useEffect(() => { pageRef.current = 1 + listRef.current = [] + setList([]) + setHasMore(true) void reload(true) // eslint-disable-next-line react-hooks/exhaustive-deps }, [tabIndex, riderId]) @@ -213,150 +327,245 @@ export default function RiderOrders() { ))} - - {error ? ( - - {error} - - - ) : ( - 加载中} - loadMoreText={ - displayList.length === 0 ? ( - - ) : ( - 没有更多了 - ) - } - > - {displayList.map((o, idx) => { - const phoneToCall = o.phone || o.mobile - const flow1Done = !!o.riderId - const flow2Done = o.deliveryStatus === 20 || o.deliveryStatus === 30 - const flow3Done = !!o.sendEndTime - const flow4Done = o.orderStatus === 1 - // 直接使用订单分页接口返回的 orderGoods - const goodsList = o.orderGoods || [] - const goodsNameList = goodsList - .map(g => g?.goodsName || (g as any)?.goodsTitle || (g as any)?.title || (g as any)?.name) - .filter(Boolean) as string[] - const goodsSummary = goodsNameList.length - ? `${goodsNameList.slice(0, 3).join('、')}${goodsList.length > 3 ? ` 等${goodsList.length}件` : ''}` - : (o.title || '-') + { + pageRef.current = 1 + listRef.current = [] + setList([]) + setHasMore(true) + await reload(true) + }} + headHeight={60} + > + + {error ? ( + + {error} + + + ) : ( + + + 加载中... + + } + loadMoreText={ + displayList.length === 0 ? ( + + ) : ( + 没有更多了 + ) + } + > + {displayList.map(o => { + 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 qty = Number(o.totalNum || 0) - const autoConfirmAt = o.sendEndTime - ? dayjs(o.sendEndTime).add(AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK, 'hour') - : null - const autoConfirmLeftMin = autoConfirmAt ? autoConfirmAt.diff(dayjs(), 'minute') : null + const flow1Done = !!o.riderId + 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 - return ( - o.orderId && Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${o.orderId}`})} - > - - - {o.orderNo || `订单#${o.orderId}`} - {getOrderStatusText(o)} - + const phoneToCall = o.phone + const storePhone = o.storePhone + const pickupName = o.warehouseName || o.storeName + const pickupAddr = o.warehouseAddress || o.storeAddress - - 下单时间:{o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'} - + return ( + + + + + {o.id ? `送水订单#${o.id}` : '送水订单'} + + {getOrderStatusText(o)} + - - - 收货点: - {o.selfTakeMerchantName || o.address || '-'} - - - 客户: - {o.realName || '-'} {o.phone ? `(${o.phone})` : ''} - - - 金额: - ¥{o.payPrice || o.totalPrice || '-'} - 数量: - {o.totalNum ?? '-'} - - - 商品: - {goodsSummary} - - {o.sendEndTime && ( + 下单时间:{timeText} + + + + 客户: + {o.nickname || '-'} {o.phone ? `(${o.phone})` : ''} + - 送达时间: - {dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')} + 收货地址: + {addr} - )} - + {!!remark && ( + + 买家留言: + {remark} + + )} - {/* 配送流程 */} - - 流程: - 1 派单 - {'>'} - 2 配送中 - {'>'} - 3 送达收货点 - {'>'} - 4 客户确认收货 - - {o.sendEndTime && o.orderStatus !== 1 && autoConfirmAt && ( - - 若客户未确认,预计 {autoConfirmAt.format('YYYY-MM-DD HH:mm')} 自动确认收货(以后台配置为准) - {typeof autoConfirmLeftMin === 'number' && autoConfirmLeftMin > 0 ? `,约剩余 ${Math.ceil(autoConfirmLeftMin / 60)} 小时` : ''} + + 数量: + {Number.isFinite(qty) ? qty : '-'} + 金额: + ¥{o.price || '-'} - )} - - - - {!!phoneToCall && ( - + + 配送时间: + {o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'} + + + + 取水点: + {pickupName || '-'} + + + 取水地址: + {pickupAddr || '-'} + + + {!!o.sendStartTime && ( + + 开始配送: + {dayjs(o.sendStartTime).format('YYYY-MM-DD HH:mm')} + )} - {canConfirmDelivered(o) && ( - + {!!o.sendEndTime && ( + + 送达时间: + {dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')} + )} - + {!!o.receiveConfirmTime && ( + + 完成时间: + {dayjs(o.receiveConfirmTime).format('YYYY-MM-DD HH:mm')} + + )} + + {o.sendEndImg ? ( + + 送达照片: + + + + + ) : null} + + + {/* 配送流程 */} + + 流程: + 1 派单 + {'>'} + 2 配送中 + {'>'} + 3 送达留档 + {'>'} + 4 完成 + + + + + {!!phoneToCall && ( + + )} + {!!addr && addr !== '-' && ( + + )} + {!!storePhone && ( + + )} + {canStartDeliver(o) && ( + + )} + {canConfirmDelivered(o) && ( + + )} + {canCompleteByPhoto(o) && ( + + )} + + - - - ) - })} - - )} - + + ) + })} + + )} + + { @@ -364,13 +573,21 @@ export default function RiderOrders() { setDeliverDialogVisible(false) setDeliverOrder(null) setDeliverImg(undefined) + setDeliverConfirmMode('photoComplete') }} > - 到达收货点后,可选拍照留存,再点确认送达。 + 到达收货点后,可选择“拍照留档直接完成”或“等待客户确认收货”。 + + + setDeliverConfirmMode(v as DeliverConfirmMode)}> + 拍照留档(直接完成) + 客户确认收货(可不拍照) + + {deliverImg && ( @@ -383,6 +600,9 @@ export default function RiderOrders() { )} + + 说明:如选择“客户确认收货”,订单进入“待客户确认”;客户在用户端确认收货或超时自动确认(需后端支持)。 +