index.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import { View, ScrollView, Text } from '@tarojs/components'
  2. import { useQuery, useMutation } from '@tanstack/react-query'
  3. import { useState, useEffect } from 'react'
  4. import Taro from '@tarojs/taro'
  5. import { deliveryAddressClient, orderClient } from '@/api'
  6. import { InferResponseType, InferRequestType } from 'hono'
  7. import { Navbar } from '@/components/ui/navbar'
  8. import { Card } from '@/components/ui/card'
  9. import { Button } from '@/components/ui/button'
  10. import { useAuth } from '@/utils/auth'
  11. import { useCart } from '@/utils/cart'
  12. import { Image } from '@/components/ui/image'
  13. type AddressResponse = InferResponseType<typeof deliveryAddressClient.$get, 200>
  14. type Address = AddressResponse['data'][0]
  15. type CreateOrderRequest = InferRequestType<typeof orderClient.$post>['json']
  16. interface CheckoutItem {
  17. id: number
  18. name: string
  19. price: number
  20. image: string
  21. quantity: number
  22. }
  23. export default function OrderSubmitPage() {
  24. const { user } = useAuth()
  25. const { clearCart } = useCart()
  26. const [selectedAddress, setSelectedAddress] = useState<Address | null>(null)
  27. const [orderItems, setOrderItems] = useState<CheckoutItem[]>([])
  28. const [totalAmount, setTotalAmount] = useState(0)
  29. // 获取地址列表
  30. const { data: addresses } = useQuery({
  31. queryKey: ['delivery-addresses', user?.id],
  32. queryFn: async () => {
  33. const response = await deliveryAddressClient.$get({
  34. query: {
  35. page: 1,
  36. pageSize: 100,
  37. filters: JSON.stringify({ userId: user?.id })
  38. }
  39. })
  40. if (response.status !== 200) {
  41. throw new Error('获取地址失败')
  42. }
  43. return response.json()
  44. },
  45. enabled: !!user?.id,
  46. })
  47. // 创建订单
  48. const createOrderMutation = useMutation({
  49. mutationFn: async () => {
  50. if (!selectedAddress || orderItems.length === 0) {
  51. throw new Error('请完善订单信息')
  52. }
  53. const goodsDetail = JSON.stringify(
  54. orderItems.map(item => ({
  55. goodsId: item.id,
  56. name: item.name,
  57. price: item.price,
  58. num: item.quantity
  59. }))
  60. )
  61. const orderData: CreateOrderRequest = {
  62. orderNo: `ORD${Date.now()}`,
  63. userId: user!.id,
  64. amount: totalAmount,
  65. payAmount: totalAmount,
  66. goodsDetail,
  67. addressId: selectedAddress.id,
  68. recevierName: selectedAddress.name,
  69. receiverMobile: selectedAddress.phone,
  70. address: `${selectedAddress.province?.name || ''}${selectedAddress.city?.name || ''}${selectedAddress.district?.name || ''}${selectedAddress.town?.name || ''}${selectedAddress.address}`,
  71. orderType: 1,
  72. payType: 0,
  73. payState: 0,
  74. state: 0
  75. }
  76. const response = await orderClient.$post({ json: orderData })
  77. if (response.status !== 201) {
  78. throw new Error('创建订单失败')
  79. }
  80. return response.json()
  81. },
  82. onSuccess: (data) => {
  83. // 清空购物车
  84. clearCart()
  85. Taro.showToast({
  86. title: '订单创建成功',
  87. icon: 'success'
  88. })
  89. // 跳转到订单详情页
  90. Taro.redirectTo({
  91. url: `/pages/order-detail/index?id=${data.id}`
  92. })
  93. },
  94. onError: (error) => {
  95. Taro.showToast({
  96. title: error.message || '创建订单失败',
  97. icon: 'none'
  98. })
  99. }
  100. })
  101. // 页面加载时获取订单数据
  102. useEffect(() => {
  103. // 从购物车获取数据
  104. const checkoutData = Taro.getStorageSync('checkoutItems')
  105. const cartData = Taro.getStorageSync('mini_cart')
  106. if (checkoutData && checkoutData.items) {
  107. setOrderItems(checkoutData.items)
  108. setTotalAmount(checkoutData.totalAmount)
  109. } else if (cartData && cartData.items) {
  110. // 使用购物车数据
  111. const items = cartData.items
  112. const total = items.reduce((sum: number, item: CheckoutItem) =>
  113. sum + (item.price * item.quantity), 0)
  114. setOrderItems(items)
  115. setTotalAmount(total)
  116. }
  117. // 设置默认地址
  118. if (addresses?.data) {
  119. const defaultAddress = addresses.data.find(addr => addr.isDefault === 1)
  120. setSelectedAddress(defaultAddress || addresses.data[0] || null)
  121. }
  122. }, [addresses])
  123. // 选择地址
  124. const handleSelectAddress = () => {
  125. Taro.navigateTo({
  126. url: '/pages/address-manage/index'
  127. })
  128. }
  129. // 提交订单
  130. const handleSubmitOrder = () => {
  131. if (!selectedAddress) {
  132. Taro.showToast({
  133. title: '请选择收货地址',
  134. icon: 'none'
  135. })
  136. return
  137. }
  138. createOrderMutation.mutate()
  139. }
  140. return (
  141. <View className="min-h-screen bg-gray-50">
  142. <Navbar
  143. title="确认订单"
  144. leftIcon="i-heroicons-chevron-left-20-solid"
  145. onClickLeft={() => Taro.navigateBack()}
  146. />
  147. <ScrollView className="h-screen pt-12 pb-20">
  148. <View className="px-4 space-y-4">
  149. {/* 收货地址 */}
  150. <Card>
  151. <View className="p-4">
  152. <View className="flex items-center justify-between mb-3">
  153. <Text className="text-lg font-bold">收货地址</Text>
  154. <Button
  155. size="sm"
  156. variant="ghost"
  157. onClick={handleSelectAddress}
  158. >
  159. 选择地址
  160. </Button>
  161. </View>
  162. {selectedAddress ? (
  163. <View>
  164. <View className="flex items-center mb-2">
  165. <Text className="font-medium mr-3">{selectedAddress.name}</Text>
  166. <Text className="text-gray-600">{selectedAddress.phone}</Text>
  167. {selectedAddress.isDefault === 1 && (
  168. <Text className="ml-2 px-2 py-1 bg-red-100 text-red-600 text-xs rounded">
  169. 默认
  170. </Text>
  171. )}
  172. </View>
  173. <Text className="text-sm text-gray-700">
  174. {selectedAddress.province?.name || ''}
  175. {selectedAddress.city?.name || ''}
  176. {selectedAddress.district?.name || ''}
  177. {selectedAddress.town?.name || ''}
  178. {selectedAddress.address}
  179. </Text>
  180. </View>
  181. ) : (
  182. <Button
  183. className="w-full"
  184. onClick={handleSelectAddress}
  185. >
  186. 请选择收货地址
  187. </Button>
  188. )}
  189. </View>
  190. </Card>
  191. {/* 商品列表 */}
  192. <Card>
  193. <View className="p-4">
  194. <Text className="text-lg font-bold mb-4">商品信息</Text>
  195. {orderItems.map((item) => (
  196. <View key={item.id} className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
  197. <Image
  198. src={item.image}
  199. className="w-16 h-16 rounded-lg mr-3"
  200. mode="aspectFill"
  201. />
  202. <View className="flex-1">
  203. <Text className="text-sm font-medium mb-1">{item.name}</Text>
  204. <Text className="text-sm text-gray-600">
  205. ¥{item.price.toFixed(2)} × {item.quantity}
  206. </Text>
  207. </View>
  208. <Text className="text-red-500 font-bold">
  209. ¥{(item.price * item.quantity).toFixed(2)}
  210. </Text>
  211. </View>
  212. ))}
  213. </View>
  214. </Card>
  215. {/* 订单金额 */}
  216. <Card>
  217. <View className="p-4">
  218. <Text className="text-lg font-bold mb-4">订单金额</Text>
  219. <View className="space-y-2">
  220. <View className="flex justify-between">
  221. <Text className="text-gray-600">商品金额</Text>
  222. <Text>¥{totalAmount.toFixed(2)}</Text>
  223. </View>
  224. <View className="flex justify-between">
  225. <Text className="text-gray-600">运费</Text>
  226. <Text>¥0.00</Text>
  227. </View>
  228. <View className="flex justify-between text-lg font-bold border-t pt-2">
  229. <Text>实付款</Text>
  230. <Text className="text-red-500">¥{totalAmount.toFixed(2)}</Text>
  231. </View>
  232. </View>
  233. </View>
  234. </Card>
  235. {/* 支付方式 */}
  236. <Card>
  237. <View className="p-4">
  238. <Text className="text-lg font-bold mb-4">支付方式</Text>
  239. <View className="flex items-center p-3 border rounded-lg">
  240. <View className="i-heroicons-credit-card-20-solid w-5 h-5 mr-3 text-blue-500" />
  241. <Text>微信支付</Text>
  242. </View>
  243. </View>
  244. </Card>
  245. </View>
  246. </ScrollView>
  247. {/* 底部提交栏 */}
  248. <View className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3">
  249. <View className="flex items-center justify-between">
  250. <View>
  251. <Text className="text-sm text-gray-600">合计: </Text>
  252. <Text className="text-lg font-bold text-red-500">
  253. ¥{totalAmount.toFixed(2)}
  254. </Text>
  255. </View>
  256. <Button
  257. onClick={handleSubmitOrder}
  258. disabled={!selectedAddress || orderItems.length === 0 || createOrderMutation.isPending}
  259. loading={createOrderMutation.isPending}
  260. >
  261. 提交订单
  262. </Button>
  263. </View>
  264. </View>
  265. </View>
  266. )
  267. }