Files
xinlong-shop-taro/src_bak/pages/order/detail.tsx
赵忠林 1fa58040f3 feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
2026-07-01 12:11:56 +08:00

520 lines
19 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'
definePageConfig({
navigationBarTitleText: '订单详情',
})
// 支付方式映射
const PAY_TYPE_MAP: Record<number, string> = {
0: '货到付款',
1: '微信支付',
2: '会员卡支付',
3: '支付宝',
15: '积分支付',
}
// 发票状态映射
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 } = order
// 退款/取消相关状态
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) {
return { bgColor: '#ff7d00', 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' | 'unshipped' | 'shipped' | 'completed' | 'cancelled' | 'refund'
const getOrderPhase = (order: ShopOrder): OrderPhase => {
const { payStatus, deliveryStatus, orderStatus } = order
// 退款/取消
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 (!payStatus) 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={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 === '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