index.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import { useState } from 'react'
  2. import { View, Text, ScrollView, Swiper, SwiperItem, Image } from '@tarojs/components'
  3. import Taro from '@tarojs/taro'
  4. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
  5. import { publicAdvertisementClient, wechatCouponStockClient } from '@/api'
  6. import { Button } from '@/components/ui/button'
  7. import { Card, CardContent, CardHeader } from '@/components/ui/card'
  8. import { cn } from '@/utils/cn'
  9. import { receiveCoupon } from '@/utils/coupon-api'
  10. import { Navbar } from '@/components/ui/navbar'
  11. import { TabBarLayout } from '@/layouts/tab-bar-layout'
  12. import { useAuth } from '@/utils/auth'
  13. import type { InferResponseType } from 'hono/client'
  14. // 使用RPC类型安全提取广告响应类型
  15. type AdvertisementResponse = InferResponseType<typeof publicAdvertisementClient.$get, 200>
  16. type Advertisement = AdvertisementResponse['data'][0]
  17. interface WechatCouponStock {
  18. id: number
  19. stockName: string
  20. stockDescription: string
  21. stockType: string
  22. couponAmount: number
  23. couponTotal: number
  24. couponRemaining: number
  25. beginTime: string
  26. endTime: string
  27. status: number
  28. coverImage: string
  29. }
  30. export default function IndexPage() {
  31. const queryClient = useQueryClient()
  32. const { user, isLoggedIn } = useAuth()
  33. // 获取广告列表
  34. const { data: advertisements, isLoading: adsLoading } = useQuery({
  35. queryKey: ['advertisements'],
  36. queryFn: async () => {
  37. const response = await publicAdvertisementClient.$get({
  38. query: {
  39. page: 1,
  40. pageSize: 10,
  41. filters: JSON.stringify({ isEnabled: 1 }),
  42. },
  43. })
  44. if (response.status !== 200) throw new Error('获取广告失败')
  45. const result = await response.json()
  46. return result.data
  47. },
  48. })
  49. // 获取代金券批次列表
  50. const { data: stocks, isLoading: stocksLoading } = useQuery({
  51. queryKey: ['wechat-coupon-stocks'],
  52. queryFn: async () => {
  53. const response = await wechatCouponStockClient.$get({
  54. query: {
  55. page: 1,
  56. pageSize: 20,
  57. filters: JSON.stringify({ status: 1 }),
  58. },
  59. })
  60. if (response.status !== 200) throw new Error('获取批次失败')
  61. const result = await response.json()
  62. return result.data as WechatCouponStock[]
  63. },
  64. })
  65. // 领取代金券
  66. const receiveMutation = useMutation({
  67. mutationFn: async (stockId: number) => {
  68. if (!user?.id) {
  69. throw new Error('请先登录')
  70. }
  71. const result = await receiveCoupon({ stockId, userId: user.id })
  72. if (!result.success) {
  73. throw new Error(result.message)
  74. }
  75. return result
  76. },
  77. onSuccess: () => {
  78. queryClient.invalidateQueries({ queryKey: ['wechat-coupon-stocks'] })
  79. Taro.showToast({
  80. title: '领取成功',
  81. icon: 'success',
  82. })
  83. },
  84. onError: (error) => {
  85. if (error instanceof Error && error.message === '请先登录') {
  86. Taro.showModal({
  87. title: '提示',
  88. content: '请先登录后再领取优惠券',
  89. success: (res) => {
  90. if (res.confirm) {
  91. Taro.navigateTo({ url: '/pages/login/index' })
  92. }
  93. }
  94. })
  95. } else {
  96. Taro.showToast({
  97. title: error instanceof Error ? error.message : '领取失败',
  98. icon: 'none',
  99. })
  100. }
  101. }
  102. })
  103. const handleReceiveCoupon = (stockId: number) => {
  104. if (!isLoggedIn) {
  105. Taro.showModal({
  106. title: '提示',
  107. content: '请先登录后再领取优惠券',
  108. success: (res) => {
  109. if (res.confirm) {
  110. Taro.navigateTo({ url: '/pages/login/index' })
  111. }
  112. }
  113. })
  114. return
  115. }
  116. receiveMutation.mutate(stockId)
  117. }
  118. const handleAdClick = (ad: Advertisement) => {
  119. if (ad.linkUrl) {
  120. Taro.navigateTo({
  121. url: `/pages/webview/index?url=${encodeURIComponent(ad.linkUrl)}`,
  122. })
  123. }
  124. }
  125. return (
  126. <TabBarLayout activeKey="home">
  127. <Navbar title="首页" leftIcon="" leftText="" />
  128. {/* 广告轮播 */}
  129. <View className="bg-white">
  130. {adsLoading ? (
  131. <View className="h-48 flex items-center justify-center">
  132. <Text className="text-gray-400">加载中...</Text>
  133. </View>
  134. ) : advertisements && advertisements.length > 0 ? (
  135. <Swiper
  136. className="h-48"
  137. indicatorColor="#999"
  138. indicatorActiveColor="#333"
  139. circular
  140. autoplay
  141. >
  142. {advertisements.map((ad) => (
  143. <SwiperItem key={ad.id} onClick={() => handleAdClick(ad)}>
  144. <Image
  145. src={ad.imageUrl}
  146. className="w-full h-full"
  147. mode="aspectFill"
  148. />
  149. </SwiperItem>
  150. ))}
  151. </Swiper>
  152. ) : (
  153. <View className="h-48 flex items-center justify-center bg-gray-100">
  154. <Text className="text-gray-400">暂无广告</Text>
  155. </View>
  156. )}
  157. </View>
  158. {/* 代金券批次列表 */}
  159. <ScrollView className="flex-1 p-4">
  160. <View className="mb-4">
  161. <Text className="text-lg font-bold text-gray-900">热门券包</Text>
  162. <Text className="text-sm text-gray-500">限时领取,先到先得</Text>
  163. </View>
  164. {stocksLoading ? (
  165. <View className="flex items-center justify-center py-8">
  166. <Text className="text-gray-400">加载中...</Text>
  167. </View>
  168. ) : stocks && stocks.length > 0 ? (
  169. <View className="space-y-4">
  170. {stocks.map((stock) => (
  171. <Card key={stock.id} className="overflow-hidden">
  172. <CardHeader className="p-0">
  173. <Image
  174. src={stock.coverImage}
  175. className="w-full h-32"
  176. mode="aspectFill"
  177. />
  178. </CardHeader>
  179. <CardContent className="p-4">
  180. <View className="flex justify-between items-start mb-2">
  181. <Text className="text-lg font-bold text-gray-900">
  182. {stock.stockName}
  183. </Text>
  184. <Text className="text-lg font-bold text-red-500">
  185. ¥{stock.couponAmount}
  186. </Text>
  187. </View>
  188. <Text className="text-sm text-gray-600 mb-2">
  189. {stock.stockDescription}
  190. </Text>
  191. <View className="flex justify-between items-center">
  192. <Text className="text-xs text-gray-500">
  193. 剩余: {stock.couponRemaining}/{stock.couponTotal}
  194. </Text>
  195. <Text className="text-xs text-gray-500">
  196. {new Date(stock.endTime).toLocaleDateString()} 截止
  197. </Text>
  198. </View>
  199. <Button
  200. className={cn(
  201. 'w-full mt-3 h-9',
  202. stock.couponRemaining > 0
  203. ? 'bg-red-500 text-white'
  204. : 'bg-gray-300 text-gray-500'
  205. )}
  206. disabled={stock.couponRemaining === 0}
  207. onClick={() => handleReceiveCoupon(stock.id)}
  208. >
  209. {stock.couponRemaining > 0 ? '立即领取' : '已领完'}
  210. </Button>
  211. </CardContent>
  212. </Card>
  213. ))}
  214. </View>
  215. ) : (
  216. <View className="flex items-center justify-center py-8">
  217. <Text className="text-gray-400">暂无券包</Text>
  218. </View>
  219. )}
  220. </ScrollView>
  221. </TabBarLayout>
  222. )
  223. }