feat(store): 添加门店管理功能和订单配送功能
- 在app.config.ts中添加门店相关路由配置 - 在config/app.ts中添加租户名称常量 - 在Header.tsx中实现门店选择功能,包括定位、距离计算和门店切换 - 更新ShopOrder模型,添加门店ID、门店名称、配送员ID和仓库ID字段 - 新增ShopStore相关API和服务,支持门店的增删改查 - 新增ShopStoreRider相关API和服务,支持配送员管理 - 新增ShopStoreUser相关API和服务,支持店员管理 - 新增ShopWarehouse相关API和服务,支持仓库管理 - 添加配送订单页面,支持订单状态管理和送达确认功能 - 优化经销商页面的样式布局
This commit is contained in:
4
src/rider/orders/index.config.ts
Normal file
4
src/rider/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '配送订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
378
src/rider/orders/index.tsx
Normal file
378
src/rider/orders/index.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
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 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'
|
||||
|
||||
export default function RiderOrders() {
|
||||
|
||||
const riderId = useMemo(() => {
|
||||
const v = Number(Taro.getStorageSync('UserId'))
|
||||
return Number.isFinite(v) && v > 0 ? v : undefined
|
||||
}, [])
|
||||
|
||||
const pageRef = useRef(1)
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
const [list, setList] = useState<ShopOrder[]>([])
|
||||
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<ShopOrder | null>(null)
|
||||
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
|
||||
|
||||
// 前端展示用:后台可配置实际自动确认收货时长
|
||||
const AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK = 24
|
||||
|
||||
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
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const isAbnormalOrder = (order: ShopOrder) => {
|
||||
const s = order.orderStatus
|
||||
return s === 2 || s === 3 || s === 4 || s === 5 || s === 6 || s === 7
|
||||
}
|
||||
|
||||
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: 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 canConfirmDelivered = (order: ShopOrder) => {
|
||||
if (!order.payStatus) return false
|
||||
if (order.orderStatus === 1) return false
|
||||
if (isAbnormalOrder(order)) return false
|
||||
// 只允许在“配送中”阶段确认送达
|
||||
if (order.deliveryStatus !== 20) 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
|
||||
},
|
||||
[tabIndex]
|
||||
)
|
||||
|
||||
const reload = useCallback(
|
||||
async (resetPage = false) => {
|
||||
if (!riderId) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const currentPage = resetPage ? 1 : pageRef.current
|
||||
const currentTab = riderTabs.find(t => t.index === tabIndex) || riderTabs[0]
|
||||
|
||||
const params: ShopOrderParam = {
|
||||
page: currentPage,
|
||||
riderId,
|
||||
statusFilter: currentTab.statusFilter,
|
||||
}
|
||||
|
||||
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
|
||||
} catch (e) {
|
||||
console.error('加载配送订单失败:', e)
|
||||
setError('加载失败,请重试')
|
||||
setHasMore(false)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[riderId, riderTabs, tabIndex]
|
||||
)
|
||||
|
||||
const reloadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return
|
||||
pageRef.current += 1
|
||||
await reload(false)
|
||||
}, [hasMore, loading, reload])
|
||||
|
||||
const openDeliverDialog = (order: ShopOrder) => {
|
||||
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 handleConfirmDelivered = async () => {
|
||||
if (!deliverOrder?.orderId) return
|
||||
if (deliverSubmitting) 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'})
|
||||
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(() => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
pageRef.current = 1
|
||||
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">
|
||||
|
||||
<View>
|
||||
<Tabs
|
||||
align="left"
|
||||
className="fixed left-0"
|
||||
style={{zIndex: 998, borderBottom: '1px solid #e5e5e5'}}
|
||||
tabStyle={{backgroundColor: '#ffffff'}}
|
||||
value={tabIndex}
|
||||
onChange={(paneKey) => setTabIndex(Number(paneKey))}
|
||||
>
|
||||
{riderTabs.map(t => (
|
||||
<TabPane key={t.index} title={loading && tabIndex === t.index ? `${t.title}...` : t.title}></TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<View style={{height: '84vh', width: '100%', padding: '0', overflowY: 'auto', overflowX: 'hidden'}} id="rider-order-scroll">
|
||||
{error ? (
|
||||
<View className="flex flex-col items-center justify-center h-64">
|
||||
<Text className="text-gray-500 mb-4">{error}</Text>
|
||||
<Button size="small" type="primary" onClick={() => reload(true)}>
|
||||
重新加载
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="rider-order-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
loadingText={<>加载中</>}
|
||||
loadMoreText={
|
||||
displayList.length === 0 ? (
|
||||
<Empty style={{backgroundColor: 'transparent'}} description="暂无配送订单"/>
|
||||
) : (
|
||||
<View className="h-24">没有更多了</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{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
|
||||
|
||||
const autoConfirmAt = o.sendEndTime
|
||||
? dayjs(o.sendEndTime).add(AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK, 'hour')
|
||||
: null
|
||||
const autoConfirmLeftMin = autoConfirmAt ? autoConfirmAt.diff(dayjs(), 'minute') : null
|
||||
|
||||
return (
|
||||
<Cell
|
||||
key={`${o.orderId || idx}`}
|
||||
style={{padding: '16px'}}
|
||||
onClick={() => o.orderId && Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${o.orderId}`})}
|
||||
>
|
||||
<View className="w-full">
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-gray-800 font-bold text-sm">{o.orderNo || `订单#${o.orderId}`}</Text>
|
||||
<Text className={`${getOrderStatusColor(o)} text-sm font-medium`}>{getOrderStatusText(o)}</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-gray-400 text-xs mt-1">
|
||||
下单时间:{o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'}
|
||||
</View>
|
||||
|
||||
<View className="mt-3 bg-white rounded-lg">
|
||||
<View className="text-sm text-gray-700">
|
||||
<Text className="text-gray-500">收货点:</Text>
|
||||
<Text>{o.selfTakeMerchantName || o.address || '-'}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">客户:</Text>
|
||||
<Text>{o.realName || '-'} {o.phone ? `(${o.phone})` : ''}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">金额:</Text>
|
||||
<Text>¥{o.payPrice || o.totalPrice || '-'}</Text>
|
||||
<Text className="text-gray-500 ml-3">数量:</Text>
|
||||
<Text>{o.totalNum ?? '-'}</Text>
|
||||
</View>
|
||||
{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>
|
||||
)}
|
||||
</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 ? (flow3Done ? 'text-green-600 font-medium' : 'text-blue-600 font-medium') : 'text-gray-400'}>2 配送中</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow3Done ? (flow4Done ? 'text-green-600 font-medium' : '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>
|
||||
|
||||
{o.sendEndTime && o.orderStatus !== 1 && autoConfirmAt && (
|
||||
<View className="mt-1 text-gray-500">
|
||||
若客户未确认,预计 {autoConfirmAt.format('YYYY-MM-DD HH:mm')} 自动确认收货(以后台配置为准)
|
||||
{typeof autoConfirmLeftMin === 'number' && autoConfirmLeftMin > 0 ? `,约剩余 ${Math.ceil(autoConfirmLeftMin / 60)} 小时` : ''}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="mt-3 flex justify-end">
|
||||
<Space>
|
||||
{!!phoneToCall && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
Taro.makePhoneCall({phoneNumber: phoneToCall})
|
||||
}}
|
||||
>
|
||||
联系客户
|
||||
</Button>
|
||||
)}
|
||||
{canConfirmDelivered(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o)
|
||||
}}
|
||||
>
|
||||
确认送达
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</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>
|
||||
</Dialog>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user