Files
xinlong-shop-taro/src/pages/order/detail.tsx
赵忠林 ad3c209fe5 feat(order): 优化订单备注结构及线下付款状态显示
- 门店订单修改金额时,修改原因从追加 comments 改为保存 merchantRemarks 字段
- ShopOrder 和 OrderCreateRequest 接口新增 buyerRemarks 和 merchantRemarks 字段,实现订单备注字段分离
- 结算页由 comments 注入变更为 buyerRemarks,门店订单列表新增买家备注展示块
- 修复线下付款订单状态显示:统一线下付款(payType=9)与货到付款(payType=8)的待发货状态显示
- 取消按钮逻辑优化,已付款订单不显示取消按钮,文案由"关闭订单"改为"取消订单"
- 订单列表和详情页弹窗标题及提示文案更新,确保状态描述准确,提升用户体验
2026-07-15 03:07:35 +08:00

561 lines
21 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, useCallback } from 'react'
import { View, Text, Image, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { Button } from '@nutui/nutui-react-taro'
import { getShopOrder, prepayShopOrder, updateShopOrder, repairOrder, removeShopOrder } from '@/api/shop/shopOrder'
import { listShopOrderGoodsByOrderId } from '@/api/shop/shopOrderGoods'
import type { ShopOrder } from '@/api/shop/shopOrder/model'
import { OrderStatus, OrderStatusText } from '@/types/order'
import Price from '@/components/common/Price'
import { getCompressedImageUrl } from '@/utils/image'
definePageConfig({
navigationBarTitleText: '订单详情',
})
// 支付方式映射
const PAY_TYPE_MAP: Record<number, string> = {
0: '余额支付',
1: '微信支付',
2: '支付宝',
3: '银联支付',
4: '现金支付',
5: 'POS机',
6: '免费',
7: '积分支付',
8: '货到付款',
9: '线下付款',
}
// 发票状态映射
const INVOICE_STATUS_MAP: Record<number, string> = {
0: '未开票',
1: '已开票',
2: '不可开票',
}
// 发货状态映射
const DELIVERY_STATUS_MAP: Record<number, string> = {
10: '未发货',
20: '已发货',
30: '部分发货',
}
/** 根据订单状态计算展示用的状态标签 */
const getOrderDisplayStatus = (order: ShopOrder): { title: string; bgColor: string; subtitle: string } => {
const { payStatus, deliveryStatus, orderStatus, payType } = order
const isCod = payType === 8 // 货到付款
const isOffline = payType === 9 // 线下付款
// 退款/取消相关状态
if (orderStatus === OrderStatus.Cancelled) {
return { bgColor: '#999', title: '已取消', subtitle: '订单已取消' }
}
if (orderStatus === OrderStatus.Cancelling) {
return { bgColor: '#ff7d00', title: '取消中', subtitle: '退款处理中' }
}
if (orderStatus === OrderStatus.RefundSuccess) {
return { bgColor: '#999', title: '已退款', subtitle: '退款已原路返回' }
}
if (orderStatus === OrderStatus.RefundApply || orderStatus === OrderStatus.ClientRefundApply) {
return { bgColor: '#ee0a24', title: '退款申请中', subtitle: '商家正在处理退款' }
}
if (orderStatus === OrderStatus.RefundRejected) {
return { bgColor: '#ee0a24', title: '退款被拒绝', subtitle: '退款申请已被拒绝' }
}
// 正常订单流程
if (!payStatus && !isCod && !isOffline) {
return { bgColor: '#ff7d00', title: '待付款', subtitle: '请尽快完成支付' }
}
if (isOffline && !payStatus) {
return { bgColor: '#4b9cf5', title: '待发货', subtitle: '请通过微信转账完成付款,商家确认后发货' }
}
if (isCod && !payStatus) {
return { bgColor: '#4b9cf5', title: '待发货', subtitle: '送达时支付' }
}
if (deliveryStatus === 10) {
return { bgColor: '#4b9cf5', title: '待发货', subtitle: '商家正在准备商品' }
}
if (deliveryStatus === 20 || deliveryStatus === 30) {
return { bgColor: '#4b9cf5', title: '待收货', subtitle: '商品运输中,请注意查收' }
}
if (orderStatus === OrderStatus.Completed) {
return { bgColor: '#0e932e', title: '已完成', subtitle: '交易已完成,感谢购买' }
}
if (orderStatus === OrderStatus.Unused) {
return { bgColor: '#0e932e', title: '已付款', subtitle: '订单已支付' }
}
// 默认
return { bgColor: '#999', title: OrderStatusText[orderStatus ?? 0] || '未知', subtitle: '' }
}
/** 根据订单状态判断当前操作阶段 */
type OrderPhase = 'unpaid' | 'offline_pending' | 'unshipped' | 'shipped' | 'completed' | 'cancelled' | 'refund'
const getOrderPhase = (order: ShopOrder): OrderPhase => {
const { payStatus, deliveryStatus, orderStatus, payType } = order
const isCod = payType === 8 // 货到付款
const isOffline = payType === 9 // 线下付款
// 退款/取消
if ([OrderStatus.Cancelled, OrderStatus.Cancelling, OrderStatus.RefundSuccess, OrderStatus.RefundApply, OrderStatus.ClientRefundApply, OrderStatus.RefundRejected].includes(orderStatus as OrderStatus)) {
return orderStatus === OrderStatus.Cancelled || orderStatus === OrderStatus.RefundSuccess ? 'cancelled' : 'refund'
}
// 线下付款且未确认收款:等待商家确认,不显示"立即支付"按钮
if (isOffline && !payStatus) return 'offline_pending'
// 货到付款订单payStatus=true 但还未实际付款,直接进入 unshipped 阶段(不需要"立即支付"按钮)
if (!payStatus && !isCod) return 'unpaid'
if (deliveryStatus === 10) return 'unshipped'
if (deliveryStatus === 20 || deliveryStatus === 30) return 'shipped'
if (orderStatus === OrderStatus.Completed) return 'completed'
return 'unshipped'
}
const OrderDetailPage: React.FC = () => {
const { id } = Taro.getCurrentInstance().router?.params || {}
const [order, setOrder] = useState<ShopOrder | null>(null)
const [loading, setLoading] = useState(false)
const loadOrder = useCallback(async () => {
if (!id) return
try {
const orderData = await getShopOrder(Number(id))
// 如果订单中没有商品清单,则单独加载
if (!orderData.orderGoods || orderData.orderGoods.length === 0) {
try {
const goods = await listShopOrderGoodsByOrderId(Number(id))
orderData.orderGoods = goods
} catch {
// 忽略加载商品失败
}
}
setOrder(orderData)
} catch {
Taro.showToast({ title: '加载失败', icon: 'none' })
}
}, [id])
useEffect(() => {
loadOrder()
}, [loadOrder])
// 格式化时间
const formatTime = (time: string | undefined) => {
if (!time) return '-'
return time.replace('T', ' ').slice(0, 19)
}
// 取消订单
const handleCancelOrder = () => {
Taro.showModal({
title: '确认取消',
content: '确定要取消该订单吗?',
confirmColor: '#ee0a24',
success: async (res) => {
if (res.confirm && order) {
try {
await updateShopOrder({ ...order, orderStatus: OrderStatus.Cancelled })
Taro.showToast({ title: '订单已取消', icon: 'success' })
loadOrder()
} catch {
Taro.showToast({ title: '取消失败', icon: 'none' })
}
}
}
})
}
// 立即支付
const handlePay = async () => {
if (!order || loading) return
setLoading(true)
try {
const payResult = await prepayShopOrder({ orderId: order.orderId!, payType: 1 })
if (payResult) {
await Taro.requestPayment({
timeStamp: payResult.timeStamp,
nonceStr: payResult.nonceStr,
package: payResult.package,
signType: payResult.signType,
paySign: payResult.paySign,
})
Taro.showToast({ title: '支付成功', icon: 'success' })
loadOrder()
}
} catch (err: any) {
if (err?.message !== 'requestPayment:fail cancel') {
Taro.showToast({ title: err?.message || '支付失败', icon: 'none' })
}
} finally {
setLoading(false)
}
}
// 确认收货
const handleConfirmReceive = () => {
Taro.showModal({
title: '确认收货',
content: '确认已收到商品?',
confirmColor: '#0e932e',
success: async (res) => {
if (res.confirm && order) {
try {
await updateShopOrder({ ...order, orderStatus: OrderStatus.Completed })
Taro.showToast({ title: '已确认收货', icon: 'success' })
loadOrder()
} catch {
Taro.showToast({ title: '操作失败', icon: 'none' })
}
}
}
})
}
// 删除订单
const handleDeleteOrder = () => {
Taro.showModal({
title: '删除订单',
content: '确定要删除该订单吗?删除后不可恢复',
confirmColor: '#ee0a24',
success: async (res) => {
if (res.confirm && order) {
try {
await removeShopOrder(order.orderId)
Taro.showToast({ title: '订单已删除', icon: 'success' })
setTimeout(() => Taro.navigateBack(), 1000)
} catch {
Taro.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
}
if (!order) {
return (
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
<Text className='text-gray-400 text-sm'>...</Text>
</View>
)
}
const displayStatus = getOrderDisplayStatus(order)
const phase = getOrderPhase(order)
const isExpress = order.deliveryType === 0 || order.deliveryType === undefined
const goodsCount = order.orderGoods?.reduce((sum, g) => sum + (g.totalNum || 1), 0) || 0
return (
<View className='min-h-screen bg-gray-50 flex flex-col'>
<ScrollView scrollY className='flex-1'>
{/* 状态卡片 */}
<View
className='p-4'
style={{ backgroundColor: displayStatus.bgColor }}
>
<Text className='text-white text-lg font-medium block'>{displayStatus.title}</Text>
<Text className='text-white text-sm opacity-90 mt-1 block'>{displayStatus.subtitle}</Text>
{phase === 'shipped' && (
<View
className='mt-2 px-3 py-2 rounded-lg inline-flex items-center'
style={{ backgroundColor: 'rgba(255,255,255,0.2)' }}
onClick={() => Taro.navigateTo({ url: `/pages/order/logistics?orderId=${order.orderId}` })}
>
<Text className='text-white text-xs'></Text>
<Text className='text-white text-xs ml-1'></Text>
</View>
)}
</View>
{/* 收货信息 */}
{isExpress ? (
<View className='bg-white mx-3 mt-3 p-3 rounded-lg'>
<View className='flex items-start gap-2'>
<Text className='text-base'>📦</Text>
<View className='flex-1'>
<View className='flex gap-2 mb-1'>
<Text className='text-sm font-medium text-gray-800'>{order.realName || '未知'}</Text>
<Text className='text-sm text-gray-600'>{order.mobile || order.phone || '-'}</Text>
</View>
<Text className='text-xs text-gray-500'>{order.address || '未填写收货地址'}</Text>
{(order.sendStartTime || order.sendEndTime) && (
<Text className='text-xs text-gray-400 mt-1'>
: {order.sendStartTime?.slice(0, 10)} ~ {order.sendEndTime?.slice(0, 10)}
</Text>
)}
</View>
</View>
</View>
) : (
<View className='bg-white mx-3 mt-3 p-3 rounded-lg'>
<View className='flex items-start gap-2'>
<Text className='text-base'>🏪</Text>
<View className='flex-1'>
<Text className='text-sm font-medium text-gray-800'>{order.selfTakeMerchantName || '自提点'}</Text>
<Text className='text-xs text-gray-500 mt-1'>{order.address || '-'}</Text>
{order.selfTakeCode && (
<View className='mt-2 px-3 py-2 rounded-lg inline-flex items-center' style={{ backgroundColor: '#fff7e6' }}>
<Text className='text-xs text-orange-600'>: {order.selfTakeCode}</Text>
</View>
)}
</View>
</View>
</View>
)}
{/* 商品列表 */}
<View className='bg-white mx-3 mt-3 rounded-lg overflow-hidden'>
<View className='p-3 border-b border-gray-100'>
<Text className='text-sm font-medium text-gray-800'> ({goodsCount})</Text>
</View>
{order.orderGoods?.map((item, idx) => (
<View
key={idx}
className='flex gap-3 p-3 border-b border-gray-50'
style={idx === (order.orderGoods?.length ?? 0) - 1 ? { borderBottom: 'none' } : undefined}
onClick={() => item.goodsId && Taro.navigateTo({ url: `/pages/shop/product-detail?id=${item.goodsId}` })}
>
<View className='w-16 h-16 rounded-md bg-gray-100 flex-shrink-0 overflow-hidden'>
{item.image ? (
<Image className='w-full h-full' src={getCompressedImageUrl(item.image)} mode='aspectFill' />
) : (
<View className='w-full h-full flex items-center justify-center'>
<Text className='text-xs text-gray-400'></Text>
</View>
)}
</View>
<View className='flex-1 flex flex-col justify-between h-16'>
<Text className='text-sm text-gray-800 line-clamp-1'>{item.goodsName || '未知商品'}</Text>
{item.spec ? (
<Text className='text-xs text-gray-500 mt-1'>: {item.spec}</Text>
) : null}
</View>
<View className='flex flex-col items-end justify-between h-16'>
<Price price={item.price || '0'} size='small' />
<Text className='text-xs text-gray-500'>x{item.totalNum || 1}</Text>
</View>
</View>
))}
</View>
{/* 金额明细 */}
<View className='bg-white mx-3 mt-3 p-3 rounded-lg'>
<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-sm text-gray-500'></Text>
<Text className='text-sm text-gray-700'>¥{order.totalPrice || '0.00'}</Text>
</View>
{order.reducePrice && Number(order.reducePrice) > 0 && (
<View className='flex justify-between'>
<Text className='text-sm text-gray-500'></Text>
<Text className='text-sm text-green-600'>-¥{order.reducePrice}</Text>
</View>
)}
<View className='flex justify-between pt-2 border-t border-gray-100'>
<Text className='text-sm font-medium text-gray-800'></Text>
<Price price={order.payPrice || '0'} size='normal' />
</View>
<View className='flex justify-between pt-2 border-t border-gray-100'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-500'>{PAY_TYPE_MAP[order.payType ?? 0] || '未知'}</Text>
</View>
</View>
</View>
{/* 订单信息 */}
<View className='bg-white mx-3 mt-3 p-3 rounded-lg mb-4'>
<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>
<View className='flex items-center gap-1'>
<Text className='text-xs text-gray-600'>{order.orderNo}</Text>
<Text
className='text-xs text-blue-500'
onClick={() => {
Taro.setClipboardData({ data: order.orderNo || '' })
Taro.showToast({ title: '已复制', icon: 'none' })
}}
>
</Text>
</View>
</View>
<View className='flex justify-between'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{DELIVERY_STATUS_MAP[order.deliveryStatus ?? 10]}</Text>
</View>
<View className='flex justify-between'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{formatTime(order.createTime)}</Text>
</View>
{order.payTime && (
<View className='flex justify-between'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{formatTime(order.payTime)}</Text>
</View>
)}
{order.deliveryTime && (
<View className='flex justify-between'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{formatTime(order.deliveryTime)}</Text>
</View>
)}
{order.expirationTime && (
<View className='flex justify-between'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{formatTime(order.expirationTime)}</Text>
</View>
)}
{order.comments && (
<View className='flex justify-start'>
<Text className='text-xs text-gray-400 mr-2'></Text>
<Text className='text-xs text-gray-600 flex-1'>{order.comments}</Text>
</View>
)}
<View className='flex justify-between'>
<Text className='text-xs text-gray-400'></Text>
<Text className='text-xs text-gray-600'>{INVOICE_STATUS_MAP[order.isInvoice ?? 0]}</Text>
</View>
</View>
</View>
</ScrollView>
{/* 底部操作按钮(根据订单阶段动态显示) */}
<View
className='bg-white border-t border-gray-100 p-3 flex flex-row gap-2'
style={{ paddingBottom: '20px' }}
>
{/* 待付款: 取消 + 支付 */}
{phase === 'unpaid' && (
<>
<Button
size='small'
className='flex-1'
style={{ borderColor: '#ddd', color: '#666' }}
onClick={handleCancelOrder}
>
</Button>
<Button
size='small'
className='flex-1'
style={{ backgroundColor: '#ee0a24' }}
loading={loading}
onClick={handlePay}
>
</Button>
</>
)}
{/* 线下付款·待确认: 取消 + 联系客服 */}
{phase === 'offline_pending' && (
<>
<Button
size='small'
className='flex-1'
style={{ borderColor: '#ddd', color: '#666' }}
onClick={handleCancelOrder}
>
</Button>
<Button
size='small'
className='flex-1'
style={{ backgroundColor: '#4b9cf5' }}
onClick={() => Taro.makePhoneCall({ phoneNumber: '400-000-0000' })}
>
</Button>
</>
)}
{/* 待发货: 退款 + 联系客服 */}
{phase === 'unshipped' && (
<>
<Button
size='small'
className='flex-1'
style={{ borderColor: '#ddd', color: '#666' }}
onClick={() => Taro.navigateTo({ url: `/pages/order/refund?orderId=${order.orderId}` })}
>
退
</Button>
<Button
size='small'
className='flex-1'
style={{ backgroundColor: '#4b9cf5' }}
onClick={() => Taro.makePhoneCall({ phoneNumber: '400-000-0000' })}
>
</Button>
</>
)}
{/* 待收货: 物流 + 确认收货 */}
{phase === 'shipped' && (
<>
<Button
size='small'
className='flex-1'
style={{ borderColor: '#4b9cf5', color: '#4b9cf5' }}
onClick={() => Taro.navigateTo({ url: `/pages/order/logistics?orderId=${order.orderId}` })}
>
</Button>
<Button
size='small'
className='flex-1'
style={{ backgroundColor: '#0e932e' }}
onClick={handleConfirmReceive}
>
</Button>
</>
)}
{/* 已完成: 售后 + 删除 */}
{phase === 'completed' && (
<>
<Button
size='small'
className='flex-1'
style={{ borderColor: '#ddd', color: '#666' }}
onClick={() => Taro.navigateTo({ url: `/pages/order/after-sale-apply?orderId=${order.orderId}&type=refund` })}
>
</Button>
<Button
size='small'
className='flex-1'
style={{ borderColor: '#ee0a24', color: '#ee0a24' }}
onClick={handleDeleteOrder}
>
</Button>
</>
)}
{/* 已取消/已退款: 删除 */}
{phase === 'cancelled' && (
<Button
size='small'
className='flex-1'
style={{ borderColor: '#ee0a24', color: '#ee0a24' }}
onClick={handleDeleteOrder}
>
</Button>
)}
</View>
</View>
)
}
export default OrderDetailPage