index.tsx 7.1 KB

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