Quellcode durchsuchen

✨ feat(mini): 新增电商功能模块

- 添加商品列表、商品详情、购物车、订单管理等功能页面
- 实现地址管理功能,支持添加/编辑/删除/设置默认地址
- 创建购物车状态管理工具,支持本地存储
- 集成订单流程:创建订单、支付、确认收货、退款等
- 更新应用配置,注册所有新的电商相关页面
yourname vor 3 Monaten
Ursprung
Commit
b8cab66320

+ 32 - 1
.roo/commands/mini-shadui-page.md

@@ -661,7 +661,37 @@ const mutation = useMutation({
 })
 ```
 
-### 3. 分页查询
+### 3. 删除操作
+```typescript
+const queryClient = useQueryClient()
+
+const mutation = useMutation({
+  mutationFn: async (id: number) => {
+    const response = await deliveryAddressClient[':id'].$delete({
+      param: { id }
+    })
+    if (response.status !== 204) {
+      throw new Error('删除地址失败')
+    }
+    return response.json()
+  },
+  onSuccess: () => {
+    queryClient.invalidateQueries({ queryKey: ['delivery-addresses'] })
+    Taro.showToast({
+      title: '删除成功',
+      icon: 'success'
+    })
+  },
+  onError: (error) => {
+    Taro.showToast({
+      title: error.message || '删除失败',
+      icon: 'none'
+    })
+  }
+})
+```
+
+### 4. 分页查询
 #### 标准分页(useQuery)
 ```typescript
 const useUserList = (page: number, pageSize: number = 10) => {
@@ -770,6 +800,7 @@ export default function InfiniteGoodsList() {
       return pagination.current < totalPages ? pagination.current + 1 : undefined
     },
     staleTime: 5 * 60 * 1000,
+    initialPageParam: 1,
   })
 
   // 合并所有分页数据

+ 11 - 1
mini/src/app.config.ts

@@ -5,7 +5,17 @@ export default defineAppConfig({
     'pages/profile/index',
     'pages/login/index',
     'pages/login/wechat-login',
-    'pages/register/index'
+    'pages/register/index',
+    // 电商相关页面
+    'pages/goods-list/index',
+    'pages/goods-detail/index',
+    'pages/cart/index',
+    'pages/order-list/index',
+    'pages/order-detail/index',
+    'pages/order-submit/index',
+    'pages/address-manage/index',
+    'pages/address-edit/index',
+    'pages/payment/index'
   ],
   window: {
     backgroundTextStyle: 'light',

+ 6 - 0
mini/src/pages/address-manage/index.config.ts

@@ -0,0 +1,6 @@
+export default definePageConfig({
+  navigationBarTitleText: '收货地址',
+  enablePullDownRefresh: false,
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})

+ 239 - 0
mini/src/pages/address-manage/index.tsx

@@ -0,0 +1,239 @@
+import { View, ScrollView, Text } from '@tarojs/components'
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { useState } from 'react'
+import Taro from '@tarojs/taro'
+import { deliveryAddressClient } from '@/api'
+import { InferResponseType, InferRequestType } 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 AddressResponse = InferResponseType<typeof deliveryAddressClient.$get, 200>
+type Address = AddressResponse['data'][0]
+type CreateAddressRequest = InferRequestType<typeof deliveryAddressClient.$post>['json']
+type UpdateAddressRequest = InferRequestType<typeof deliveryAddressClient[':id']['$put']>['json']
+
+export default function AddressManagePage() {
+  const { user } = useAuth()
+  const queryClient = useQueryClient()
+  const [selectedAddressId, setSelectedAddressId] = useState<number | null>(null)
+
+  // 获取地址列表
+  const { data: addresses, isLoading } = useQuery({
+    queryKey: ['delivery-addresses', user?.id],
+    queryFn: async () => {
+      const response = await deliveryAddressClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ userId: user?.id })
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取地址失败')
+      }
+      return response.json()
+    },
+    enabled: !!user?.id,
+    staleTime: 5 * 60 * 1000,
+  })
+
+  // 删除地址
+  const deleteAddressMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const response = await deliveryAddressClient[':id'].$delete({
+        param: { id }
+      })
+      if (response.status !== 204) {
+        throw new Error('删除地址失败')
+      }
+      return response.json()
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['delivery-addresses'] })
+      Taro.showToast({
+        title: '删除成功',
+        icon: 'success'
+      })
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '删除失败',
+        icon: 'none'
+      })
+    }
+  })
+
+  // 设置默认地址
+  const setDefaultMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const response = await deliveryAddressClient[':id']['$put']({
+        param: { id },
+        json: { isDefault: 1 }
+      })
+      if (response.status !== 200) {
+        throw new Error('设置默认地址失败')
+      }
+      return response.json()
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['delivery-addresses'] })
+      Taro.showToast({
+        title: '设置成功',
+        icon: 'success'
+      })
+    }
+  })
+
+  // 跳转到添加地址页面
+  const handleAddAddress = () => {
+    Taro.navigateTo({
+      url: '/pages/address-edit/index'
+    })
+  }
+
+  // 跳转到编辑地址页面
+  const handleEditAddress = (address: Address) => {
+    Taro.navigateTo({
+      url: `/pages/address-edit/index?id=${address.id}`
+    })
+  }
+
+  // 选择地址并返回
+  const handleSelectAddress = (address: Address) => {
+    // 检查是否是选择模式
+    const pages = Taro.getCurrentPages()
+    const prevPage = pages[pages.length - 2]
+    
+    if (prevPage && prevPage.route === 'pages/order-submit/index') {
+      // 返回订单页面
+      prevPage.setData({
+        selectedAddress: address
+      })
+      Taro.navigateBack()
+    }
+  }
+
+  // 删除地址
+  const handleDeleteAddress = (id: number) => {
+    Taro.showModal({
+      title: '删除地址',
+      content: '确定要删除这个收货地址吗?',
+      success: (res) => {
+        if (res.confirm) {
+          deleteAddressMutation.mutate(id)
+        }
+      }
+    })
+  }
+
+  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 pb-16">
+        <View className="px-4 py-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>
+          ) : (
+            <>
+              {addresses?.data?.map((address) => (
+                <Card key={address.id} className="mb-4">
+                  <View className="p-4">
+                    <View className="flex items-start justify-between mb-2">
+                      <View className="flex-1">
+                        <View className="flex items-center mb-1">
+                          <Text className="font-medium text-gray-900 mr-2">
+                            {address.name}
+                          </Text>
+                          <Text className="text-sm text-gray-600">
+                            {address.phone}
+                          </Text>
+                          {address.isDefault === 1 && (
+                            <Text className="ml-2 px-2 py-1 bg-red-100 text-red-600 text-xs rounded">
+                              默认
+                            </Text>
+                          )}
+                        </View>
+                        
+                        <Text className="text-sm text-gray-700">
+                          {address.province?.name || ''} 
+                          {address.city?.name || ''} 
+                          {address.district?.name || ''} 
+                          {address.town?.name || ''} 
+                          {address.address}
+                        </Text>
+                      </View>
+                    </View>
+                    
+                    <View className="flex items-center justify-between pt-3 border-t border-gray-100">
+                      <View className="flex space-x-4">
+                        {address.isDefault !== 1 && (
+                          <Button
+                            size="sm"
+                            variant="ghost"
+                            onClick={() => setDefaultMutation.mutate(address.id)}
+                          >
+                            设为默认
+                          </Button>
+                        )}
+                        
+                        <Button
+                          size="sm"
+                          variant="ghost"
+                          onClick={() => handleEditAddress(address)}
+                        >
+                          编辑
+                        </Button>
+                        
+                        <Button
+                          size="sm"
+                          variant="ghost"
+                          className="text-red-500"
+                          onClick={() => handleDeleteAddress(address.id)}
+                        >
+                          删除
+                        </Button>
+                      </View>
+                      
+                      <Button
+                        size="sm"
+                        onClick={() => handleSelectAddress(address)}
+                      >
+                        选择
+                      </Button>
+                    </View>
+                  </View>
+                </Card>
+              ))}
+              
+              {addresses?.data?.length === 0 && (
+                <View className="flex flex-col items-center py-20">
+                  <View className="i-heroicons-map-pin-20-solid w-16 h-16 text-gray-300 mb-4" />
+                  <Text className="text-gray-500 mb-4">暂无收货地址</Text>
+                </View>
+              )}
+            </>
+          )}
+        </View>
+      </ScrollView>
+
+      {/* 底部添加按钮 */}
+      <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3">
+        <Button 
+          className="w-full"
+          onClick={handleAddAddress}
+        >
+          添加新地址
+        </Button>
+      </View>
+    </View>
+  )
+}

+ 6 - 0
mini/src/pages/cart/index.config.ts

@@ -0,0 +1,6 @@
+export default definePageConfig({
+  navigationBarTitleText: '购物车',
+  enablePullDownRefresh: false,
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})

+ 211 - 0
mini/src/pages/cart/index.tsx

@@ -0,0 +1,211 @@
+import { View, ScrollView, Text } from '@tarojs/components'
+import { useState } from 'react'
+import Taro from '@tarojs/taro'
+import { Navbar } from '@/components/ui/navbar'
+import { Card } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Image } from '@/components/ui/image'
+import { useCart } from '@/utils/cart'
+
+export default function CartPage() {
+  const { cart, updateQuantity, removeFromCart, clearCart } = useCart()
+  const [selectedItems, setSelectedItems] = useState<number[]>([])
+
+  // 全选/取消全选
+  const toggleSelectAll = () => {
+    if (selectedItems.length === cart.items.length) {
+      setSelectedItems([])
+    } else {
+      setSelectedItems(cart.items.map(item => item.id))
+    }
+  }
+
+  // 选择/取消选择商品
+  const toggleSelectItem = (id: number) => {
+    setSelectedItems(prev =>
+      prev.includes(id)
+        ? prev.filter(itemId => itemId !== id)
+        : [...prev, id]
+    )
+  }
+
+  // 计算选中商品的总价
+  const selectedItemsTotal = cart.items
+    .filter(item => selectedItems.includes(item.id))
+    .reduce((sum, item) => sum + (item.price * item.quantity), 0)
+
+  // 去结算
+  const handleCheckout = () => {
+    if (selectedItems.length === 0) {
+      Taro.showToast({
+        title: '请选择商品',
+        icon: 'none'
+      })
+      return
+    }
+
+    const checkoutItems = cart.items.filter(item => selectedItems.includes(item.id))
+    
+    // 存储选中的商品信息
+    Taro.setStorageSync('checkoutItems', {
+      items: checkoutItems,
+      totalAmount: selectedItemsTotal
+    })
+    
+    Taro.navigateTo({
+      url: '/pages/order-submit/index'
+    })
+  }
+
+  return (
+    <View className="min-h-screen bg-gray-50">
+      <Navbar
+        title="购物车"
+        leftIcon="i-heroicons-chevron-left-20-solid"
+        onClickLeft={() => Taro.navigateBack()}
+        rightIcon="i-heroicons-trash-20-solid"
+        onClickRight={() => {
+          Taro.showModal({
+            title: '清空购物车',
+            content: '确定要清空购物车吗?',
+            success: (res) => {
+              if (res.confirm) {
+                clearCart()
+                setSelectedItems([])
+              }
+            }
+          })
+        }}
+      />
+      
+      <ScrollView className="h-screen pt-12 pb-20">
+        <View className="px-4 py-4">
+          {cart.items.length === 0 ? (
+            <View className="flex flex-col items-center justify-center py-20">
+              <View className="i-heroicons-shopping-cart-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 className="bg-white rounded-lg p-4 mb-4 flex items-center">
+                <View 
+                  className={`w-5 h-5 border-2 rounded-full flex items-center justify-center mr-3 ${
+                    selectedItems.length === cart.items.length 
+                      ? 'bg-blue-500 border-blue-500' 
+                      : 'border-gray-300'
+                  }`}
+                  onClick={toggleSelectAll}
+                >
+                  {selectedItems.length === cart.items.length && (
+                    <View className="i-heroicons-check-20-solid w-3 h-3 text-white" />
+                  )}
+                </View>
+                <Text className="text-gray-700">全选 ({cart.items.length}件商品)</Text>
+              </View>
+
+              {/* 商品列表 */}
+              {cart.items.map((item) => (
+                <Card key={item.id} className="mb-4">
+                  <View className="p-4">
+                    <View className="flex items-start">
+                      <View 
+                        className={`w-5 h-5 border-2 rounded-full flex items-center justify-center mr-3 mt-8 ${
+                          selectedItems.includes(item.id)
+                            ? 'bg-blue-500 border-blue-500'
+                            : 'border-gray-300'
+                        }`}
+                        onClick={() => toggleSelectItem(item.id)}
+                      >
+                        {selectedItems.includes(item.id) && (
+                          <View className="i-heroicons-check-20-solid w-3 h-3 text-white" />
+                        )}
+                      </View>
+                      
+                      <Image 
+                        src={item.image}
+                        className="w-20 h-20 rounded-lg mr-3"
+                        mode="aspectFill"
+                      />
+                      
+                      <View className="flex-1">
+                        <Text className="text-sm font-medium text-gray-900 mb-2 line-clamp-2">
+                          {item.name}
+                        </Text>
+                        
+                        <Text className="text-red-500 font-bold mb-2">
+                          ¥{item.price.toFixed(2)}
+                        </Text>
+                        
+                        <View className="flex items-center justify-between">
+                          <View className="flex items-center border border-gray-300 rounded">
+                            <Button
+                              size="sm"
+                              variant="ghost"
+                              className="px-2"
+                              onClick={() => updateQuantity(item.id, item.quantity - 1)}
+                            >
+                              -
+                            </Button>
+                            <Text className="px-3 py-1 border-x border-gray-300 text-sm">
+                              {item.quantity}
+                            </Text>
+                            <Button
+                              size="sm"
+                              variant="ghost"
+                              className="px-2"
+                              onClick={() => updateQuantity(item.id, item.quantity + 1)}
+                            >
+                              +
+                            </Button>
+                          </View>
+                          
+                          <Button
+                            size="sm"
+                            variant="ghost"
+                            className="text-red-500"
+                            onClick={() => removeFromCart(item.id)}
+                          >
+                            删除
+                          </Button>
+                        </View>
+                      </View>
+                    </View>
+                  </View>
+                </Card>
+              ))}
+            </>
+          )}
+        </View>
+      </ScrollView>
+
+      {/* 底部结算栏 */}
+      {cart.items.length > 0 && (
+        <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3">
+          <View className="flex items-center justify-between">
+            <View>
+              <Text className="text-sm text-gray-600">
+                已选 {selectedItems.length} 件
+              </Text>
+              <Text className="text-lg font-bold text-red-500">
+                ¥{selectedItemsTotal.toFixed(2)}
+              </Text>
+            </View>
+            
+            <Button
+              onClick={handleCheckout}
+              disabled={selectedItems.length === 0}
+            >
+              去结算 ({selectedItems.length})
+            </Button>
+          </View>
+        </View>
+      )}
+    </View>
+  )
+}

+ 6 - 0
mini/src/pages/goods-detail/index.config.ts

@@ -0,0 +1,6 @@
+export default definePageConfig({
+  navigationBarTitleText: '商品详情',
+  enablePullDownRefresh: false,
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})

+ 235 - 0
mini/src/pages/goods-detail/index.tsx

@@ -0,0 +1,235 @@
+import { View, ScrollView, Text, Image, RichText } from '@tarojs/components'
+import { useQuery } from '@tanstack/react-query'
+import { useState } from 'react'
+import Taro from '@tarojs/taro'
+import { goodsClient } from '@/api'
+import { InferResponseType } from 'hono'
+import { Navbar } from '@/components/ui/navbar'
+import { Button } from '@/components/ui/button'
+import { Card } from '@/components/ui/card'
+import { Carousel } from '@/components/ui/carousel'
+import { useCart } from '@/utils/cart'
+
+type GoodsResponse = InferResponseType<typeof goodsClient[':id']['$get'], 200>
+
+export default function GoodsDetailPage() {
+  const [quantity, setQuantity] = useState(1)
+  const { addToCart } = useCart()
+  
+  // 获取商品ID
+  const params = Taro.getCurrentInstance().router?.params
+  const goodsId = params?.id ? parseInt(params.id) : 0
+
+  const { data: goods, isLoading } = useQuery({
+    queryKey: ['goods', goodsId],
+    queryFn: async () => {
+      const response = await goodsClient[':id'].$get({
+        param: { id: goodsId }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取商品详情失败')
+      }
+      return response.json()
+    },
+    enabled: goodsId > 0,
+    staleTime: 5 * 60 * 1000,
+  })
+
+  // 商品轮播图
+  const carouselItems = goods?.slideImages?.map(file => ({
+    src: file.fullUrl || '',
+    title: goods.name,
+    description: ''
+  })) || []
+
+  // 添加到购物车
+  const handleAddToCart = () => {
+    if (!goods) return
+    
+    if (quantity > goods.stock) {
+      Taro.showToast({
+        title: '库存不足',
+        icon: 'none'
+      })
+      return
+    }
+
+    addToCart({
+      id: goods.id,
+      name: goods.name,
+      price: goods.price,
+      image: goods.imageFile?.fullUrl || '',
+      stock: goods.stock,
+      quantity
+    })
+    
+    Taro.showToast({
+      title: '已添加到购物车',
+      icon: 'success'
+    })
+  }
+
+  // 立即购买
+  const handleBuyNow = () => {
+    if (!goods) return
+    
+    if (quantity > goods.stock) {
+      Taro.showToast({
+        title: '库存不足',
+        icon: 'none'
+      })
+      return
+    }
+
+    // 将商品信息存入临时存储,跳转到订单确认页
+    Taro.setStorageSync('buyNow', {
+      goods: {
+        id: goods.id,
+        name: goods.name,
+        price: goods.price,
+        image: goods.imageFile?.fullUrl || '',
+        quantity
+      },
+      totalAmount: goods.price * quantity
+    })
+    
+    Taro.navigateTo({
+      url: '/pages/order-submit/index'
+    })
+  }
+
+  if (isLoading) {
+    return (
+      <View className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+      </View>
+    )
+  }
+
+  if (!goods) {
+    return (
+      <View className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <Text className="text-gray-500">商品不存在</Text>
+      </View>
+    )
+  }
+
+  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 pb-20">
+        {/* 商品轮播图 */}
+        {carouselItems.length > 0 && (
+          <View className="mb-4">
+            <Carousel
+              items={carouselItems}
+              height={375}
+              autoplay={true}
+              interval={4000}
+              circular={true}
+            />
+          </View>
+        )}
+
+        <View className="px-4 space-y-4">
+          {/* 商品信息 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-xl font-bold text-gray-900 mb-2">
+                {goods.name}
+              </Text>
+              
+              <Text className="text-sm text-gray-500 mb-4">
+                {goods.instructions || '暂无商品描述'}
+              </Text>
+              
+              <View className="flex items-center justify-between mb-4">
+                <View>
+                  <Text className="text-red-500 text-2xl font-bold">
+                    ¥{goods.price.toFixed(2)}
+                  </Text>
+                  <Text className="text-sm text-gray-400 line-through ml-2">
+                    ¥{goods.costPrice.toFixed(2)}
+                  </Text>
+                </View>
+                <View className="text-sm text-gray-500">
+                  库存: {goods.stock}件
+                </View>
+              </View>
+              
+              <View className="text-sm text-gray-500">
+                已售: {goods.salesNum}件
+              </View>
+            </View>
+          </Card>
+
+          {/* 商品详情 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-4">商品详情</Text>
+              {goods.detail ? (
+                <RichText
+                  nodes={goods.detail
+                    .replace(/<img/g, '<img style="max-width:100%;height:auto"')
+                    .replace(/<p>/g, '<p style="margin:10px 0">')
+                  }
+                />
+              ) : (
+                <Text className="text-gray-500">暂无商品详情</Text>
+              )}
+            </View>
+          </Card>
+        </View>
+      </ScrollView>
+
+      {/* 底部操作栏 */}
+      <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3">
+        <View className="flex items-center justify-between">
+          <View className="flex items-center">
+            <Text className="text-gray-600 mr-2">数量:</Text>
+            <View className="flex items-center border border-gray-300 rounded">
+              <Button
+                size="sm"
+                variant="ghost"
+                className="px-3"
+                onClick={() => setQuantity(Math.max(1, quantity - 1))}
+              >
+                -
+              </Button>
+              <Text className="px-4 py-1 border-x border-gray-300">{quantity}</Text>
+              <Button
+                size="sm"
+                variant="ghost"
+                className="px-3"
+                onClick={() => setQuantity(Math.min(goods.stock, quantity + 1))}
+              >
+                +
+              </Button>
+            </View>
+          </View>
+          
+          <View className="flex space-x-2">
+            <Button 
+              variant="outline"
+              onClick={handleAddToCart}
+              disabled={goods.stock <= 0}
+            >
+              加入购物车
+            </Button>
+            <Button 
+              onClick={handleBuyNow}
+              disabled={goods.stock <= 0}
+            >
+              立即购买
+            </Button>
+          </View>
+        </View>
+      </View>
+    </View>
+  )
+}

+ 7 - 0
mini/src/pages/goods-list/index.config.ts

@@ -0,0 +1,7 @@
+export default definePageConfig({
+  navigationBarTitleText: '商品列表',
+  enablePullDownRefresh: true,
+  backgroundTextStyle: 'dark',
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})

+ 216 - 0
mini/src/pages/goods-list/index.tsx

@@ -0,0 +1,216 @@
+import { View, ScrollView, Text } from '@tarojs/components'
+import { useInfiniteQuery } from '@tanstack/react-query'
+import { useState } from 'react'
+import Taro from '@tarojs/taro'
+import { goodsClient } 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 { useCart } from '@/utils/cart'
+
+type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>
+type Goods = GoodsResponse['data'][0]
+
+export default function GoodsListPage() {
+  const [searchKeyword, setSearchKeyword] = useState('')
+  const { addToCart } = useCart()
+
+  const {
+    data,
+    isLoading,
+    isFetchingNextPage,
+    fetchNextPage,
+    hasNextPage,
+    refetch
+  } = useInfiniteQuery({
+    queryKey: ['goods-infinite', searchKeyword],
+    queryFn: async ({ pageParam = 1 }) => {
+      const response = await goodsClient.$get({
+        query: {
+          page: pageParam,
+          pageSize: 10,
+          keyword: searchKeyword,
+          filters: JSON.stringify({ state: 1 }) // 只显示可用的商品
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取商品失败')
+      }
+      return response.json()
+    },
+    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 allGoods = data?.pages.flatMap(page => page.data) || []
+
+  // 触底加载更多
+  const handleScrollToLower = () => {
+    if (hasNextPage && !isFetchingNextPage) {
+      fetchNextPage()
+    }
+  }
+
+  // 下拉刷新
+  const onPullDownRefresh = () => {
+    refetch().finally(() => {
+      Taro.stopPullDownRefresh()
+    })
+  }
+
+  // 跳转到商品详情
+  const handleGoodsClick = (goods: Goods) => {
+    Taro.navigateTo({
+      url: `/pages/goods-detail/index?id=${goods.id}`
+    })
+  }
+
+  // 添加到购物车
+  const handleAddToCart = (goods: Goods) => {
+    addToCart({
+      id: goods.id,
+      name: goods.name,
+      price: goods.price,
+      image: goods.imageFile?.fullUrl || '',
+      stock: goods.stock,
+      quantity: 1
+    })
+    Taro.showToast({
+      title: '已添加到购物车',
+      icon: 'success'
+    })
+  }
+
+  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="px-4 py-4">
+          {/* 搜索栏 */}
+          <View className="bg-white rounded-lg p-3 mb-4 shadow-sm">
+            <input
+              type="text"
+              placeholder="搜索商品"
+              className="w-full outline-none"
+              value={searchKeyword}
+              onChange={(e) => setSearchKeyword(e.detail.value)}
+              onConfirm={() => refetch()}
+            />
+          </View>
+
+          {/* 商品列表 */}
+          {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>
+          ) : (
+            <>
+              {allGoods.map((goods) => (
+                <Card key={goods.id} className="mb-4 overflow-hidden">
+                  <View className="flex p-4">
+                    <View 
+                      className="w-24 h-24 bg-gray-100 rounded-lg mr-4"
+                      onClick={() => handleGoodsClick(goods)}
+                    >
+                      {goods.imageFile?.fullUrl ? (
+                        <img 
+                          src={goods.imageFile.fullUrl} 
+                          className="w-full h-full object-cover rounded-lg"
+                          mode="aspectFill"
+                        />
+                      ) : (
+                        <View className="w-full h-full flex items-center justify-center text-gray-400">
+                          <View className="i-heroicons-photo-20-solid w-8 h-8" />
+                        </View>
+                      )}
+                    </View>
+                    
+                    <View className="flex-1">
+                      <Text 
+                        className="text-lg font-medium text-gray-900 mb-2 line-clamp-2"
+                        onClick={() => handleGoodsClick(goods)}
+                      >
+                        {goods.name}
+                      </Text>
+                      
+                      <Text className="text-sm text-gray-500 mb-2">
+                        {goods.instructions || '暂无描述'}
+                      </Text>
+                      
+                      <View className="flex items-center justify-between">
+                        <View>
+                          <Text className="text-red-500 text-lg font-bold">
+                            ¥{goods.price.toFixed(2)}
+                          </Text>
+                          <Text className="text-xs text-gray-400 ml-2">
+                            已售{goods.salesNum}
+                          </Text>
+                        </View>
+                        
+                        <View className="flex items-center space-x-2">
+                          <Button 
+                            size="sm" 
+                            variant="outline"
+                            onClick={() => handleGoodsClick(goods)}
+                          >
+                            详情
+                          </Button>
+                          <Button 
+                            size="sm" 
+                            onClick={() => handleAddToCart(goods)}
+                            disabled={goods.stock <= 0}
+                          >
+                            {goods.stock > 0 ? '加入购物车' : '已售罄'}
+                          </Button>
+                        </View>
+                      </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 && allGoods.length > 0 && (
+                <View className="text-center py-4 text-sm text-gray-400">
+                  没有更多了
+                </View>
+              )}
+              
+              {!isLoading && allGoods.length === 0 && (
+                <View className="flex flex-col items-center py-10">
+                  <View className="i-heroicons-inbox-20-solid w-12 h-12 text-gray-300 mb-4" />
+                  <Text className="text-gray-500">暂无商品</Text>
+                </View>
+              )}
+            </>
+          )}
+        </View>
+      </ScrollView>
+    </View>
+  )
+}

+ 6 - 0
mini/src/pages/order-detail/index.config.ts

@@ -0,0 +1,6 @@
+export default definePageConfig({
+  navigationBarTitleText: '订单详情',
+  enablePullDownRefresh: false,
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})

+ 239 - 0
mini/src/pages/order-detail/index.tsx

@@ -0,0 +1,239 @@
+import { View, ScrollView, Text } from '@tarojs/components'
+import { useQuery } from '@tanstack/react-query'
+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'
+
+type OrderResponse = InferResponseType<typeof orderClient[':id']['$get'], 200>
+
+export default function OrderDetailPage() {
+  // 获取订单ID
+  const params = Taro.getCurrentInstance().router?.params
+  const orderId = params?.id ? parseInt(params.id) : 0
+
+  const { data: order, isLoading } = useQuery({
+    queryKey: ['order', orderId],
+    queryFn: async () => {
+      const response = await orderClient[':id'].$get({
+        param: { id: orderId }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取订单详情失败')
+      }
+      return response.json()
+    },
+    enabled: orderId > 0,
+    staleTime: 5 * 60 * 1000,
+  })
+
+  // 解析商品详情
+  const parseGoodsDetail = (goodsDetail: string | null) => {
+    try {
+      return goodsDetail ? JSON.parse(goodsDetail) : []
+    } catch {
+      return []
+    }
+  }
+
+  const goods = order ? parseGoodsDetail(order.goodsDetail) : []
+
+  if (isLoading) {
+    return (
+      <View className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
+      </View>
+    )
+  }
+
+  if (!order) {
+    return (
+      <View className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <Text className="text-gray-500">订单不存在</Text>
+      </View>
+    )
+  }
+
+  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 pb-20">
+        <View className="px-4 space-y-4">
+          {/* 订单状态 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-2">订单状态</Text>
+              <View className="flex justify-between items-center">
+                <Text className="text-lg font-medium text-blue-500">
+                  {order.payState === 0 ? '待付款' : 
+                   order.state === 0 ? '待发货' :
+                   order.state === 1 ? '待收货' :
+                   order.state === 2 ? '已完成' : '已取消'}
+                </Text>
+                <Text className="text-sm text-gray-500">
+                  订单号: {order.orderNo}
+                </Text>
+              </View>
+            </View>
+          </Card>
+
+          {/* 收货信息 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-3">收货信息</Text>
+              <View>
+                <Text className="font-medium">{order.recevierName}</Text>
+                <Text className="text-sm text-gray-600">{order.receiverMobile}</Text>
+                <Text className="text-sm text-gray-600 mt-1">{order.address}</Text>
+              </View>
+            </View>
+          </Card>
+
+          {/* 商品列表 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-4">商品信息</Text>
+              {goods.map((item: any, index: number) => (
+                <View key={index} className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
+                  <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">{item.name}</Text>
+                    <Text className="text-sm text-gray-500">
+                      ¥{item.price.toFixed(2)} × {item.num}
+                    </Text>
+                  </View>
+                  <Text className="text-red-500 font-bold">
+                    ¥{(item.price * item.num).toFixed(2)}
+                  </Text>
+                </View>
+              ))}
+            </View>
+          </Card>
+
+          {/* 订单金额 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-4">订单金额</Text>
+              <View className="space-y-2">
+                <View className="flex justify-between">
+                  <Text className="text-gray-600">商品总价</Text>
+                  <Text>¥{order.amount.toFixed(2)}</Text>
+                </View>
+                <View className="flex justify-between">
+                  <Text className="text-gray-600">运费</Text>
+                  <Text>¥{order.freightAmount.toFixed(2)}</Text>
+                </View>
+                <View className="flex justify-between">
+                  <Text className="text-gray-600">优惠</Text>
+                  <Text>-¥{order.discountAmount.toFixed(2)}</Text>
+                </View>
+                <View className="flex justify-between text-lg font-bold border-t pt-2">
+                  <Text>实付款</Text>
+                  <Text className="text-red-500">¥{order.payAmount.toFixed(2)}</Text>
+                </View>
+              </View>
+            </View>
+          </Card>
+
+          {/* 订单信息 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-4">订单信息</Text>
+              <View className="space-y-2 text-sm">
+                <View className="flex justify-between">
+                  <Text className="text-gray-600">订单编号</Text>
+                  <Text>{order.orderNo}</Text>
+                </View>
+                <View className="flex justify-between">
+                  <Text className="text-gray-600">创建时间</Text>
+                  <Text>{new Date(order.createdAt).toLocaleString()}</Text>
+                </View>
+                {order.payState === 2 && (
+                  <View className="flex justify-between">
+                    <Text className="text-gray-600">支付时间</Text>
+                    <Text>{new Date(order.createdAt).toLocaleString()}</Text>
+                  </View>
+                )}
+              </View>
+            </View>
+          </Card>
+        </View>
+      </ScrollView>
+
+      {/* 底部操作栏 */}
+      <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3">
+        <View className="flex space-x-2 justify-end">
+          {order.payState === 0 && (
+            <>
+              <Button variant="outline" onClick={() => {
+                Taro.showModal({
+                  title: '取消订单',
+                  content: '确定要取消订单吗?',
+                  success: (res) => {
+                    if (res.confirm) {
+                      // 调用取消订单API
+                      Taro.showToast({
+                        title: '已取消订单',
+                        icon: 'success'
+                      })
+                    }
+                  }
+                })
+              }}>
+                取消订单
+              </Button>
+              <Button onClick={() => {
+                Taro.navigateTo({
+                  url: `/pages/payment/index?orderId=${order.id}`
+                })
+              }}>
+                立即支付
+              </Button>
+            </>
+          )}
+          
+          {order.state === 1 && (
+            <Button onClick={() => {
+              Taro.showModal({
+                title: '确认收货',
+                content: '确认已收到商品吗?',
+                success: (res) => {
+                  if (res.confirm) {
+                    // 调用确认收货API
+                    Taro.showToast({
+                      title: '已确认收货',
+                      icon: 'success'
+                    })
+                  }
+                }
+              })
+            }}>
+              确认收货
+            </Button>
+          )}
+          
+          {order.state === 2 && (
+            <Button variant="outline" onClick={() => {
+              Taro.navigateTo({
+                url: `/pages/order-refund/index?orderId=${order.id}`
+              })
+            }}>
+              申请退款
+            </Button>
+          )}
+        </View>
+      </View>
+    </View>
+  )
+}

+ 7 - 0
mini/src/pages/order-list/index.config.ts

@@ -0,0 +1,7 @@
+export default definePageConfig({
+  navigationBarTitleText: '我的订单',
+  enablePullDownRefresh: true,
+  backgroundTextStyle: 'dark',
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})

+ 319 - 0
mini/src/pages/order-list/index.tsx

@@ -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>
+  )
+}

+ 6 - 0
mini/src/pages/order-submit/index.config.ts

@@ -0,0 +1,6 @@
+export default definePageConfig({
+  navigationBarTitleText: '确认订单',
+  enablePullDownRefresh: false,
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black'
+})

+ 296 - 0
mini/src/pages/order-submit/index.tsx

@@ -0,0 +1,296 @@
+import { View, ScrollView, Text } from '@tarojs/components'
+import { useQuery, useMutation } from '@tanstack/react-query'
+import { useState, useEffect } from 'react'
+import Taro from '@tarojs/taro'
+import { deliveryAddressClient, orderClient } from '@/api'
+import { InferResponseType, InferRequestType } 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'
+import { useCart } from '@/utils/cart'
+
+type AddressResponse = InferResponseType<typeof deliveryAddressClient.$get, 200>
+type Address = AddressResponse['data'][0]
+type CreateOrderRequest = InferRequestType<typeof orderClient.$post>['json']
+
+interface CheckoutItem {
+  id: number
+  name: string
+  price: number
+  image: string
+  quantity: number
+}
+
+export default function OrderSubmitPage() {
+  const { user } = useAuth()
+  const { clearCart } = useCart()
+  const [selectedAddress, setSelectedAddress] = useState<Address | null>(null)
+  const [orderItems, setOrderItems] = useState<CheckoutItem[]>([])
+  const [totalAmount, setTotalAmount] = useState(0)
+
+  // 获取地址列表
+  const { data: addresses } = useQuery({
+    queryKey: ['delivery-addresses', user?.id],
+    queryFn: async () => {
+      const response = await deliveryAddressClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ userId: user?.id })
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('获取地址失败')
+      }
+      return response.json()
+    },
+    enabled: !!user?.id,
+  })
+
+  // 创建订单
+  const createOrderMutation = useMutation({
+    mutationFn: async () => {
+      if (!selectedAddress || orderItems.length === 0) {
+        throw new Error('请完善订单信息')
+      }
+
+      const goodsDetail = JSON.stringify(
+        orderItems.map(item => ({
+          goodsId: item.id,
+          name: item.name,
+          price: item.price,
+          num: item.quantity
+        }))
+      )
+
+      const orderData: CreateOrderRequest = {
+        orderNo: `ORD${Date.now()}`,
+        userId: user!.id,
+        amount: totalAmount,
+        payAmount: totalAmount,
+        goodsDetail,
+        addressId: selectedAddress.id,
+        recevierName: selectedAddress.name,
+        receiverMobile: selectedAddress.phone,
+        address: `${selectedAddress.province?.name || ''}${selectedAddress.city?.name || ''}${selectedAddress.district?.name || ''}${selectedAddress.town?.name || ''}${selectedAddress.address}`,
+        orderType: 1,
+        payType: 0,
+        payState: 0,
+        state: 0
+      }
+
+      const response = await orderClient.$post({ json: orderData })
+      if (response.status !== 201) {
+        throw new Error('创建订单失败')
+      }
+      return response.json()
+    },
+    onSuccess: (data) => {
+      // 清空购物车
+      clearCart()
+      
+      Taro.showToast({
+        title: '订单创建成功',
+        icon: 'success'
+      })
+      
+      // 跳转到订单详情页
+      Taro.redirectTo({
+        url: `/pages/order-detail/index?id=${data.id}`
+      })
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message || '创建订单失败',
+        icon: 'none'
+      })
+    }
+  })
+
+  // 页面加载时获取订单数据
+  useEffect(() => {
+    // 从购物车获取数据
+    const checkoutData = Taro.getStorageSync('checkoutItems')
+    const cartData = Taro.getStorageSync('mini_cart')
+    
+    if (checkoutData && checkoutData.items) {
+      setOrderItems(checkoutData.items)
+      setTotalAmount(checkoutData.totalAmount)
+    } else if (cartData && cartData.items) {
+      // 使用购物车数据
+      const items = cartData.items
+      const total = items.reduce((sum: number, item: CheckoutItem) => 
+        sum + (item.price * item.quantity), 0)
+      setOrderItems(items)
+      setTotalAmount(total)
+    }
+
+    // 设置默认地址
+    if (addresses?.data) {
+      const defaultAddress = addresses.data.find(addr => addr.isDefault === 1)
+      setSelectedAddress(defaultAddress || addresses.data[0] || null)
+    }
+  }, [addresses])
+
+  // 选择地址
+  const handleSelectAddress = () => {
+    Taro.navigateTo({
+      url: '/pages/address-manage/index'
+    })
+  }
+
+  // 提交订单
+  const handleSubmitOrder = () => {
+    if (!selectedAddress) {
+      Taro.showToast({
+        title: '请选择收货地址',
+        icon: 'none'
+      })
+      return
+    }
+
+    createOrderMutation.mutate()
+  }
+
+  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 pb-20">
+        <View className="px-4 space-y-4">
+          {/* 收货地址 */}
+          <Card>
+            <View className="p-4">
+              <View className="flex items-center justify-between mb-3">
+                <Text className="text-lg font-bold">收货地址</Text>
+                <Button
+                  size="sm"
+                  variant="ghost"
+                  onClick={handleSelectAddress}
+                >
+                  选择地址
+                </Button>
+              </View>
+              
+              {selectedAddress ? (
+                <View>
+                  <View className="flex items-center mb-2">
+                    <Text className="font-medium mr-3">{selectedAddress.name}</Text>
+                    <Text className="text-gray-600">{selectedAddress.phone}</Text>
+                    {selectedAddress.isDefault === 1 && (
+                      <Text className="ml-2 px-2 py-1 bg-red-100 text-red-600 text-xs rounded">
+                        默认
+                      </Text>
+                    )}
+                  </View>
+                  <Text className="text-sm text-gray-700">
+                    {selectedAddress.province?.name || ''}
+                    {selectedAddress.city?.name || ''}
+                    {selectedAddress.district?.name || ''}
+                    {selectedAddress.town?.name || ''}
+                    {selectedAddress.address}
+                  </Text>
+                </View>
+              ) : (
+                <Button
+                  className="w-full"
+                  onClick={handleSelectAddress}
+                >
+                  请选择收货地址
+                </Button>
+              )}
+            </View>
+          </Card>
+
+          {/* 商品列表 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-4">商品信息</Text>
+              
+              {orderItems.map((item) => (
+                <View key={item.id} className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
+                  <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 mb-1">{item.name}</Text>
+                    <Text className="text-sm text-gray-600">
+                      ¥{item.price.toFixed(2)} × {item.quantity}
+                    </Text>
+                  </View>
+                  
+                  <Text className="text-red-500 font-bold">
+                    ¥{(item.price * item.quantity).toFixed(2)}
+                  </Text>
+                </View>
+              ))}
+            </View>
+          </Card>
+
+          {/* 订单金额 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-4">订单金额</Text>
+              
+              <View className="space-y-2">
+                <View className="flex justify-between">
+                  <Text className="text-gray-600">商品金额</Text>
+                  <Text>¥{totalAmount.toFixed(2)}</Text>
+                </View>
+                
+                <View className="flex justify-between">
+                  <Text className="text-gray-600">运费</Text>
+                  <Text>¥0.00</Text>
+                </View>
+                
+                <View className="flex justify-between text-lg font-bold border-t pt-2">
+                  <Text>实付款</Text>
+                  <Text className="text-red-500">¥{totalAmount.toFixed(2)}</Text>
+                </View>
+              </View>
+            </View>
+          </Card>
+
+          {/* 支付方式 */}
+          <Card>
+            <View className="p-4">
+              <Text className="text-lg font-bold mb-4">支付方式</Text>
+              <View className="flex items-center p-3 border rounded-lg">
+                <View className="i-heroicons-credit-card-20-solid w-5 h-5 mr-3 text-blue-500" />
+                <Text>微信支付</Text>
+              </View>
+            </View>
+          </Card>
+        </View>
+      </ScrollView>
+
+      {/* 底部提交栏 */}
+      <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3">
+        <View className="flex items-center justify-between">
+          <View>
+            <Text className="text-sm text-gray-600">合计: </Text>
+            <Text className="text-lg font-bold text-red-500">
+              ¥{totalAmount.toFixed(2)}
+            </Text>
+          </View>
+          
+          <Button
+            onClick={handleSubmitOrder}
+            disabled={!selectedAddress || orderItems.length === 0 || createOrderMutation.isPending}
+            loading={createOrderMutation.isPending}
+          >
+            提交订单
+          </Button>
+        </View>
+      </View>
+    </View>
+  )
+}

+ 175 - 0
mini/src/utils/cart.ts

@@ -0,0 +1,175 @@
+import { useState, useEffect } from 'react'
+import Taro from '@tarojs/taro'
+
+export interface CartItem {
+  id: number
+  name: string
+  price: number
+  image: string
+  stock: number
+  quantity: number
+}
+
+export interface CartState {
+  items: CartItem[]
+  totalAmount: number
+  totalCount: number
+}
+
+const CART_STORAGE_KEY = 'mini_cart'
+
+export const useCart = () => {
+  const [cart, setCart] = useState<CartState>({
+    items: [],
+    totalAmount: 0,
+    totalCount: 0
+  })
+
+  // 从本地存储加载购物车
+  useEffect(() => {
+    const loadCart = () => {
+      try {
+        const savedCart = Taro.getStorageSync(CART_STORAGE_KEY)
+        if (savedCart && Array.isArray(savedCart.items)) {
+          const totalAmount = savedCart.items.reduce((sum: number, item: CartItem) => 
+            sum + (item.price * item.quantity), 0)
+          const totalCount = savedCart.items.reduce((sum: number, item: CartItem) => 
+            sum + item.quantity, 0)
+          
+          setCart({
+            items: savedCart.items,
+            totalAmount,
+            totalCount
+          })
+        }
+      } catch (error) {
+        console.error('加载购物车失败:', error)
+      }
+    }
+
+    loadCart()
+  }, [])
+
+  // 保存购物车到本地存储
+  const saveCart = (items: CartItem[]) => {
+    const totalAmount = items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
+    const totalCount = items.reduce((sum, item) => sum + item.quantity, 0)
+    
+    const newCart = {
+      items,
+      totalAmount,
+      totalCount
+    }
+    
+    setCart(newCart)
+    
+    try {
+      Taro.setStorageSync(CART_STORAGE_KEY, { items })
+    } catch (error) {
+      console.error('保存购物车失败:', error)
+    }
+  }
+
+  // 添加商品到购物车
+  const addToCart = (item: CartItem) => {
+    const existingItem = cart.items.find(cartItem => cartItem.id === item.id)
+    
+    if (existingItem) {
+      // 如果商品已存在,增加数量
+      const newQuantity = Math.min(existingItem.quantity + item.quantity, item.stock)
+      if (newQuantity === existingItem.quantity) {
+        Taro.showToast({
+          title: '库存不足',
+          icon: 'none'
+        })
+        return
+      }
+      
+      const newItems = cart.items.map(cartItem =>
+        cartItem.id === item.id
+          ? { ...cartItem, quantity: newQuantity }
+          : cartItem
+      )
+      saveCart(newItems)
+      Taro.showToast({
+        title: '已更新购物车',
+        icon: 'success'
+      })
+    } else {
+      // 添加新商品
+      if (item.quantity > item.stock) {
+        Taro.showToast({
+          title: '库存不足',
+          icon: 'none'
+        })
+        return
+      }
+      
+      saveCart([...cart.items, item])
+    }
+  }
+
+  // 从购物车移除商品
+  const removeFromCart = (id: number) => {
+    const newItems = cart.items.filter(item => item.id !== id)
+    saveCart(newItems)
+    Taro.showToast({
+      title: '已移除商品',
+      icon: 'success'
+    })
+  }
+
+  // 更新商品数量
+  const updateQuantity = (id: number, quantity: number) => {
+    const item = cart.items.find(item => item.id === id)
+    if (!item) return
+    
+    if (quantity <= 0) {
+      removeFromCart(id)
+      return
+    }
+    
+    if (quantity > item.stock) {
+      Taro.showToast({
+        title: '库存不足',
+        icon: 'none'
+      })
+      return
+    }
+    
+    const newItems = cart.items.map(item =>
+      item.id === id ? { ...item, quantity } : item
+    )
+    saveCart(newItems)
+  }
+
+  // 清空购物车
+  const clearCart = () => {
+    saveCart([])
+    Taro.showToast({
+      title: '已清空购物车',
+      icon: 'success'
+    })
+  }
+
+  // 检查商品是否在购物车中
+  const isInCart = (id: number) => {
+    return cart.items.some(item => item.id === id)
+  }
+
+  // 获取购物车中商品数量
+  const getItemQuantity = (id: number) => {
+    const item = cart.items.find(item => item.id === id)
+    return item ? item.quantity : 0
+  }
+
+  return {
+    cart,
+    addToCart,
+    removeFromCart,
+    updateQuantity,
+    clearCart,
+    isInCart,
+    getItemQuantity
+  }
+}