index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import React from 'react'
  2. import { View, Text, ScrollView, Swiper, SwiperItem, Image } 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, { usePullDownRefresh, useReachBottom, useShareAppMessage } 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. refetch
  54. } = useInfiniteQuery({
  55. queryKey: ['home-goods-infinite'],
  56. queryFn: async ({ pageParam = 1 }) => {
  57. console.debug('请求商品数据,页码:', pageParam)
  58. const response = await goodsClient.$get({
  59. query: {
  60. page: pageParam,
  61. pageSize: 10,
  62. filters: JSON.stringify({ state: 1 }), // 只显示可用的商品
  63. sortBy: 'sort', // 按sort字段排序
  64. sortOrder: 'DESC' // 倒序排列
  65. }
  66. })
  67. if (response.status !== 200) {
  68. throw new Error('获取商品失败')
  69. }
  70. const result = await response.json()
  71. console.debug('API响应数据:', {
  72. page: pageParam,
  73. dataCount: result.data?.length || 0,
  74. pagination: result.pagination
  75. })
  76. return result
  77. },
  78. getNextPageParam: (lastPage, allPages) => {
  79. const { pagination } = lastPage
  80. const totalPages = Math.ceil(pagination.total / pagination.pageSize)
  81. // // 调试信息
  82. // console.debug('分页信息:', {
  83. // current: pagination.current,
  84. // pageSize: pagination.pageSize,
  85. // total: pagination.total,
  86. // totalPages,
  87. // hasNext: pagination.current < totalPages,
  88. // allPagesCount: allPages.length,
  89. // allGoodsCount: allPages.flatMap(page => page.data).length
  90. // })
  91. return pagination.current < totalPages ? pagination.current + 1 : undefined
  92. },
  93. staleTime: 5 * 60 * 1000,
  94. initialPageParam: 1,
  95. })
  96. // 合并所有分页数据
  97. const allGoods = data?.pages.flatMap(page => page.data) || []
  98. // 数据转换:将API返回的商品数据转换为GoodsData接口格式
  99. const convertToGoodsData = (goods: Goods): GoodsData => {
  100. // 判断是否有规格选项:spuId === 0 表示是父商品,且有子商品列表
  101. // 根据GoodsServiceMt实现,父商品返回childGoodsIds字段
  102. const childGoodsIds = (goods as any).childGoodsIds
  103. const hasSpecOptions = goods.spuId === 0 && childGoodsIds && childGoodsIds.length > 0
  104. // parentGoodsId: 如果是父商品,parentGoodsId = goods.id;如果是子商品,parentGoodsId = goods.spuId
  105. const parentGoodsId = goods.spuId === 0 ? goods.id : goods.spuId
  106. const imageUrl = goods?.imageFile?.fullUrl || ''
  107. return {
  108. id: goods?.id?.toString() || '', // 将number类型的id转换为string
  109. name: goods?.name || '',
  110. cover_image: imageUrl,
  111. price: goods?.price || 0,
  112. originPrice: goods?.originPrice || 0,
  113. tags: (goods?.salesNum || 0) > 100 ? ['热销'] : ['新品'],
  114. hasSpecOptions,
  115. parentGoodsId,
  116. stock: goods?.stock || 0,
  117. image: imageUrl, // 与cover_image保持一致
  118. quantity: 1 // 默认数量为1
  119. }
  120. }
  121. // 转换后的商品列表
  122. const goodsList = allGoods.map(convertToGoodsData)
  123. // 广告数据转换:提取图片URL并过滤掉没有图片的广告
  124. const finalImgSrcs = advertisementData?.data || []
  125. // 错误处理
  126. if (adError) {
  127. console.error('广告数据获取失败:', adError)
  128. }
  129. // // 使用Taro全局钩子 - 触底加载更多
  130. // useReachBottom(() => {
  131. // if (hasNextPage && !isFetchingNextPage) {
  132. // fetchNextPage()
  133. // }
  134. // })
  135. // 触底加载更多
  136. const handleScrollToLower = () => {
  137. // console.debug('触底加载更多:', {
  138. // hasNextPage,
  139. // isFetchingNextPage,
  140. // allGoodsCount: allGoods.length,
  141. // pagesCount: data?.pages?.length || 0,
  142. // currentPage: data?.pages?.[data.pages.length - 1]?.pagination?.current || 0
  143. // })
  144. fetchNextPage();
  145. // if (hasNextPage && isFetchingNextPage) {
  146. // console.debug('开始加载下一页...')
  147. // fetchNextPage()
  148. // } else {
  149. // console.debug('无法加载下一页,原因:', {
  150. // hasNextPage,
  151. // isFetchingNextPage
  152. // })
  153. // }
  154. }
  155. // 使用Taro全局钩子 - 下拉刷新
  156. usePullDownRefresh(() => {
  157. refetch().finally(() => {
  158. Taro.stopPullDownRefresh()
  159. })
  160. })
  161. // 分享功能
  162. useShareAppMessage(() => {
  163. return {
  164. title: '发现好物 - 精选商品推荐',
  165. path: '/pages/index/index',
  166. imageUrl: finalImgSrcs && finalImgSrcs.length > 0 ? finalImgSrcs[0].imageFile?.fullUrl : undefined
  167. }
  168. })
  169. // // 商品点击
  170. // const handleGoodsClick = (goods: GoodsData, index: number) => {
  171. // console.log('点击商品:', goods, index)
  172. // }
  173. // 跳转到商品详情
  174. const handleGoodsClick = (goods: Goods) => {
  175. Taro.navigateTo({
  176. url: `/pages/goods-detail/index?id=${goods.id}`
  177. })
  178. }
  179. // 添加购物车
  180. const handleAddCart = (goods: GoodsData, index: number) => {
  181. console.debug('[home] handleAddCart called', { goods, index, goodsId: goods.id, goodsIdType: typeof goods.id })
  182. // 直接使用传递的商品数据,不再依赖原始商品查找
  183. // 安全解析商品ID:支持数字和字符串类型
  184. let id: number
  185. if (typeof goods.id === 'number') {
  186. id = goods.id
  187. } else if (typeof goods.id === 'string') {
  188. id = parseInt(goods.id, 10)
  189. } else {
  190. console.error('商品ID类型无效:', goods.id, typeof goods.id)
  191. Taro.showToast({
  192. title: '商品ID错误',
  193. icon: 'none'
  194. })
  195. return
  196. }
  197. if (isNaN(id)) {
  198. console.error('商品ID解析失败:', goods.id)
  199. Taro.showToast({
  200. title: '商品ID错误',
  201. icon: 'none'
  202. })
  203. return
  204. }
  205. // 验证必要字段
  206. if (!goods.name) {
  207. console.warn('商品名称为空,使用默认值')
  208. }
  209. console.debug('[home] Calling addToCart with cart item:', { id, parentGoodsId: goods.parentGoodsId || 0, name: goods.name || '未命名商品', price: goods.price || 0, quantity: goods.quantity || 1 })
  210. addToCart({
  211. id: id,
  212. parentGoodsId: goods.parentGoodsId || 0, // 默认为0(单规格商品)
  213. name: goods.name || '未命名商品',
  214. price: goods.price || 0,
  215. image: goods.image || goods.cover_image || '', // 优先使用image字段,其次cover_image
  216. stock: goods.stock || 0,
  217. quantity: goods.quantity || 1
  218. })
  219. Taro.showToast({
  220. title: '已添加到购物车',
  221. icon: 'success'
  222. })
  223. }
  224. // 商品图片点击
  225. const handleThumbClick = (goods: GoodsData, index: number) => {
  226. console.log('点击商品图片:', goods, index)
  227. }
  228. // 搜索框点击
  229. const handleSearchClick = () => {
  230. Taro.navigateTo({
  231. url: '/pages/search/index'
  232. })
  233. }
  234. return (
  235. <TabBarLayout activeKey="home">
  236. <Navbar
  237. title="首页"
  238. leftIcon=""
  239. onClickLeft={() => Taro.navigateBack()}
  240. rightIcon=""
  241. onClickRight={() => {}}
  242. />
  243. <ScrollView
  244. className="home-scroll-view"
  245. scrollY
  246. onScrollToLower={handleScrollToLower}
  247. >
  248. {/* 页面头部 - 搜索栏和轮播图 */}
  249. <View className="home-page-header">
  250. {/* 搜索栏 */}
  251. <View className="search" onClick={handleSearchClick}>
  252. <TDesignSearch
  253. placeholder="搜索商品..."
  254. disabled={true}
  255. shape="round"
  256. />
  257. </View>
  258. {/* 轮播图 */}
  259. <View className="swiper-wrap">
  260. {isAdLoading ? (
  261. <View className="loading-container">
  262. <Text className="loading-text">广告加载中...</Text>
  263. </View>
  264. ) : adError ? (
  265. <View className="error-container">
  266. <Text className="error-text">广告加载失败</Text>
  267. </View>
  268. ) : finalImgSrcs && finalImgSrcs.length > 0 ? (
  269. <Carousel
  270. items={finalImgSrcs.filter(item => item.imageFile?.fullUrl).map(item => ({
  271. src: item.imageFile!.fullUrl,
  272. title: item.title || '',
  273. description: item.description || ''
  274. }))}
  275. height={800}
  276. autoplay={true}
  277. interval={4000}
  278. circular={true}
  279. imageMode="aspectFit"
  280. />
  281. )
  282. : (
  283. <View className="empty-container">
  284. <Text className="empty-text">暂无广告</Text>
  285. </View>
  286. )}
  287. </View>
  288. </View>
  289. {/* 页面内容 - 商品列表 */}
  290. <View className="home-page-container">
  291. {isLoading ? (
  292. <View className="loading-container">
  293. <Text className="loading-text">加载中...</Text>
  294. </View>
  295. ) : error ? (
  296. <View className="error-container">
  297. <Text className="error-text">加载失败,请重试</Text>
  298. </View>
  299. ) : goodsList.length === 0 ? (
  300. <View className="empty-container">
  301. <Text className="empty-text">暂无商品</Text>
  302. </View>
  303. ) : (
  304. <>
  305. <GoodsList
  306. goodsList={goodsList}
  307. onClick={handleGoodsClick}
  308. onAddCart={handleAddCart}
  309. onThumbClick={handleThumbClick}
  310. />
  311. {/* 加载更多状态 */}
  312. {isFetchingNextPage && (
  313. <View className="loading-more-container">
  314. <Text className="loading-more-text">加载更多...</Text>
  315. </View>
  316. )}
  317. {/* 无更多数据状态 */}
  318. {!hasNextPage && goodsList.length > 0 && (
  319. <View className="no-more-container">
  320. <Text className="no-more-text">
  321. {`已经到底啦 (共${goodsList.length}件商品)`}
  322. </Text>
  323. </View>
  324. )}
  325. </>
  326. )}
  327. <View className='height130'></View>
  328. </View>
  329. </ScrollView>
  330. </TabBarLayout>
  331. )
  332. }
  333. export default HomePage