diff --git a/src/api/glt/gltTicketOrder/model/index.ts b/src/api/glt/gltTicketOrder/model/index.ts index 8fda597..03f155c 100644 --- a/src/api/glt/gltTicketOrder/model/index.ts +++ b/src/api/glt/gltTicketOrder/model/index.ts @@ -34,6 +34,18 @@ export interface GltTicketOrder { address?: string; // 配送时间 sendTime?: string; + // 配送开始时间(配送员点击“开始配送”) + sendStartTime?: string; + // 配送结束时间(配送员确认送达) + sendEndTime?: string; + // 配送员送达拍照(选填/必填由后端策略决定) + sendEndImg?: string; + // 发货/配送状态(建议:10待配送 20配送中 30待客户确认 40已完成) + deliveryStatus?: number; + // 客户确认收货时间(客户点击确认收货) + receiveConfirmTime?: string; + // 客户确认方式(建议:10客户手动确认 20配送照片自动确认 30后台超时自动确认) + receiveConfirmType?: number; // 买家留言 buyerRemarks?: string; // 用于统计 @@ -71,4 +83,10 @@ export interface GltTicketOrderParam extends PageParam { id?: number; keywords?: string; userId?: number; + // 配送员用户ID(用于配送员端查询) + riderId?: number; + // 发货/配送状态(建议与 GltTicketOrder.deliveryStatus 对齐) + deliveryStatus?: number; + // 兼容 ShopOrderParam 的筛选字段(如后端已实现可直接复用) + statusFilter?: number; } diff --git a/src/user/ticket/orders/index.config.ts b/src/user/ticket/orders/index.config.ts index d891ee3..1671edc 100644 --- a/src/user/ticket/orders/index.config.ts +++ b/src/user/ticket/orders/index.config.ts @@ -1,6 +1,5 @@ export default definePageConfig({ - navigationBarTitleText: '送水订单', + navigationBarTitleText: '配送订单', navigationBarTextStyle: 'black', navigationBarBackgroundColor: '#ffffff' }) - diff --git a/src/user/ticket/orders/index.tsx b/src/user/ticket/orders/index.tsx index e8b1412..5f0a905 100644 --- a/src/user/ticket/orders/index.tsx +++ b/src/user/ticket/orders/index.tsx @@ -1,119 +1,492 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' import { View, Text } from '@tarojs/components' -import { Cell, CellGroup, InfiniteLoading, PullToRefresh, Empty, Loading } from '@nutui/nutui-react-taro' +import { + Tabs, + TabPane, + Cell, + Space, + Button, + Dialog, + Image, + Empty, + InfiniteLoading, + PullToRefresh, + Loading +} from '@nutui/nutui-react-taro' import dayjs from 'dayjs' -import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder' -import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model' +import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder' +import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model' +import { uploadFile } from '@/api/system/file' const PAGE_SIZE = 10 export default function TicketOrdersPage() { - const [list, setList] = useState([]) - const [loading, setLoading] = useState(false) - const [hasMore, setHasMore] = useState(true) - const [page, setPage] = useState(1) - - const userId = (() => { + const riderId = useMemo(() => { const raw = Taro.getStorageSync('UserId') const id = Number(raw) return Number.isFinite(id) && id > 0 ? id : undefined - })() + }, []) - const reload = async (isRefresh = true) => { - if (loading) return - if (!userId) { - setList([]) - setHasMore(false) - return - } + const pageRef = useRef(1) + const listRef = useRef([]) - setLoading(true) - try { - const currentPage = isRefresh ? 1 : page - const res = await pageGltTicketOrder({ + const [tabIndex, setTabIndex] = useState(0) + 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 [deliverImg, setDeliverImg] = useState(undefined) + + const riderTabs = useMemo( + () => [ + { index: 0, title: '全部' }, + { index: 1, title: '待配送', deliveryStatus: 10 }, + { index: 2, title: '配送中', deliveryStatus: 20 }, + { index: 3, title: '待确认', deliveryStatus: 30 }, + { index: 4, title: '已完成', deliveryStatus: 40 } + ], + [] + ) + + 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 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 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: 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: 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 + }) + }, + [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) + + const params: GltTicketOrderParam = { page: currentPage, limit: PAGE_SIZE, - userId - } as any) + riderId, + deliveryStatus: currentTab?.deliveryStatus + } - const resList = res?.list || [] - const next = isRefresh ? resList : [...list, ...resList] - setList(next) + try { + 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 total = typeof res?.count === 'number' ? res.count : next.length - setHasMore(next.length < total) - setPage(currentPage + 1) + 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('加载失败,请重试') + setHasMore(false) + } finally { + setLoading(false) + } + }, + [loading, riderId, riderTabs, tabIndex] + ) + + const reloadMore = useCallback(async () => { + if (loading || !hasMore) return + await reload(false) + }, [hasMore, loading, reload]) + + const openDeliverDialog = (order: GltTicketOrder) => { + setDeliverOrder(order) + setDeliverImg(order.sendEndImg) + setDeliverDialogVisible(true) + } + + const handleChooseDeliverImg = async () => { + try { + const file = await uploadFile() + setDeliverImg(file?.url) } catch (e) { - console.error('获取送水订单失败:', e) - Taro.showToast({ title: '获取送水订单失败', icon: 'none' }) - setHasMore(false) + console.error('上传送达照片失败:', e) + 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 + await reload(true) + } catch (e) { + console.error('开始配送失败:', e) + Taro.showToast({ title: '开始配送失败', icon: 'none' }) + } + } + + const handleConfirmDelivered = async () => { + if (!deliverOrder?.id) return + if (deliverSubmitting) return + setDeliverSubmitting(true) + try { + // 说明: + // - sendEndImg:送达照片留档(可选/必填由后端策略决定) + // - sendEndTime:配送员确认送达时间 + // - deliveryStatus:建议后端设置为 30(待客户确认) + await updateGltTicketOrder({ + id: deliverOrder.id, + deliveryStatus: 30, + sendEndTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), + sendEndImg: deliverImg + }) + Taro.showToast({ title: '已确认送达', icon: 'success' }) + setDeliverDialogVisible(false) + setDeliverOrder(null) + setDeliverImg(undefined) + pageRef.current = 1 + await reload(true) + } catch (e) { + console.error('确认送达失败:', e) + Taro.showToast({ title: '确认送达失败', icon: 'none' }) } finally { - setLoading(false) + setDeliverSubmitting(false) } } useEffect(() => { - }, []) + listRef.current = list + }, [list]) useDidShow(() => { - setPage(1) + pageRef.current = 1 + listRef.current = [] + setList([]) setHasMore(true) - reload(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]) + + if (!riderId) { + return ( + + 请先登录 + + ) + } + + const displayList = filterByTab(list) + return ( + setTabIndex(Number(paneKey))} align="left"> + {riderTabs.map(t => ( + + ))} + - - reload(true)}> - {list.length === 0 && !loading ? ( + + { + pageRef.current = 1 + listRef.current = [] + setList([]) + setHasMore(true) + await reload(true) + }} + > + {error ? ( - + + {error} + + ) : ( - - {list.map((o) => { + + + 加载中... + + } + loadMoreText={ + displayList.length === 0 ? ( + + + + ) : ( + 没有更多了 + ) + } + > + {displayList.map(o => { const qty = Number(o.totalNum || 0) - 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 || '' + 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 flow1Done = !!o.riderId + const flow2Done = !!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20) + const flow3Done = !!o.sendEndTime + const flow4Done = !!o.receiveConfirmTime || o.deliveryStatus === 40 + + const phoneToCall = o.phone + return ( - - 送水 {qty || '-'} 桶 - {addr ? {addr} : null} - {remark ? 备注:{remark} : null} + + + + {`订单#${o.id}`} + {getOrderStatusText(o)} - } - extra={{timeText}} - /> + + 下单时间:{timeText} + + + + 收货地址: + {addr} + + + 客户: + + {o.nickname || '-'} {o.phone ? `(${o.phone})` : ''} + + + + 预约配送: + {o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'} + + + 数量: + {qty || '-'} + 门店: + {o.storeName || '-'} + + {remark ? ( + + 备注: + {remark} + + ) : null} + {o.sendStartTime ? ( + + 开始配送: + {dayjs(o.sendStartTime).format('YYYY-MM-DD HH:mm')} + + ) : null} + {o.sendEndTime ? ( + + 送达时间: + {dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')} + + ) : null} + {o.receiveConfirmTime ? ( + + 确认收货: + {dayjs(o.receiveConfirmTime).format('YYYY-MM-DD HH:mm')} + + ) : null} + {o.sendEndImg ? ( + + 送达照片: + + + + + ) : null} + + + {/* 配送流程 */} + + 流程: + 1 派单 + {'>'} + 2 配送中 + {'>'} + 3 送达留档 + {'>'} + 4 客户确认收货 + + + + + {!!phoneToCall && ( + + )} + {canStartDeliver(o) && ( + + )} + {canConfirmDelivered(o) && ( + + )} + + + + ) })} - + )} - - reload(false)} - loadingText={ - - - 加载中... - - } - loadMoreText={ - - {list.length === 0 ? '暂无数据' : '没有更多了'} - - } - /> + + { + if (deliverSubmitting) return + setDeliverDialogVisible(false) + setDeliverOrder(null) + setDeliverImg(undefined) + }} + > + + 到达收货点后,可拍照留档(推荐/可设为必填),再点确认送达。 + + + + {deliverImg && ( + + + + + + + )} + + 送达后订单进入“待客户确认”;客户在用户端确认收货或超时自动确认(需后端支持)。 + + + ) } diff --git a/src/user/ticket/use.tsx b/src/user/ticket/use.tsx index ea8b6ad..2a0ea1e 100644 --- a/src/user/ticket/use.tsx +++ b/src/user/ticket/use.tsx @@ -277,13 +277,13 @@ const OrderConfirm = () => { // 设置商品信息 if (goodsRes) { setGoods(goodsRes) + hasInitialLoadedRef.current = true } // 设置默认收货地址 if (addressRes && addressRes.length > 0) { setAddress(addressRes[0]) } - hasInitialLoadedRef.current = true // Tickets are non-blocking for first paint; load in background. loadUserTickets() } catch (err) {