Files
xinlong-shop-taro/src/pages/order/logistics.tsx
赵忠林 b45a5722eb feat(order-logistics): 改造订单详情物流查看功能
- 后端 ShopOrderServiceImpl.getByIdRel 填充发货单 shopOrderDelivery 信息
- 前端新增 ShopOrderDelivery 类型,ShopOrder 接口添加 shopOrderDelivery 字段
- 物流页支持仅传 orderId 即可加载订单及发货单,无需必须传 expressNo 和 expressCompany
- 物流轨迹查询条件调整,失败时展示发货单信息,不阻塞页面
- 新增发货信息卡展示发货人、联系方式、地址、物流单号、发货时间及发货方式
- 新增收货信息卡,展示订单收货人名称、电话、地址
- 订单列表“待付款”Tab修复状态显示为“待付款”,兼容线下付款订单逻辑
- 组件OrderCard中去除isOffline判断,修正订单状态文案一致性
- 物流详情页复制功能优化,支持复制正确的物流单号
- 优化快递状态提示、时间格式化显示及页面空状态处理
- 电话联系客服按钮事件名称修正和调用逻辑优化
2026-07-15 16:38:24 +08:00

325 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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