Files
template-10584/src/user/order/components/OrderList.tsx
赵忠林 0a6f21d182 feat(user/order): 新增订单评价功能
- 添加订单评价页面组件
- 实现订单信息和商品列表展示
- 添加评价功能相关的样式和布局
- 优化订单列表页面,增加申请退款和查看物流等功能
2025-08-23 16:22:01 +08:00

834 lines
29 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 {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image, Dialog} from '@nutui/nutui-react-taro'
import {useEffect, useState, useCallback, CSSProperties} from "react";
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro';
import {InfiniteLoading} from '@nutui/nutui-react-taro'
import dayjs from "dayjs";
import {pageShopOrder, updateShopOrder, createOrder} from "@/api/shop/shopOrder";
import {ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
import {copyText} from "@/utils/common";
import PaymentCountdown from "@/components/PaymentCountdown";
import {PaymentType} from "@/utils/payment";
import {goTo, switchTab} from "@/utils/navigation";
// 判断订单是否支付已过期
const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => {
if (!createTime) return false;
const createTimeObj = dayjs(createTime);
const expireTime = createTimeObj.add(timeoutHours, 'hour');
const now = dayjs();
return now.isAfter(expireTime);
};
const getInfiniteUlStyle = (showSearch: boolean = false): CSSProperties => ({
marginTop: showSearch ? '0' : '0', // 如果显示搜索框,增加更多的上边距
height: showSearch ? '75vh' : '84vh', // 相应调整高度
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden'
// 注意:小程序不支持 boxShadow
})
// 统一的订单状态标签配置,与后端 statusFilter 保持一致
const tabs = [
{
index: 0,
key: '全部',
title: '全部',
description: '所有订单',
statusFilter: -1 // 使用-1表示全部订单
},
{
index: 1,
key: '待付款',
title: '待付款',
description: '等待付款的订单',
statusFilter: 0 // 对应后端pay_status = false
},
{
index: 2,
key: '待发货',
title: '待发货',
description: '已付款待发货的订单',
statusFilter: 1 // 对应后端pay_status = true AND delivery_status = 10
},
{
index: 3,
key: '待收货',
title: '待收货',
description: '已发货待收货的订单',
statusFilter: 3 // 对应后端pay_status = true AND delivery_status = 20
},
{
index: 4,
key: '已完成',
title: '已完成',
description: '已完成的订单',
statusFilter: 5 // 对应后端order_status = 1
},
{
index: 5,
key: '退货/售后',
title: '退货/售后',
description: '退货/售后的订单',
statusFilter: 6 // 对应后端order_status = 6 (已退款)
}
]
// 扩展订单接口,包含商品信息
interface OrderWithGoods extends ShopOrder {
orderGoods?: ShopOrderGoods[];
}
interface OrderListProps {
onReload?: () => void;
searchParams?: ShopOrderParam;
showSearch?: boolean;
onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化
}
function OrderList(props: OrderListProps) {
const [list, setList] = useState<OrderWithGoods[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 根据传入的statusFilter设置初始tab索引
const getInitialTabIndex = () => {
if (props.searchParams?.statusFilter !== undefined) {
const tab = tabs.find(t => t.statusFilter === props.searchParams?.statusFilter);
return tab ? tab.index : 0;
}
return 0;
};
const [tapIndex, setTapIndex] = useState<number>(() => {
const initialIndex = getInitialTabIndex();
console.log('初始化tapIndex:', initialIndex, '对应statusFilter:', props.searchParams?.statusFilter);
return initialIndex;
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [cancelDialogVisible, setCancelDialogVisible] = useState(false)
const [orderToCancel, setOrderToCancel] = useState<ShopOrder | null>(null)
const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false)
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState<ShopOrder | null>(null)
// 获取订单状态文本
const getOrderStatusText = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return '已取消';
if (order.orderStatus === 4) return '退款申请中';
if (order.orderStatus === 5) return '退款被拒绝';
if (order.orderStatus === 6) return '退款成功';
if (order.orderStatus === 7) return '客户端申请退款';
// 检查支付状态 (payStatus为boolean类型false/0表示未付款true/1表示已付款)
if (!order.payStatus) return '等待买家付款';
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return '待发货';
if (order.deliveryStatus === 20) return '待收货';
if (order.deliveryStatus === 30) return '已完成';
// 最后检查订单完成状态
if (order.orderStatus === 1) return '已完成';
if (order.orderStatus === 0) return '未使用';
return '未知状态';
};
// 获取订单状态颜色
const getOrderStatusColor = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return 'text-gray-500'; // 已取消
if (order.orderStatus === 4) return 'text-orange-500'; // 退款申请中
if (order.orderStatus === 5) return 'text-red-500'; // 退款被拒绝
if (order.orderStatus === 6) return 'text-green-500'; // 退款成功
if (order.orderStatus === 7) return 'text-orange-500'; // 客户端申请退款
// 检查支付状态
if (!order.payStatus) return 'text-orange-500'; // 等待买家付款
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return 'text-blue-500'; // 待发货
if (order.deliveryStatus === 20) return 'text-purple-500'; // 待收货
if (order.deliveryStatus === 30) return 'text-green-500'; // 已收货
// 最后检查订单完成状态
if (order.orderStatus === 1) return 'text-green-600'; // 已完成
if (order.orderStatus === 0) return 'text-gray-500'; // 未使用
return 'text-gray-600'; // 默认颜色
};
// 使用后端统一的 statusFilter 进行筛选
const getOrderStatusParams = (index: string | number) => {
let params: ShopOrderParam = {};
// 添加用户ID过滤
params.userId = Taro.getStorageSync('UserId');
// 获取当前tab的statusFilter配置
const currentTab = tabs.find(tab => tab.index === Number(index));
if (currentTab && currentTab.statusFilter !== undefined) {
params.statusFilter = currentTab.statusFilter;
}
// 注意当statusFilter为undefined时不要添加到params中这样API请求就不会包含这个参数
console.log(`Tab ${index} (${currentTab?.title}) 筛选参数:`, params);
return params;
};
const reload = useCallback(async (resetPage = false, targetPage?: number) => {
setLoading(true);
setError(null); // 清除之前的错误
const currentPage = resetPage ? 1 : (targetPage || page);
const statusParams = getOrderStatusParams(tapIndex);
// 合并搜索条件tab的statusFilter优先级更高
const searchConditions: any = {
page: currentPage,
userId: statusParams.userId, // 用户ID
...props.searchParams, // 搜索关键词等其他条件
};
// statusFilter总是添加到搜索条件中包括-1表示全部
if (statusParams.statusFilter !== undefined) {
searchConditions.statusFilter = statusParams.statusFilter;
}
console.log('订单筛选条件:', {
tapIndex,
statusParams,
searchConditions,
finalStatusFilter: searchConditions.statusFilter
});
try {
const res = await pageShopOrder(searchConditions);
let newList: OrderWithGoods[];
if (res?.list && res?.list.length > 0) {
// 批量获取订单商品信息,限制并发数量
const batchSize = 3; // 限制并发数量为3
const ordersWithGoods: OrderWithGoods[] = [];
for (let i = 0; i < res.list.length; i += batchSize) {
const batch = res.list.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(async (order) => {
try {
const orderGoods = await listShopOrderGoods({orderId: order.orderId});
return {
...order,
orderGoods: orderGoods || []
};
} catch (error) {
console.error('获取订单商品失败:', error);
return {
...order,
orderGoods: []
};
}
})
);
ordersWithGoods.push(...batchResults);
}
// 使用函数式更新避免依赖 list
setList(prevList => {
const newList = resetPage ? ordersWithGoods : (prevList || []).concat(ordersWithGoods);
return newList;
});
// 正确判断是否还有更多数据
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
setHasMore(hasMoreData);
} else {
setList(prevList => resetPage ? [] : prevList);
setHasMore(false);
}
setPage(currentPage);
setLoading(false);
} catch (error) {
console.error('加载订单失败:', error);
setLoading(false);
setError('加载订单失败,请重试');
// 添加错误提示
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
});
}
}, [tapIndex, page, props.searchParams]); // 移除 list 依赖
const reloadMore = useCallback(async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
setPage(nextPage);
await reload(false, nextPage);
}, [loading, hasMore, page, reload]);
// 确认收货 - 显示确认对话框
const confirmReceive = (order: ShopOrder) => {
setOrderToConfirmReceive(order);
setConfirmReceiveDialogVisible(true);
};
// 确认收货 - 执行收货操作
const handleConfirmReceive = async () => {
if (!orderToConfirmReceive) return;
try {
setConfirmReceiveDialogVisible(false);
await updateShopOrder({
...orderToConfirmReceive,
deliveryStatus: 30, // 已收货
orderStatus: 1 // 已完成
});
Taro.showToast({
title: '确认收货成功',
icon: 'success'
});
await reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新
// 清空状态
setOrderToConfirmReceive(null);
} catch (error) {
console.error('确认收货失败:', error);
Taro.showToast({
title: '确认收货失败',
icon: 'none'
});
// 重新显示对话框
setConfirmReceiveDialogVisible(true);
}
};
// 取消确认收货对话框
const handleCancelReceiveDialog = () => {
setConfirmReceiveDialogVisible(false);
setOrderToConfirmReceive(null);
};
// 申请退款 (待发货状态)
const applyRefund = (order: ShopOrder) => {
// 跳转到退款申请页面
Taro.navigateTo({
url: `/user/order/refund/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
});
};
// 查看物流 (待收货状态)
const viewLogistics = (order: ShopOrder) => {
// 跳转到物流查询页面
Taro.navigateTo({
url: `/user/order/logistics/index?orderId=${order.orderId}&orderNo=${order.orderNo}&expressNo=${order.transactionId || ''}&expressCompany=SF`
});
};
// 再次购买 (已完成状态)
const buyAgain = (order: ShopOrder) => {
console.log('再次购买:', order);
goTo(`/shop/orderConfirm/index?goodsId=${order.orderGoods[0].goodsId}`)
// Taro.showToast({
// title: '再次购买功能开发中',
// icon: 'none'
// });
};
// 评价商品 (已完成状态)
const evaluateGoods = (order: ShopOrder) => {
// 跳转到评价页面
Taro.navigateTo({
url: `/user/order/evaluate/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
});
};
// 查看进度 (退款/售后状态)
const viewProgress = (order: ShopOrder) => {
// 根据订单状态确定售后类型
let afterSaleType = 'refund' // 默认退款
if (order.orderStatus === 4) {
afterSaleType = 'refund' // 退款申请中
} else if (order.orderStatus === 7) {
afterSaleType = 'return' // 退货申请中
}
// 跳转到售后进度页面
Taro.navigateTo({
url: `/user/order/progress/index?orderId=${order.orderId}&orderNo=${order.orderNo}&type=${afterSaleType}`
});
};
// 撤销申请 (退款/售后状态)
const cancelApplication = (order: ShopOrder) => {
console.log('撤销申请:', order);
Taro.showToast({
title: '撤销申请功能开发中',
icon: 'none'
});
};
// 取消订单
const cancelOrder = (order: ShopOrder) => {
setOrderToCancel(order);
setCancelDialogVisible(true);
};
// 确认取消订单
const handleConfirmCancel = async () => {
if (!orderToCancel) return;
try {
setCancelDialogVisible(false);
// 更新订单状态为已取消,而不是删除订单
await updateShopOrder({
...orderToCancel,
orderStatus: 2 // 已取消
});
Taro.showToast({
title: '订单已取消',
icon: 'success'
});
void reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新
} catch (error) {
console.error('取消订单失败:', error);
Taro.showToast({
title: '取消订单失败',
icon: 'error'
});
} finally {
setOrderToCancel(null);
}
};
// 取消对话框的取消操作
const handleCancelDialog = () => {
setCancelDialogVisible(false);
setOrderToCancel(null);
};
// 立即支付
const payOrder = async (order: ShopOrder) => {
try {
if (!order.orderId || !order.orderNo) {
Taro.showToast({
title: '订单信息错误',
icon: 'error'
});
return;
}
// 检查订单是否已过期
if (order.createTime && isPaymentExpired(order.createTime)) {
Taro.showToast({
title: '订单已过期,无法支付',
icon: 'error'
});
return;
}
// 检查订单状态
if (order.payStatus) {
Taro.showToast({
title: '订单已支付',
icon: 'none'
});
return;
}
if (order.orderStatus === 2) {
Taro.showToast({
title: '订单已取消,无法支付',
icon: 'error'
});
return;
}
Taro.showLoading({title: '发起支付...'});
// 构建商品数据
const goodsItems = order.orderGoods?.map(goods => ({
goodsId: goods.goodsId,
quantity: goods.totalNum || 1
})) || [];
// 对于已存在的订单,我们需要重新发起支付
// 构建支付请求数据,包含完整的商品信息
const paymentData = {
orderId: order.orderId,
orderNo: order.orderNo,
goodsItems: goodsItems,
addressId: order.addressId,
payType: PaymentType.WECHAT
};
console.log('重新支付数据:', paymentData);
// 直接调用createOrder API进行重新支付
const result = await createOrder(paymentData as any);
if (!result) {
throw new Error('支付发起失败');
}
// 验证微信支付必要参数
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
throw new Error('微信支付参数不完整');
}
// 调用微信支付
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256',
paySign: result.paySign,
});
// 支付成功
Taro.showToast({
title: '支付成功',
icon: 'success'
});
// 重新加载订单列表
void reload(true);
props.onReload?.();
// 跳转到订单页面
setTimeout(() => {
Taro.navigateTo({url: '/user/order/order'});
}, 2000);
} catch (error: any) {
console.error('支付失败:', error);
let errorMessage = '支付失败,请重试';
if (error.message) {
if (error.message.includes('cancel')) {
errorMessage = '用户取消支付';
} else if (error.message.includes('余额不足')) {
errorMessage = '账户余额不足';
} else {
errorMessage = error.message;
}
}
Taro.showToast({
title: errorMessage,
icon: 'error'
});
} finally {
Taro.hideLoading();
}
};
useEffect(() => {
void reload(true); // 首次加载或tab切换时重置页码
}, [tapIndex]); // 只监听tapIndex变化避免reload依赖循环
// 监听外部statusFilter变化同步更新tab索引
useEffect(() => {
// 获取当前的statusFilter如果未定义则默认为-1全部
const currentStatusFilter = props.searchParams?.statusFilter !== undefined
? props.searchParams.statusFilter
: -1;
const tab = tabs.find(t => t.statusFilter === currentStatusFilter);
const targetTabIndex = tab ? tab.index : 0;
console.log('外部statusFilter变化:', {
statusFilter: currentStatusFilter,
originalStatusFilter: props.searchParams?.statusFilter,
currentTapIndex: tapIndex,
targetTabIndex,
shouldUpdate: targetTabIndex !== tapIndex
});
if (targetTabIndex !== tapIndex) {
setTapIndex(targetTabIndex);
// 不需要调用reload因为tapIndex变化会触发reload
}
}, [props.searchParams?.statusFilter, tapIndex]); // 监听statusFilter变化
return (
<>
<Tabs
align={'left'}
className={'fixed left-0'}
style={{
zIndex: 998,
borderBottom: '1px solid #e5e5e5'
}}
tabStyle={{
backgroundColor: '#ffffff'
// 注意:小程序不支持 boxShadow
}}
value={tapIndex}
onChange={(paneKey) => {
console.log('Tab切换:', paneKey, '类型:', typeof paneKey);
const newTapIndex = Number(paneKey);
setTapIndex(newTapIndex);
// 通知父组件更新 searchParams.statusFilter
const currentTab = tabs.find(tab => tab.index === newTapIndex);
if (currentTab && props.onSearchParamsChange) {
const newSearchParams = {
...props.searchParams,
statusFilter: currentTab.statusFilter
};
console.log('通知父组件更新searchParams:', newSearchParams);
props.onSearchParamsChange(newSearchParams);
}
}}
>
{
tabs?.map((item, _) => {
return (
<TabPane
key={item.index}
title={loading && tapIndex === item.index ? `${item.title}...` : item.title}
></TabPane>
)
})
}
</Tabs>
<View style={getInfiniteUlStyle(props.showSearch)} id="scroll">
{error ? (
<View className="flex flex-col items-center justify-center h-64">
<View className="text-gray-500 mb-4">{error}</View>
<Button
size="small"
type="primary"
onClick={() => reload(true)}
>
</Button>
</View>
) : (
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
}}
onScrollToUpper={() => {
}}
loadingText={
<>
</>
}
loadMoreText={
list.length === 0 ? (
<Empty style={{backgroundColor: 'transparent'}} description="您还没有订单哦"/>
) : (
<View className={'h-24'}>
</View>
)
}
>
{/* 订单列表 */}
{list.length > 0 && list
?.filter((item) => {
// 如果是待付款标签页tapIndex === 1过滤掉支付已过期的订单
if (tapIndex === 1 && !item.payStatus && item.orderStatus !== 2 && item.createTime) {
return !isPaymentExpired(item.createTime);
}
return true;
})
?.map((item, index) => {
return (
<Cell key={index} style={{padding: '16px'}}
onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
<Space direction={'vertical'} className={'w-full flex flex-col'}>
<View className={'order-no flex justify-between'}>
<View className={'flex items-center'}>
<Text className={'text-gray-600 font-bold text-sm'}
onClick={(e) => {
e.stopPropagation();
copyText(`${item.orderNo}`)
}}>{item.orderNo}</Text>
</View>
{/* 右侧显示合并的状态和倒计时 */}
<View className={`${getOrderStatusColor(item)} font-medium`}>
{!item.payStatus && item.orderStatus !== 2 ? (
<PaymentCountdown
createTime={item.createTime}
payStatus={item.payStatus}
realTime={false}
showSeconds={false}
mode={'badge'}
/>
) : (
getOrderStatusText(item)
)}
</View>
</View>
<View
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</View>
{/* 商品信息 */}
<View className={'goods-info'}>
{item.orderGoods && item.orderGoods.length > 0 ? (
item.orderGoods.map((goods, goodsIndex) => (
<View key={goodsIndex} className={'flex items-center mb-2'}>
<Image
src={goods.image || '/default-goods.png'}
width="50"
height="50"
lazyLoad={false}
className={'rounded'}
/>
<View className={'ml-2 flex flex-col flex-1'}>
<Text className={'text-sm font-bold'}>{goods.goodsName}</Text>
{goods.spec && <Text className={'text-gray-500 text-xs'}>{goods.spec}</Text>}
<Text className={'text-gray-500 text-xs'}>{goods.totalNum}</Text>
</View>
<Text className={'text-sm'}>{goods.price}</Text>
</View>
))
) : (
<View className={'flex items-center'}>
<Avatar
src='/default-goods.png'
size={'50'}
shape={'square'}
/>
<View className={'ml-2'}>
<Text className={'text-sm'}>{item.title || '订单商品'}</Text>
<Text className={'text-gray-400 text-xs'}>{item.totalNum}</Text>
</View>
</View>
)}
</View>
<Text className={'w-full text-right'}>{item.payPrice}</Text>
{/* 操作按钮 */}
<Space className={'btn flex justify-end'}>
{/* 待付款状态:显示取消订单和立即支付 */}
{(!item.payStatus) && item.orderStatus !== 2 && (
<Space>
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
void cancelOrder(item);
}}></Button>
<Button size={'small'} type="primary" onClick={(e) => {
e.stopPropagation();
void payOrder(item);
}}></Button>
</Space>
)}
{/* 待发货状态:显示申请退款 */}
{item.payStatus && item.deliveryStatus === 10 && item.orderStatus !== 2 && item.orderStatus !== 4 && (
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
applyRefund(item);
}}>退</Button>
)}
{/* 待收货状态:显示查看物流和确认收货 */}
{item.deliveryStatus === 20 && item.orderStatus !== 2 && (
<Space>
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
viewLogistics(item);
}}></Button>
<Button size={'small'} type="primary" onClick={(e) => {
e.stopPropagation();
confirmReceive(item);
}}></Button>
</Space>
)}
{/* 已完成状态:显示再次购买、评价商品、申请退款 */}
{item.orderStatus === 1 && (
<Space>
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
buyAgain(item);
}}></Button>
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
evaluateGoods(item);
}}></Button>
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
applyRefund(item);
}}>退</Button>
</Space>
)}
{/* 退款/售后状态:显示查看进度和撤销申请 */}
{(item.orderStatus === 4 || item.orderStatus === 7) && (
<Space>
<Button size={'small'} onClick={(e) => {
e.stopPropagation();
viewProgress(item);
}}></Button>
</Space>
)}
{/* 退款成功状态:显示再次购买 */}
{item.orderStatus === 6 && (
<Button size={'small'} type="primary" onClick={(e) => {
e.stopPropagation();
buyAgain(item);
}}></Button>
)}
</Space>
</Space>
</Cell>
)
})}
</InfiniteLoading>
)}
</View>
{/* 取消订单确认对话框 */}
<Dialog
title="确认取消"
visible={cancelDialogVisible}
confirmText="确认取消"
cancelText="我再想想"
onConfirm={handleConfirmCancel}
onCancel={handleCancelDialog}
>
</Dialog>
{/* 确认收货确认对话框 */}
<Dialog
title="确认收货"
visible={confirmReceiveDialogVisible}
confirmText="确认收货"
cancelText="我再想想"
onConfirm={handleConfirmReceive}
onCancel={handleCancelReceiveDialog}
>
</Dialog>
</>
)
}
export default OrderList