@@ -1,105 +1,304 @@
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 < GltTicketOrder [ ] > ( [ ] )
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 < GltTicketOrder [ ] > ( [ ] )
const [ tabIndex , setTabIndex ] = u seState ( 0 )
const [ list , setList ] = u seState < GltTicketOrder [ ] > ( [ ] )
const [ hasMore , setHasMore ] = useState ( true )
const [ loading , setLoading ] = useState ( false )
const [ error , setError ] = useState < string | null > ( null )
const [ deliverDialogVisible , setDeliverDialogVisible ] = useState ( false )
const [ deliverSubmitting , setDeliverSubmitting ] = useState ( false )
const [ deliverOrder , setDeliverOrder ] = useState < GltTicketOrder | null > ( null )
const [ deliverImg , setDeliverImg ] = useState < string | undefined > ( 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 )
try {
const currentPage = isRefresh ? 1 : page
const res = await pageGltTicketOrder ( {
setError ( null )
const currentPage = resetPage ? 1 : pageRef.current
const currentTab = riderTabs . find ( t = > t . index === tabIndex )
const params : GltTicketOrderParam = {
page : currentPage ,
limit : PAGE_SIZE ,
us erId
} a s any )
rid erId,
deliveryStatu s : currentTab?.deliveryStatus
}
const resList = res ? . list || [ ]
const next = isRef resh ? resList : [ . . . list , . . . resList ]
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 prev = resetPage ? [ ] : listRef . current
const next = resetPage ? incoming : prev.concat ( incoming )
listRef . current = next
setList ( next )
const total = typeof res ? . count === 'number' ? res.count : next.length
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 )
setPage ( currentPage + 1 )
} else {
setHasMore ( incomingAll . length >= PAGE_SIZE )
}
pageRef . current = currentPage + 1
} catch ( e ) {
console . error ( '获取送水 订单失败:' , e )
Taro . showToast ( { title : '获取送水订单失败' , icon : 'none' } )
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' } )
}
}
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 {
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 (
< View className = "bg-gray-50 min-h-screen p-4" >
< Text > 请 先 登 录 < / Text >
< / View >
)
}
const displayList = filterByTab ( list )
return (
< View className = "bg-gray-50 min-h-screen" >
< Tabs value = { tabIndex } onChange = { paneKey = > setTabIndex ( Number ( paneKey ) ) } align = "left" >
{ riderTabs . map ( t = > (
< TabPane key = { t . index } title = { loading && tabIndex === t . index ? ` ${ t . title } ... ` : t . title } / >
) ) }
< / Tabs >
< View className = "px-3" >
< PullToRefresh onRefresh = { ( ) = > reload ( true ) } >
{ list . length === 0 && ! loading ? (
< View className = "px-3 pb-4 " >
< PullToRefresh
onRefresh = { async ( ) = > {
pageRef . current = 1
listRef . current = [ ]
setList ( [ ] )
setHasMore ( true )
await reload ( true )
} }
>
{ error ? (
< View className = "bg-white rounded-lg p-6" >
< Empty description = "暂无送水订单" / >
< View className = "flex flex-col items-center justify-center" >
< Text className = "text-gray-500 mb-3" > { error } < / Text >
< Button size = "small" type = "primary" onClick = { ( ) = > reload ( true ) } >
重 新 加 载
< / Button >
< / View >
< / View >
) : (
< CellGroup >
{ list . 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 || ''
return (
< Cell
key = { o . id }
title = {
< View className = "flex flex-col" >
< Text className = "text-sm text-gray-900" > 送 水 { qty || '-' } 桶 < / Text >
{ addr ? < Text className = "text-xs text-gray-500" > { addr } < / Text > : null }
{ remark ? < Text className = "text-xs text-gray-500" > 备 注 : { remark } < / Text > : null }
< / View >
}
extra = { < Text className = "text-xs text-gray-500" > { timeText } < / Text > }
/ >
)
} ) }
< / CellGroup >
) }
< InfiniteLoading
hasMore = { hasMore }
onLoadMore = { ( ) = > reload ( false ) }
onLoadMore = { reloadMore }
loadingText = {
< View className = "flex justify-center items-center py-4" >
< Loading / >
@@ -107,13 +306,187 @@ export default function TicketOrdersPage() {
< / View >
}
loadMoreText = {
< View className = "text-center py-4 text-gray-500" >
{ list . length === 0 ? '暂无数据' : '没有更多了' }
displayList . length === 0 ? (
< View className = "bg-white rounded-lg p-6" >
< Empty description = "暂无配送订单" / >
< / View >
) : (
< View className = "text-center py-4 text-gray-500" > 没 有 更 多 了 < / View >
)
}
/ >
>
{ 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 || 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 (
< Cell key = { String ( o . id ) } style = { { padding : '16px' } } >
< View className = "w-full" >
< View className = "flex justify-between items-center" >
< Text className = "text-gray-800 font-bold text-sm" > { ` 订单# ${ o . id } ` } < / Text >
< Text className = { ` ${ getOrderStatusColor ( o ) } text-sm font-medium ` } > { getOrderStatusText ( o ) } < / Text >
< / View >
< View className = "text-gray-400 text-xs mt-1" > 下 单 时 间 : { timeText } < / View >
< View className = "mt-3 bg-white rounded-lg" >
< View className = "text-sm text-gray-700" >
< Text className = "text-gray-500" > 收 货 地 址 : < / Text >
< Text > { addr } < / Text >
< / View >
< View className = "text-sm text-gray-700 mt-1" >
< Text className = "text-gray-500" > 客 户 : < / Text >
< Text >
{ o . nickname || '-' } { o . phone ? ` ( ${ o . phone } ) ` : '' }
< / Text >
< / View >
< 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 >
< / View >
< View className = "text-sm text-gray-700 mt-1" >
< Text className = "text-gray-500" > 数 量 : < / Text >
< Text > { qty || '-' } < / Text >
< Text className = "text-gray-500 ml-3" > 门 店 : < / Text >
< Text > { o . storeName || '-' } < / Text >
< / View >
{ remark ? (
< View className = "text-sm text-gray-700 mt-1" >
< Text className = "text-gray-500" > 备 注 : < / Text >
< Text > { remark } < / Text >
< / View >
) : null }
{ o . sendStartTime ? (
< View className = "text-sm text-gray-700 mt-1" >
< Text className = "text-gray-500" > 开 始 配 送 : < / Text >
< Text > { dayjs ( o . sendStartTime ) . format ( 'YYYY-MM-DD HH:mm' ) } < / Text >
< / View >
) : null }
{ o . sendEndTime ? (
< View className = "text-sm text-gray-700 mt-1" >
< Text className = "text-gray-500" > 送 达 时 间 : < / Text >
< Text > { dayjs ( o . sendEndTime ) . format ( 'YYYY-MM-DD HH:mm' ) } < / Text >
< / View >
) : null }
{ o . receiveConfirmTime ? (
< View className = "text-sm text-gray-700 mt-1" >
< Text className = "text-gray-500" > 确 认 收 货 : < / Text >
< Text > { dayjs ( o . receiveConfirmTime ) . format ( 'YYYY-MM-DD HH:mm' ) } < / Text >
< / View >
) : null }
{ o . sendEndImg ? (
< View className = "text-sm text-gray-700 mt-2" >
< Text className = "text-gray-500" > 送 达 照 片 : < / Text >
< View className = "mt-2" >
< Image src = { o . sendEndImg } width = "100%" height = "120" / >
< / View >
< / View >
) : null }
< / View >
{ /* 配送流程 */ }
< View className = "mt-3 bg-gray-50 rounded-lg p-2 text-xs" >
< Text className = "text-gray-600" > 流 程 : < / Text >
< Text className = { flow1Done ? 'text-green-600 font-medium' : 'text-gray-400' } > 1 派 单 < / Text >
< Text className = "mx-1 text-gray-400" > { '>' } < / Text >
< Text className = { flow2Done ? 'text-blue-600 font-medium' : 'text-gray-400' } > 2 配 送 中 < / Text >
< 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 >
< / View >
< View className = "mt-3 flex justify-end" >
< Space >
{ ! ! phoneToCall && (
< Button
size = "small"
onClick = { e = > {
e . stopPropagation ( )
Taro . makePhoneCall ( { phoneNumber : phoneToCall } )
} }
>
联 系 客 户
< / Button >
) }
{ canStartDeliver ( o ) && (
< Button
size = "small"
onClick = { e = > {
e . stopPropagation ( )
void handleStartDeliver ( o )
} }
>
开 始 配 送
< / Button >
) }
{ canConfirmDelivered ( o ) && (
< Button
size = "small"
type = "primary"
onClick = { e = > {
e . stopPropagation ( )
openDeliverDialog ( o )
} }
>
确 认 送 达
< / Button >
) }
< / Space >
< / View >
< / View >
< / Cell >
)
} ) }
< / InfiniteLoading >
) }
< / PullToRefresh >
< / View >
< Dialog
title = "确认送达"
visible = { deliverDialogVisible }
confirmText = { deliverSubmitting ? '提交中...' : '确认送达' }
cancelText = "取消"
onConfirm = { handleConfirmDelivered }
onCancel = { ( ) = > {
if ( deliverSubmitting ) return
setDeliverDialogVisible ( false )
setDeliverOrder ( null )
setDeliverImg ( undefined )
} }
>
< View className = "text-sm text-gray-700" >
< View > 到 达 收 货 点 后 , 可 拍 照 留 档 ( 推 荐 / 可 设 为 必 填 ) , 再 点 确 认 送 达 。 < / View >
< View className = "mt-3" >
< Button size = "small" onClick = { handleChooseDeliverImg } >
{ deliverImg ? '重新拍照/上传' : '拍照/上传' }
< / Button >
< / View >
{ deliverImg && (
< View className = "mt-3" >
< Image src = { deliverImg } width = "100%" height = "120" / >
< View className = "mt-2 flex justify-end" >
< Button size = "small" onClick = { ( ) = > setDeliverImg ( undefined ) } >
移 除 照 片
< / Button >
< / View >
< / View >
) }
< View className = "mt-3 text-xs text-gray-500" >
送 达 后 订 单 进 入 “ 待 客 户 确 认 ” ; 客 户 在 用 户 端 确 认 收 货 或 超 时 自 动 确 认 ( 需 后 端 支 持 ) 。
< / View >
< / View >
< / Dialog >
< / View >
)
}