index.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import React from 'react'
  2. import { View, Text, ScrollView } from '@tarojs/components'
  3. import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
  4. import { TabBarLayout } from '@/layouts/tab-bar-layout'
  5. import TDesignSearch from '@/components/tdesign/search'
  6. import GoodsList from '@/components/goods-list'
  7. import { GoodsData } from '@/components/goods-card'
  8. import { goodsClient, advertisementClient } from '@/api'
  9. import { InferResponseType } from 'hono'
  10. import './index.css'
  11. import { useAuth } from '@/utils/auth'
  12. import { useCart } from '@/contexts/CartContext'
  13. import { Navbar } from '@/components/ui/navbar'
  14. import { Carousel } from '@/components/ui/carousel'
  15. import Taro from '@tarojs/taro'
  16. type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>
  17. type Goods = GoodsResponse['data'][0]
  18. type AdvertisementResponse = InferResponseType<typeof advertisementClient.$get, 200>
  19. type Advertisement = AdvertisementResponse['data'][0]
  20. const HomePage: React.FC = () => {
  21. const { isLoggedIn } = useAuth();
  22. const { addToCart } = useCart();
  23. if( !isLoggedIn ) return null;
  24. // 广告数据查询
  25. const {
  26. data: advertisementData,
  27. isLoading: isAdLoading,
  28. error: adError
  29. } = useQuery({
  30. queryKey: ['home-advertisements'],
  31. queryFn: async () => {
  32. const response = await advertisementClient.$get({
  33. query: {
  34. filters: JSON.stringify({ status: 1, typeId: 1 }), // 过滤启用的首页轮播广告
  35. sortBy: 'sort', // 按sort字段排序
  36. sortOrder: 'ASC'
  37. }
  38. })
  39. if (response.status !== 200) {
  40. throw new Error('获取广告数据失败')
  41. }
  42. return response.json()
  43. },
  44. staleTime: 5 * 60 * 1000, // 5分钟缓存
  45. })
  46. const {
  47. data,
  48. isLoading,
  49. isFetchingNextPage,
  50. fetchNextPage,
  51. hasNextPage,
  52. error
  53. } = useInfiniteQuery({
  54. queryKey: ['home-goods-infinite'],
  55. queryFn: async ({ pageParam = 1 }) => {
  56. const response = await goodsClient.$get({
  57. query: {
  58. page: pageParam,
  59. pageSize: 10,
  60. filters: JSON.stringify({ state: 1 }) // 只显示可用的商品
  61. }
  62. })
  63. if (response.status !== 200) {
  64. throw new Error('获取商品失败')
  65. }
  66. return response.json()
  67. },
  68. getNextPageParam: (lastPage) => {
  69. const { pagination } = lastPage
  70. const totalPages = Math.ceil(pagination.total / pagination.pageSize)
  71. return pagination.current < totalPages ? pagination.current + 1 : undefined
  72. },
  73. staleTime: 5 * 60 * 1000,
  74. initialPageParam: 1,
  75. })
  76. // 合并所有分页数据
  77. const allGoods = data?.pages.flatMap(page => page.data) || []
  78. // 数据转换:将API返回的商品数据转换为GoodsData接口格式
  79. const convertToGoodsData = (goods: Goods): GoodsData => {
  80. return {
  81. id: goods?.id?.toString() || '', // 将number类型的id转换为string
  82. name: goods?.name || '',
  83. cover_image: goods?.imageFile?.fullUrl || '',
  84. price: goods?.price || 0,
  85. originPrice: goods?.originPrice || 0,
  86. tags: (goods?.salesNum || 0) > 100 ? ['热销'] : ['新品']
  87. }
  88. }
  89. // 转换后的商品列表
  90. const goodsList = allGoods.map(convertToGoodsData)
  91. // 广告数据转换:提取图片URL并过滤掉没有图片的广告
  92. const finalImgSrcs = advertisementData?.data || []
  93. // 错误处理
  94. if (adError) {
  95. console.error('广告数据获取失败:', adError)
  96. }
  97. // 触底加载更多
  98. const handleScrollToLower = () => {
  99. if (hasNextPage && !isFetchingNextPage) {
  100. fetchNextPage()
  101. }
  102. }
  103. // // 商品点击
  104. // const handleGoodsClick = (goods: GoodsData, index: number) => {
  105. // console.log('点击商品:', goods, index)
  106. // }
  107. // 跳转到商品详情
  108. const handleGoodsClick = (goods: Goods) => {
  109. Taro.navigateTo({
  110. url: `/pages/goods-detail/index?id=${goods.id}`
  111. })
  112. }
  113. // 添加购物车
  114. const handleAddCart = (goods: GoodsData, index: number) => {
  115. // 找到对应的原始商品数据
  116. const originalGoods = allGoods.find(g => g.id.toString() === goods.id)
  117. if (originalGoods) {
  118. addToCart({
  119. id: originalGoods.id,
  120. name: originalGoods.name,
  121. price: originalGoods.price,
  122. image: originalGoods.imageFile?.fullUrl || '',
  123. stock: originalGoods.stock,
  124. quantity: 1
  125. })
  126. Taro.showToast({
  127. title: '已添加到购物车',
  128. icon: 'success'
  129. })
  130. }
  131. }
  132. // 商品图片点击
  133. const handleThumbClick = (goods: GoodsData, index: number) => {
  134. console.log('点击商品图片:', goods, index)
  135. }
  136. // 搜索框点击
  137. const handleSearchClick = () => {
  138. Taro.navigateTo({
  139. url: '/pages/search/index'
  140. })
  141. }
  142. return (
  143. <TabBarLayout activeKey="home">
  144. <Navbar
  145. title="首页"
  146. leftIcon=""
  147. onClickLeft={() => Taro.navigateBack()}
  148. rightIcon=""
  149. onClickRight={() => {}}
  150. />
  151. <ScrollView
  152. className="home-scroll-view"
  153. scrollY
  154. onScrollToLower={handleScrollToLower}
  155. >
  156. {/* 页面头部 - 搜索栏和轮播图 */}
  157. <View className="home-page-header">
  158. {/* 搜索栏 */}
  159. <View className="search" onClick={handleSearchClick}>
  160. <TDesignSearch
  161. placeholder="搜索商品..."
  162. disabled={true}
  163. shape="round"
  164. />
  165. </View>
  166. {/* 轮播图 */}
  167. <View className="swiper-wrap">
  168. {isAdLoading ? (
  169. <View className="loading-container">
  170. <Text className="loading-text">广告加载中...</Text>
  171. </View>
  172. ) : adError ? (
  173. <View className="error-container">
  174. <Text className="error-text">广告加载失败</Text>
  175. </View>
  176. ) : finalImgSrcs && finalImgSrcs.length > 0 ? (
  177. <Carousel
  178. items={finalImgSrcs.filter(item => item.imageFile?.fullUrl).map(item => ({
  179. src: item.imageFile!.fullUrl,
  180. title: item.title || '',
  181. description: item.description || ''
  182. }))}
  183. height={800}
  184. autoplay={true}
  185. interval={4000}
  186. circular={true}
  187. imageMode="aspectFill"
  188. />
  189. ) : (
  190. <View className="empty-container">
  191. <Text className="empty-text">暂无广告</Text>
  192. </View>
  193. )}
  194. </View>
  195. </View>
  196. {/* 页面内容 - 商品列表 */}
  197. <View className="home-page-container">
  198. {isLoading ? (
  199. <View className="loading-container">
  200. <Text className="loading-text">加载中...</Text>
  201. </View>
  202. ) : error ? (
  203. <View className="error-container">
  204. <Text className="error-text">加载失败,请重试</Text>
  205. </View>
  206. ) : goodsList.length === 0 ? (
  207. <View className="empty-container">
  208. <Text className="empty-text">暂无商品</Text>
  209. </View>
  210. ) : (
  211. <>
  212. <GoodsList
  213. goodsList={goodsList}
  214. onClick={handleGoodsClick}
  215. onAddCart={handleAddCart}
  216. onThumbClick={handleThumbClick}
  217. />
  218. {/* 加载更多状态 */}
  219. {isFetchingNextPage && (
  220. <View className="loading-more-container">
  221. <Text className="loading-more-text">加载更多...</Text>
  222. </View>
  223. )}
  224. {/* 无更多数据状态 */}
  225. {!hasNextPage && goodsList.length > 0 && (
  226. <View className="no-more-container">
  227. <Text className="no-more-text">已经到底啦</Text>
  228. </View>
  229. )}
  230. </>
  231. )}
  232. </View>
  233. </ScrollView>
  234. </TabBarLayout>
  235. )
  236. }
  237. export default HomePage