|
@@ -0,0 +1,319 @@
|
|
|
|
|
+import { View, ScrollView, Text } from '@tarojs/components'
|
|
|
|
|
+import { useInfiniteQuery } from '@tanstack/react-query'
|
|
|
|
|
+import { useState } from 'react'
|
|
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
|
|
+import { orderClient } from '@/api'
|
|
|
|
|
+import { InferResponseType } from 'hono'
|
|
|
|
|
+import { Navbar } from '@/components/ui/navbar'
|
|
|
|
|
+import { Card } from '@/components/ui/card'
|
|
|
|
|
+import { Button } from '@/components/ui/button'
|
|
|
|
|
+import { useAuth } from '@/utils/auth'
|
|
|
|
|
+
|
|
|
|
|
+type OrderResponse = InferResponseType<typeof orderClient.$get, 200>
|
|
|
|
|
+type Order = OrderResponse['data'][0]
|
|
|
|
|
+
|
|
|
|
|
+// 订单状态映射
|
|
|
|
|
+const orderStatusMap = {
|
|
|
|
|
+ 0: { text: '待发货', color: 'text-orange-500' },
|
|
|
|
|
+ 1: { text: '已发货', color: 'text-blue-500' },
|
|
|
|
|
+ 2: { text: '已完成', color: 'text-green-500' },
|
|
|
|
|
+ 3: { text: '已退货', color: 'text-red-500' }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 支付状态映射
|
|
|
|
|
+const payStatusMap = {
|
|
|
|
|
+ 0: { text: '未支付', color: 'text-red-500' },
|
|
|
|
|
+ 1: { text: '支付中', color: 'text-yellow-500' },
|
|
|
|
|
+ 2: { text: '已支付', color: 'text-green-500' },
|
|
|
|
|
+ 3: { text: '已退款', color: 'text-gray-500' },
|
|
|
|
|
+ 4: { text: '支付失败', color: 'text-red-500' },
|
|
|
|
|
+ 5: { text: '已关闭', color: 'text-gray-500' }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function OrderListPage() {
|
|
|
|
|
+ const { user } = useAuth()
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState('all')
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ data,
|
|
|
|
|
+ isLoading,
|
|
|
|
|
+ isFetchingNextPage,
|
|
|
|
|
+ fetchNextPage,
|
|
|
|
|
+ hasNextPage,
|
|
|
|
|
+ refetch
|
|
|
|
|
+ } = useInfiniteQuery({
|
|
|
|
|
+ queryKey: ['orders', user?.id, activeTab],
|
|
|
|
|
+ queryFn: async ({ pageParam = 1 }) => {
|
|
|
|
|
+ let filters: any = { userId: user?.id }
|
|
|
|
|
+
|
|
|
|
|
+ // 根据标签筛选
|
|
|
|
|
+ switch (activeTab) {
|
|
|
|
|
+ case 'unpaid':
|
|
|
|
|
+ filters.payState = 0
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'unshipped':
|
|
|
|
|
+ filters.payState = 2
|
|
|
|
|
+ filters.state = 0
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'shipped':
|
|
|
|
|
+ filters.state = 1
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'completed':
|
|
|
|
|
+ filters.state = 2
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const response = await orderClient.$get({
|
|
|
|
|
+ query: {
|
|
|
|
|
+ page: pageParam,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ filters: JSON.stringify(filters),
|
|
|
|
|
+ order: JSON.stringify({ createdAt: 'DESC' })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ if (response.status !== 200) {
|
|
|
|
|
+ throw new Error('获取订单失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ return response.json()
|
|
|
|
|
+ },
|
|
|
|
|
+ enabled: !!user?.id,
|
|
|
|
|
+ getNextPageParam: (lastPage) => {
|
|
|
|
|
+ const { pagination } = lastPage
|
|
|
|
|
+ const totalPages = Math.ceil(pagination.total / pagination.pageSize)
|
|
|
|
|
+ return pagination.current < totalPages ? pagination.current + 1 : undefined
|
|
|
|
|
+ },
|
|
|
|
|
+ staleTime: 5 * 60 * 1000,
|
|
|
|
|
+ initialPageParam: 1,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 合并所有分页数据
|
|
|
|
|
+ const allOrders = data?.pages.flatMap(page => page.data) || []
|
|
|
|
|
+
|
|
|
|
|
+ // 触底加载更多
|
|
|
|
|
+ const handleScrollToLower = () => {
|
|
|
|
|
+ if (hasNextPage && !isFetchingNextPage) {
|
|
|
|
|
+ fetchNextPage()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 下拉刷新
|
|
|
|
|
+ const onPullDownRefresh = () => {
|
|
|
|
|
+ refetch().finally(() => {
|
|
|
|
|
+ Taro.stopPullDownRefresh()
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 查看订单详情
|
|
|
|
|
+ const handleOrderDetail = (order: Order) => {
|
|
|
|
|
+ Taro.navigateTo({
|
|
|
|
|
+ url: `/pages/order-detail/index?id=${order.id}`
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析商品详情
|
|
|
|
|
+ const parseGoodsDetail = (goodsDetail: string | null) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return goodsDetail ? JSON.parse(goodsDetail) : []
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算订单商品数量
|
|
|
|
|
+ const getOrderItemCount = (order: Order) => {
|
|
|
|
|
+ const goods = parseGoodsDetail(order.goodsDetail)
|
|
|
|
|
+ return goods.reduce((sum: number, item: any) => sum + (item.num || 0), 0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const tabs = [
|
|
|
|
|
+ { key: 'all', label: '全部' },
|
|
|
|
|
+ { key: 'unpaid', label: '待付款' },
|
|
|
|
|
+ { key: 'unshipped', label: '待发货' },
|
|
|
|
|
+ { key: 'shipped', label: '待收货' },
|
|
|
|
|
+ { key: 'completed', label: '已完成' }
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View className="min-h-screen bg-gray-50">
|
|
|
|
|
+ <Navbar
|
|
|
|
|
+ title="我的订单"
|
|
|
|
|
+ leftIcon="i-heroicons-chevron-left-20-solid"
|
|
|
|
|
+ onClickLeft={() => Taro.navigateBack()}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <ScrollView
|
|
|
|
|
+ className="h-screen pt-12"
|
|
|
|
|
+ scrollY
|
|
|
|
|
+ onScrollToLower={handleScrollToLower}
|
|
|
|
|
+ refresherEnabled
|
|
|
|
|
+ refresherTriggered={false}
|
|
|
|
|
+ onRefresherRefresh={onPullDownRefresh}
|
|
|
|
|
+ >
|
|
|
|
|
+ {/* 标签栏 */}
|
|
|
|
|
+ <View className="bg-white mb-4">
|
|
|
|
|
+ <View className="flex">
|
|
|
|
|
+ {tabs.map((tab) => (
|
|
|
|
|
+ <View
|
|
|
|
|
+ key={tab.key}
|
|
|
|
|
+ className={`flex-1 py-3 text-center ${
|
|
|
|
|
+ activeTab === tab.key
|
|
|
|
|
+ ? 'text-blue-500 border-b-2 border-blue-500'
|
|
|
|
|
+ : 'text-gray-600'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ onClick={() => setActiveTab(tab.key)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {tab.label}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ <View className="px-4">
|
|
|
|
|
+ {isLoading ? (
|
|
|
|
|
+ <View className="flex justify-center py-10">
|
|
|
|
|
+ <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {allOrders.map((order) => {
|
|
|
|
|
+ const goods = parseGoodsDetail(order.goodsDetail)
|
|
|
|
|
+ const totalQuantity = getOrderItemCount(order)
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Card key={order.id} className="mb-4">
|
|
|
|
|
+ <View className="p-4">
|
|
|
|
|
+ {/* 订单头部 */}
|
|
|
|
|
+ <View className="flex justify-between items-center mb-3 pb-3 border-b border-gray-100">
|
|
|
|
|
+ <Text className="text-sm text-gray-600">
|
|
|
|
|
+ 订单号: {order.orderNo}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ <Text className={`text-sm font-medium ${payStatusMap[order.payState as keyof typeof payStatusMap].color}`}>
|
|
|
|
|
+ {payStatusMap[order.payState as keyof typeof payStatusMap].text}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 商品列表 */}
|
|
|
|
|
+ <View className="mb-3">
|
|
|
|
|
+ {goods.slice(0, 3).map((item: any, index: number) => (
|
|
|
|
|
+ <View key={index} className="flex items-center py-2">
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={item.image || ''}
|
|
|
|
|
+ className="w-16 h-16 rounded-lg mr-3"
|
|
|
|
|
+ mode="aspectFill"
|
|
|
|
|
+ />
|
|
|
|
|
+ <View className="flex-1">
|
|
|
|
|
+ <Text className="text-sm font-medium line-clamp-2">
|
|
|
|
|
+ {item.name}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ <Text className="text-sm text-gray-500">
|
|
|
|
|
+ ¥{item.price.toFixed(2)} × {item.num}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ))}
|
|
|
|
|
+
|
|
|
|
|
+ {goods.length > 3 && (
|
|
|
|
|
+ <Text className="text-sm text-gray-500 text-center mt-2">
|
|
|
|
|
+ 共 {totalQuantity} 件商品
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 订单信息 */}
|
|
|
|
|
+ <View className="border-t border-gray-100 pt-3">
|
|
|
|
|
+ <View className="flex justify-between items-center mb-3">
|
|
|
|
|
+ <Text className="text-sm text-gray-600">
|
|
|
|
|
+ 实付款: ¥{order.payAmount.toFixed(2)}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ <Text className={`text-sm font-medium ${orderStatusMap[order.state as keyof typeof orderStatusMap].color}`}>
|
|
|
|
|
+ {orderStatusMap[order.state as keyof typeof orderStatusMap].text}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 操作按钮 */}
|
|
|
|
|
+ <View className="flex justify-end space-x-2">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ onClick={() => handleOrderDetail(order)}
|
|
|
|
|
+ >
|
|
|
|
|
+ 查看详情
|
|
|
|
|
+ </Button>
|
|
|
|
|
+
|
|
|
|
|
+ {order.payState === 0 && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ variant="primary"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ // 跳转到支付页面
|
|
|
|
|
+ Taro.navigateTo({
|
|
|
|
|
+ url: `/pages/payment/index?orderId=${order.id}`
|
|
|
|
|
+ })
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ 去支付
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {order.state === 2 && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ // 申请退款
|
|
|
|
|
+ Taro.showModal({
|
|
|
|
|
+ title: '确认收货',
|
|
|
|
|
+ content: '确认已收到商品吗?',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ // 这里可以调用确认收货的API
|
|
|
|
|
+ Taro.showToast({
|
|
|
|
|
+ title: '已确认收货',
|
|
|
|
|
+ icon: 'success'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ 确认收货
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+
|
|
|
|
|
+ {isFetchingNextPage && (
|
|
|
|
|
+ <View className="flex justify-center py-4">
|
|
|
|
|
+ <View className="i-heroicons-arrow-path-20-solid animate-spin w-6 h-6 text-blue-500" />
|
|
|
|
|
+ <Text className="ml-2 text-sm text-gray-500">加载更多...</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {!hasNextPage && allOrders.length > 0 && (
|
|
|
|
|
+ <View className="text-center py-4 text-sm text-gray-400">
|
|
|
|
|
+ 没有更多了
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {!isLoading && allOrders.length === 0 && (
|
|
|
|
|
+ <View className="flex flex-col items-center py-20">
|
|
|
|
|
+ <View className="i-heroicons-clipboard-document-list-20-solid w-16 h-16 text-gray-300 mb-4" />
|
|
|
|
|
+ <Text className="text-gray-500 mb-4">暂无订单</Text>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={() => Taro.navigateTo({ url: '/pages/goods-list/index' })}
|
|
|
|
|
+ >
|
|
|
|
|
+ 去购物
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </ScrollView>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|