- 后端 ShopOrderServiceImpl.getByIdRel 填充发货单 shopOrderDelivery 信息 - 前端新增 ShopOrderDelivery 类型,ShopOrder 接口添加 shopOrderDelivery 字段 - 物流页支持仅传 orderId 即可加载订单及发货单,无需必须传 expressNo 和 expressCompany - 物流轨迹查询条件调整,失败时展示发货单信息,不阻塞页面 - 新增发货信息卡展示发货人、联系方式、地址、物流单号、发货时间及发货方式 - 新增收货信息卡,展示订单收货人名称、电话、地址 - 订单列表“待付款”Tab修复状态显示为“待付款”,兼容线下付款订单逻辑 - 组件OrderCard中去除isOffline判断,修正订单状态文案一致性 - 物流详情页复制功能优化,支持复制正确的物流单号 - 优化快递状态提示、时间格式化显示及页面空状态处理 - 电话联系客服按钮事件名称修正和调用逻辑优化
325 lines
13 KiB
TypeScript
325 lines
13 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
||
import { View, Text, ScrollView } from '@tarojs/components'
|
||
import Taro from '@tarojs/taro'
|
||
import { Button } from '@nutui/nutui-react-taro'
|
||
import { queryLogistics, formatLogisticsStatus } from '@/api/shop/shopLogistics'
|
||
import type { LogisticsInfo, LogisticsTrack } from '@/api/shop/shopLogistics'
|
||
import { getShopOrder } from '@/api/shop/shopOrder'
|
||
import type { ShopOrder, ShopOrderDelivery } from '@/api/shop/shopOrder/model'
|
||
import { OrderStatus } from '@/types/order'
|
||
import Loading from '@/components/common/Loading'
|
||
import EmptyState from '@/components/common/EmptyState'
|
||
|
||
definePageConfig({
|
||
navigationBarTitleText: '物流详情',
|
||
})
|
||
|
||
// 发货方式映射
|
||
const DELIVERY_METHOD_MAP: Record<number, string> = {
|
||
10: '手动录入',
|
||
20: '无需物流',
|
||
30: '电子面单',
|
||
}
|
||
|
||
const LogisticsPage: React.FC = () => {
|
||
const { orderId, expressNo: urlExpressNo, expressCompany: urlExpressCompany } = Taro.getCurrentInstance().router?.params || {}
|
||
const [loading, setLoading] = useState(true)
|
||
const [order, setOrder] = useState<ShopOrder | null>(null)
|
||
const [delivery, setDelivery] = useState<ShopOrderDelivery | null>(null)
|
||
const [logisticsInfo, setLogisticsInfo] = useState<LogisticsInfo | null>(null)
|
||
const [trackList, setTrackList] = useState<LogisticsTrack[]>([])
|
||
|
||
useEffect(() => {
|
||
loadLogistics()
|
||
}, [])
|
||
|
||
const loadLogistics = async () => {
|
||
if (!orderId) {
|
||
Taro.showToast({ title: '缺少订单参数', icon: 'none' })
|
||
setLoading(false)
|
||
return
|
||
}
|
||
setLoading(true)
|
||
try {
|
||
// 1) 拉取订单详情(含 shopOrderDelivery,由后端 getByIdRel 填充)
|
||
let orderData: ShopOrder | null = null
|
||
try {
|
||
orderData = await getShopOrder(Number(orderId))
|
||
} catch (err) {
|
||
console.error('加载订单信息失败', err)
|
||
}
|
||
if (orderData) {
|
||
setOrder(orderData)
|
||
setDelivery(orderData.shopOrderDelivery ?? null)
|
||
}
|
||
|
||
// 2) 决定运单号 / 物流公司:优先 URL 参数,其次订单的 shopOrderDelivery
|
||
const expressNo = urlExpressNo || orderData?.shopOrderDelivery?.expressNo || orderData?.expressNo || ''
|
||
// expressCompany 通常是快递公司代码(SF/YTO 等);shopOrderDelivery.expressName 是中文名
|
||
const expressCompany = urlExpressCompany || ''
|
||
|
||
// 3) 如果有运单号 + 物流公司代码,尝试查询物流轨迹(失败不阻塞发货单展示)
|
||
if (expressNo && expressCompany) {
|
||
try {
|
||
const res = await queryLogistics({ orderId, expressNo, expressCompany })
|
||
if (res?.data) {
|
||
setLogisticsInfo(res.data.logisticsInfo || null)
|
||
setTrackList(res.data.trackList || [])
|
||
}
|
||
} catch (err) {
|
||
console.error('查询物流轨迹失败', err)
|
||
}
|
||
}
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 复制单号
|
||
const copyExpressNo = () => {
|
||
const no = logisticsInfo?.expressNo || delivery?.expressNo || order?.expressNo || ''
|
||
if (!no) return
|
||
Taro.setClipboardData({
|
||
data: no,
|
||
success: () => Taro.showToast({ title: '单号已复制', icon: 'success' }),
|
||
})
|
||
}
|
||
|
||
// 联系客服
|
||
const contactService = () => {
|
||
Taro.makePhoneCall({
|
||
phoneNumber: '400-811-1111',
|
||
fail: () => Taro.showToast({ title: '拨打失败', icon: 'none' }),
|
||
})
|
||
}
|
||
|
||
// 格式化时间
|
||
const formatTime = (time?: string) => {
|
||
if (!time) return '-'
|
||
return time.replace('T', ' ').slice(0, 19)
|
||
}
|
||
|
||
if (loading) {
|
||
return <Loading fullscreen />
|
||
}
|
||
|
||
// 既无物流轨迹,又无发货单,又无订单 → 空状态
|
||
const hasAnything = logisticsInfo || trackList?.length || delivery || order
|
||
if (!hasAnything) {
|
||
return (
|
||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||
<EmptyState text='暂无物流信息' />
|
||
</View>
|
||
)
|
||
}
|
||
|
||
const statusInfo = logisticsInfo ? formatLogisticsStatus(logisticsInfo.status) : null
|
||
|
||
// 发货状态文案:优先用 logisticsInfo.status,否则按订单 deliveryStatus 推断
|
||
const getDeliveryStatusText = (): string => {
|
||
if (logisticsInfo?.status) return logisticsInfo.status
|
||
const ds = order?.deliveryStatus
|
||
if (ds === 30) return '部分发货'
|
||
if (ds === 20) return '已发货'
|
||
if (ds === 10) return '未发货'
|
||
if (order?.orderStatus === OrderStatus.Completed) return '已签收'
|
||
return '已发货'
|
||
}
|
||
|
||
// 物流公司名:优先 logisticsInfo,其次 delivery.expressName
|
||
const expressCompanyName = logisticsInfo?.expressCompanyName || delivery?.expressName || '未知物流'
|
||
// 运单号:优先 logisticsInfo,其次 delivery,最后 order
|
||
const expressNo = logisticsInfo?.expressNo || delivery?.expressNo || order?.expressNo || ''
|
||
// 发货人信息
|
||
const sendName = delivery?.sendName || order?.expressMerchantName || '-'
|
||
const sendPhone = delivery?.sendPhone || '-'
|
||
const sendAddress = delivery?.sendAddress || '-'
|
||
const deliveryMethodText = delivery?.deliveryMethod != null
|
||
? DELIVERY_METHOD_MAP[delivery.deliveryMethod] || '-'
|
||
: (expressNo ? '手动录入' : '无需物流')
|
||
|
||
return (
|
||
<View className='min-h-screen bg-gray-50 pb-4 flex flex-col'>
|
||
<ScrollView scrollY className='flex-1'>
|
||
{/* 快递信息卡片 */}
|
||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||
<View className='flex items-center gap-3'>
|
||
<View className='w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center'>
|
||
<Text className='text-xl'>📦</Text>
|
||
</View>
|
||
<View className='flex-1'>
|
||
<View className='flex items-center gap-2'>
|
||
<Text className='text-sm font-medium text-gray-800'>{expressCompanyName}</Text>
|
||
<Text className='text-xs text-gray-400'>•</Text>
|
||
<Text className='text-sm text-gray-500'>{getDeliveryStatusText()}</Text>
|
||
</View>
|
||
<Text className='text-xs text-gray-400 mt-1'>
|
||
运单号: {expressNo || '暂无'}
|
||
</Text>
|
||
</View>
|
||
{expressNo ? (
|
||
<View
|
||
className='px-3 py-1 rounded-full bg-gray-50'
|
||
onClick={copyExpressNo}
|
||
>
|
||
<Text className='text-xs text-gray-500'>复制</Text>
|
||
</View>
|
||
) : null}
|
||
</View>
|
||
|
||
{/* 发货方式 */}
|
||
<View className='mt-3 pt-3 border-t border-gray-100 flex justify-between'>
|
||
<Text className='text-xs text-gray-400'>发货方式</Text>
|
||
<Text className='text-xs text-gray-600'>{deliveryMethodText}</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 发货单信息(shopOrderDelivery) */}
|
||
{delivery ? (
|
||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>发货信息</Text>
|
||
<View className='space-y-2'>
|
||
<View className='flex justify-between'>
|
||
<Text className='text-xs text-gray-400'>发货人</Text>
|
||
<Text className='text-xs text-gray-600'>{sendName}</Text>
|
||
</View>
|
||
<View className='flex justify-between'>
|
||
<Text className='text-xs text-gray-400'>联系方式</Text>
|
||
<Text className='text-xs text-gray-600'>{sendPhone}</Text>
|
||
</View>
|
||
<View className='flex justify-between'>
|
||
<Text className='text-xs text-gray-400'>发货地址</Text>
|
||
<Text className='text-xs text-gray-600 text-right flex-1 ml-4'>{sendAddress}</Text>
|
||
</View>
|
||
<View className='flex justify-between'>
|
||
<Text className='text-xs text-gray-400'>物流单号</Text>
|
||
<View className='flex items-center gap-1'>
|
||
<Text className='text-xs text-gray-600'>{delivery.expressNo || '-'}</Text>
|
||
{delivery.expressNo ? (
|
||
<Text className='text-xs text-blue-500' onClick={copyExpressNo}>复制</Text>
|
||
) : null}
|
||
</View>
|
||
</View>
|
||
{delivery.createTime ? (
|
||
<View className='flex justify-between'>
|
||
<Text className='text-xs text-gray-400'>发货时间</Text>
|
||
<Text className='text-xs text-gray-600'>{formatTime(delivery.createTime)}</Text>
|
||
</View>
|
||
) : null}
|
||
</View>
|
||
</View>
|
||
) : null}
|
||
|
||
{/* 收货人信息(从订单读取) */}
|
||
{order ? (
|
||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||
<Text className='text-sm font-medium text-gray-800 mb-2 block'>收货信息</Text>
|
||
<View className='flex items-center gap-2'>
|
||
<Text className='text-sm text-gray-600'>{order.realName || '-'}</Text>
|
||
<Text className='text-sm text-gray-500'>{order.mobile || order.phone || '-'}</Text>
|
||
</View>
|
||
<Text className='text-xs text-gray-400 mt-1'>{order.address || '-'}</Text>
|
||
</View>
|
||
) : null}
|
||
|
||
{/* 物流轨迹(仅在查询到时展示) */}
|
||
{trackList && trackList.length > 0 ? (
|
||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>物流详情</Text>
|
||
<View className='relative'>
|
||
{/* 竖线 */}
|
||
<View className='absolute left-4 top-0 bottom-0 w-px bg-gray-200' />
|
||
|
||
{trackList.map((item, idx) => (
|
||
<View key={idx} className={`relative flex gap-3 pb-4 ${idx === trackList.length - 1 ? 'pb-0' : ''}`}>
|
||
{/* 圆点 */}
|
||
<View
|
||
className={`w-8 h-8 rounded-full flex items-center justify-center z-10 ${
|
||
item.isCompleted ? 'bg-green-500' : 'bg-white border-2 border-green-500'
|
||
}`}
|
||
>
|
||
{item.isCompleted && idx !== 0 ? (
|
||
<Text className='text-white text-xs'>✓</Text>
|
||
) : !item.isCompleted ? (
|
||
<View className='w-2 h-2 rounded-full bg-green-500' />
|
||
) : null}
|
||
</View>
|
||
|
||
{/* 内容 */}
|
||
<View className='flex-1 pt-1'>
|
||
<View className='flex justify-between items-start'>
|
||
<Text className={`text-sm ${item.isCompleted ? 'text-gray-600' : 'text-gray-800 font-medium'}`}>
|
||
{item.status}
|
||
</Text>
|
||
<Text className='text-xs text-gray-400'>{formatTime(item.time)}</Text>
|
||
</View>
|
||
<Text className={`text-xs mt-1 ${item.isCompleted ? 'text-gray-400' : 'text-gray-500'}`}>
|
||
{item.description}
|
||
</Text>
|
||
<Text className='text-xs text-gray-400 mt-1'>{item.location}</Text>
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
) : null}
|
||
|
||
{/* 当前状态(仅在查询到 logisticsInfo 时展示) */}
|
||
{logisticsInfo ? (
|
||
<View className='bg-white mx-3 mt-3 p-4 rounded-xl'>
|
||
<View className='flex items-start gap-3'>
|
||
<View className={`w-10 h-10 rounded-full flex items-center justify-center ${statusInfo?.icon === '🚚' ? 'bg-green-50' : 'bg-gray-50'}`}>
|
||
<Text>{statusInfo?.icon}</Text>
|
||
</View>
|
||
<View className='flex-1'>
|
||
<Text className='text-sm font-medium text-gray-800'>{logisticsInfo.status}</Text>
|
||
{logisticsInfo.currentLocation ? (
|
||
<Text className='text-xs text-gray-400 mt-1'>
|
||
当前位于: {logisticsInfo.currentLocation}
|
||
</Text>
|
||
) : null}
|
||
<Text className='text-xs text-gray-400 mt-1'>
|
||
更新时间: {formatTime(logisticsInfo.updateTime)}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
) : null}
|
||
|
||
{/* 温馨提示 */}
|
||
<View className='bg-orange-50 mx-3 mt-3 p-4 rounded-xl'>
|
||
<Text className='text-sm text-orange-600 font-medium'>温馨提示</Text>
|
||
<Text className='text-xs text-orange-500 mt-1 leading-5'>
|
||
1. 快递在运输过程中可能存在延迟,请耐心等待{'\n'}
|
||
2. 如有疑问可联系快递公司客服{'\n'}
|
||
3. 签收前请检查包裹是否完好
|
||
</Text>
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{/* 底部操作 */}
|
||
<View className='bg-white border-t border-gray-100 p-3' style={{ paddingBottom: '20px' }}>
|
||
<View className='flex gap-3'>
|
||
<Button
|
||
size='small'
|
||
className='flex-1 rounded-full border-gray-300 text-gray-600'
|
||
onClick={contactService}
|
||
>
|
||
联系客服
|
||
</Button>
|
||
<Button
|
||
size='small'
|
||
className='flex-1 rounded-full'
|
||
style={{ backgroundColor: '#0e932e' }}
|
||
onClick={() => Taro.showToast({ title: '功能开发中', icon: 'none' })}
|
||
>
|
||
催单
|
||
</Button>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default LogisticsPage
|