2
0

index.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import React, { useState, useEffect } from 'react'
  2. import { View, Text, ScrollView } from '@tarojs/components'
  3. import { useInfiniteQuery } from '@tanstack/react-query'
  4. import Taro, { useRouter } from '@tarojs/taro'
  5. import { Navbar } from '@/components/ui/navbar'
  6. import TDesignSearch from '@/components/tdesign/search'
  7. import GoodsList from '@/components/goods-list'
  8. import { goodsClient } from '@/api'
  9. import { InferResponseType } from 'hono'
  10. import { useCart } from '@/contexts/CartContext'
  11. import './index.css'
  12. type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>
  13. type Goods = GoodsResponse['data'][0]
  14. const SearchResultPage: React.FC = () => {
  15. const { addToCart } = useCart()
  16. // 使用useRouter钩子获取路由参数
  17. const router = useRouter()
  18. const params = router.params
  19. const searchKeyword = params?.keyword || ''
  20. // 直接使用路由参数,无需useEffect
  21. const [keyword, setKeyword] = useState(searchKeyword)
  22. const [searchValue, setSearchValue] = useState(searchKeyword)
  23. const {
  24. data,
  25. isLoading,
  26. isFetchingNextPage,
  27. fetchNextPage,
  28. hasNextPage,
  29. refetch
  30. } = useInfiniteQuery({
  31. queryKey: ['search-goods-infinite', keyword],
  32. queryFn: async ({ pageParam = 1 }) => {
  33. const response = await goodsClient.$get({
  34. query: {
  35. page: pageParam,
  36. pageSize: 10,
  37. keyword: keyword,
  38. filters: JSON.stringify({ state: 1 }) // 只显示可用的商品
  39. }
  40. })
  41. if (response.status !== 200) {
  42. throw new Error('搜索商品失败')
  43. }
  44. return response.json()
  45. },
  46. getNextPageParam: (lastPage) => {
  47. const { pagination } = lastPage
  48. const totalPages = Math.ceil(pagination.total / pagination.pageSize)
  49. return pagination.current < totalPages ? pagination.current + 1 : undefined
  50. },
  51. staleTime: 5 * 60 * 1000,
  52. initialPageParam: 1,
  53. enabled: !!keyword, // 只有有搜索关键词时才执行查询
  54. })
  55. // 合并所有分页数据
  56. const allGoods = data?.pages.flatMap(page => page.data) || []
  57. // 触底加载更多
  58. const handleScrollToLower = () => {
  59. if (hasNextPage && !isFetchingNextPage) {
  60. fetchNextPage()
  61. }
  62. }
  63. // 下拉刷新
  64. const onPullDownRefresh = () => {
  65. refetch().finally(() => {
  66. Taro.stopPullDownRefresh()
  67. })
  68. }
  69. // 处理搜索提交
  70. const handleSubmit = (value: string) => {
  71. if (!value.trim()) return
  72. // 更新搜索关键词并重新搜索
  73. setKeyword(value)
  74. setSearchValue(value)
  75. // 重置分页数据
  76. refetch()
  77. }
  78. // 跳转到商品详情
  79. const handleGoodsClick = (goods: Goods) => {
  80. Taro.navigateTo({
  81. url: `/pages/goods-detail/index?id=${goods.id}`
  82. })
  83. }
  84. // 添加到购物车
  85. const handleAddToCart = (goods: Goods) => {
  86. addToCart({
  87. id: goods.id,
  88. name: goods.name,
  89. price: goods.price,
  90. image: goods.imageFile?.fullUrl || '',
  91. stock: goods.stock,
  92. quantity: 1
  93. })
  94. Taro.showToast({
  95. title: '已添加到购物车',
  96. icon: 'success'
  97. })
  98. }
  99. return (
  100. <View className="search-result-page">
  101. <Navbar
  102. title="搜索结果"
  103. leftIcon="i-heroicons-chevron-left-20-solid"
  104. onClickLeft={() => Taro.navigateBack()}
  105. className="bg-white"
  106. />
  107. <ScrollView
  108. className="search-result-content"
  109. scrollY
  110. onScrollToLower={handleScrollToLower}
  111. refresherEnabled
  112. refresherTriggered={false}
  113. onRefresherRefresh={onPullDownRefresh}
  114. >
  115. {/* 搜索栏 - 参照tcb-shop-demo设计 */}
  116. <View className="search-bar-container">
  117. <View className="search-input-wrapper">
  118. <View className="i-heroicons-magnifying-glass-20-solid search-icon" />
  119. <input
  120. className="search-input"
  121. placeholder="搜索商品..."
  122. value={searchValue}
  123. onChange={(e) => setSearchValue(e.target.value)}
  124. onKeyPress={(e) => {
  125. if (e.key === 'Enter') {
  126. handleSubmit(searchValue)
  127. }
  128. }}
  129. />
  130. {searchValue && (
  131. <View
  132. className="i-heroicons-x-mark-20-solid clear-icon"
  133. onClick={() => {
  134. setSearchValue('')
  135. setKeyword('')
  136. }}
  137. />
  138. )}
  139. </View>
  140. </View>
  141. {/* 搜索结果 */}
  142. <View className="result-container">
  143. {/* 搜索结果标题 */}
  144. {keyword && (
  145. <View className="result-header">
  146. <Text className="result-title">
  147. 搜索结果:"{keyword}"
  148. </Text>
  149. <Text className="result-count">
  150. 共找到 {data?.pages[0]?.pagination?.total || 0} 件商品
  151. </Text>
  152. </View>
  153. )}
  154. {/* 商品列表 */}
  155. {isLoading ? (
  156. <View className="loading-container">
  157. <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
  158. <Text className="loading-text">搜索中...</Text>
  159. </View>
  160. ) : allGoods.length === 0 ? (
  161. <View className="empty-container">
  162. <View className="i-heroicons-magnifying-glass-20-solid empty-icon" />
  163. <Text className="empty-text">
  164. {keyword ? '暂无相关商品' : '请输入搜索关键词'}
  165. </Text>
  166. <Text className="empty-subtext">
  167. {keyword ? '换个关键词试试吧' : '搜索你想要的商品'}
  168. </Text>
  169. </View>
  170. ) : (
  171. <>
  172. <View className="goods-list-container">
  173. <GoodsList
  174. goodsList={allGoods.map(goods => ({
  175. id: goods.id.toString(),
  176. name: goods.name,
  177. cover_image: goods.imageFile?.fullUrl,
  178. price: goods.price,
  179. originPrice: goods.originPrice,
  180. tags: goods.stock <= 0 ? ['已售罄'] : goods.salesNum > 100 ? ['热销'] : []
  181. }))}
  182. onClick={(goods) => handleGoodsClick(allGoods.find(g => g.id.toString() === goods.id)!)}
  183. onAddCart={(goods) => handleAddToCart(allGoods.find(g => g.id.toString() === goods.id)!)}
  184. />
  185. </View>
  186. {/* 加载更多状态 */}
  187. {isFetchingNextPage && (
  188. <View className="loading-more-container">
  189. <View className="i-heroicons-arrow-path-20-solid animate-spin w-6 h-6 text-blue-500" />
  190. <Text className="loading-more-text">加载更多...</Text>
  191. </View>
  192. )}
  193. {/* 无更多数据状态 */}
  194. {!hasNextPage && allGoods.length > 0 && (
  195. <View className="no-more-container">
  196. <View className="i-heroicons-check-circle-20-solid w-4 h-4 mr-1" />
  197. <Text className="no-more-text">已经到底啦</Text>
  198. </View>
  199. )}
  200. </>
  201. )}
  202. </View>
  203. </ScrollView>
  204. </View>
  205. )
  206. }
  207. export default SearchResultPage