| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- import { View, ScrollView, Text } from '@tarojs/components'
- import { useInfiniteQuery } from '@tanstack/react-query'
- import { useState } from 'react'
- import Taro from '@tarojs/taro'
- import { goodsClient } from '@/api'
- import { InferResponseType } from 'hono'
- import { Navbar } from '@/components/ui/navbar'
- import { Card } from '@/components/ui/card'
- import { Button } from '@/components/ui/button'
- import { useCart } from '@/utils/cart'
- import { Image } from '@/components/ui/image'
- import { Input } from '@/components/ui/input'
- import { TabBarLayout } from '@/layouts/tab-bar-layout'
- type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>
- type Goods = GoodsResponse['data'][0]
- export default function GoodsListPage() {
- const [searchKeyword, setSearchKeyword] = useState('')
- const [activeCategory, setActiveCategory] = useState('all')
- const { addToCart } = useCart()
- const categories = [
- { id: 'all', name: '全部' },
- { id: 'hot', name: '热销' },
- { id: 'new', name: '新品' },
- ]
- const {
- data,
- isLoading,
- isFetchingNextPage,
- fetchNextPage,
- hasNextPage,
- refetch
- } = useInfiniteQuery({
- queryKey: ['goods-infinite', searchKeyword],
- queryFn: async ({ pageParam = 1 }) => {
- const response = await goodsClient.$get({
- query: {
- page: pageParam,
- pageSize: 10,
- keyword: searchKeyword,
- filters: JSON.stringify({ state: 1 }) // 只显示可用的商品
- }
- })
- if (response.status !== 200) {
- throw new Error('获取商品失败')
- }
- return response.json()
- },
- getNextPageParam: (lastPage) => {
- const { pagination } = lastPage
- const totalPages = Math.ceil(pagination.total / pagination.pageSize)
- return pagination.current < totalPages ? pagination.current + 1 : undefined
- },
- staleTime: 5 * 60 * 1000,
- initialPageParam: 1,
- })
- // 合并所有分页数据
- const allGoods = data?.pages.flatMap(page => page.data) || []
- // 触底加载更多
- const handleScrollToLower = () => {
- if (hasNextPage && !isFetchingNextPage) {
- fetchNextPage()
- }
- }
- // 下拉刷新
- const onPullDownRefresh = () => {
- refetch().finally(() => {
- Taro.stopPullDownRefresh()
- })
- }
- // 跳转到商品详情
- const handleGoodsClick = (goods: Goods) => {
- Taro.navigateTo({
- url: `/pages/goods-detail/index?id=${goods.id}`
- })
- }
- // 添加到购物车
- const handleAddToCart = (goods: Goods) => {
- addToCart({
- id: goods.id,
- name: goods.name,
- price: goods.price,
- image: goods.imageFile?.fullUrl || '',
- stock: goods.stock,
- quantity: 1
- })
- Taro.showToast({
- title: '已添加到购物车',
- icon: 'success'
- })
- }
- return (
- <TabBarLayout activeKey="goods-list">
- <Navbar
- title="商品列表"
- leftIcon="i-heroicons-chevron-left-20-solid"
- onClickLeft={() => Taro.navigateBack()}
- className="bg-white shadow-sm"
- />
-
- <ScrollView
- className="flex-1"
- scrollY
- onScrollToLower={handleScrollToLower}
- refresherEnabled
- refresherTriggered={false}
- onRefresherRefresh={onPullDownRefresh}
- >
- <View className="px-4 py-4">
- {/* 搜索栏 */}
- <View className="bg-white rounded-2xl p-4 mb-4 shadow-lg">
- <View className="flex items-center space-x-3">
- <View className="flex-1 relative">
- <View className="absolute left-4 top-1/2 -translate-y-1/2">
- <View className="i-heroicons-magnifying-glass-20-solid w-5 h-5 text-gray-400" />
- </View>
- <Input
- type="text"
- placeholder="搜索你想要的商品..."
- className="w-full pl-12 pr-4 py-3 bg-gray-50 rounded-xl outline-none focus:bg-gray-100 transition-colors text-sm"
- value={searchKeyword}
- onChange={(value) => setSearchKeyword(value)}
- onConfirm={() => refetch()}
- />
- </View>
- {searchKeyword && (
- <Button
- size="mini"
- variant="ghost"
- className="!w-8 !h-8 !p-0"
- onClick={() => {
- setSearchKeyword('')
- refetch()
- }}
- >
- <View className="i-heroicons-x-mark-20-solid w-4 h-4 text-gray-500" />
- </Button>
- )}
- </View>
- </View>
- {/* 分类筛选 */}
- <View className="flex space-x-2 mb-4 overflow-x-auto">
- {categories.map((category) => (
- <View
- key={category.id}
- className={`whitespace-nowrap rounded-full px-4 py-2 transition-all cursor-pointer ${
- activeCategory === category.id
- ? 'bg-blue-500 text-white shadow-md'
- : 'text-gray-600 bg-gray-100 hover:bg-gray-200'
- }`}
- onClick={() => {
- setActiveCategory(category.id)
- refetch()
- }}
- >
- {category.name}
- </View>
- ))}
- </View>
- {/* 商品列表 */}
- {isLoading ? (
- <View className="flex justify-center py-10">
- <View className="i-heroicons-arrow-path-20-solid animate-spin w-8 h-8 text-blue-500" />
- </View>
- ) : (
- <>
- <View className="grid grid-cols-2 gap-4">
- {allGoods.map((goods) => (
- <Card
- key={goods.id}
- className="overflow-hidden bg-white rounded-2xl shadow-sm active:shadow-lg transition-all duration-300 active:scale-95"
- onClick={() => handleGoodsClick(goods)}
- >
- <View className="relative">
- <View className="w-full aspect-square bg-gradient-to-br from-gray-100 to-gray-200 overflow-hidden">
- {goods.imageFile?.fullUrl ? (
- <Image
- src={goods.imageFile.fullUrl}
- className="w-full h-full object-cover transition-transform duration-300 active:scale-110"
- mode="aspectFill"
- />
- ) : (
- <View className="w-full h-full flex items-center justify-center text-gray-400">
- <View className="i-heroicons-photo-20-solid w-12 h-12" />
- </View>
- )}
- </View>
-
- {goods.stock <= 0 && (
- <View className="absolute inset-0 bg-black/50 flex items-center justify-center">
- <Text className="text-white font-bold text-sm">已售罄</Text>
- </View>
- )}
-
- {goods.stock > 0 && goods.salesNum > 100 && (
- <View className="absolute top-2 left-2 bg-gradient-to-r from-orange-500 to-red-500 text-white text-xs px-2 py-1 rounded-full shadow-lg">
- 热销
- </View>
- )}
-
- {goods.stock > 0 && goods.salesNum <= 100 && (
- <View className="absolute top-2 right-2 bg-white/90 backdrop-blur text-gray-700 text-xs px-2 py-1 rounded-full shadow-md">
- 库存{goods.stock}
- </View>
- )}
- </View>
-
- <View className="p-3">
- <Text className="text-sm font-semibold text-gray-900 mb-1 line-clamp-2 h-10 leading-tight">
- {goods.name}
- </Text>
-
- <Text className="text-xs text-gray-500 mb-2 line-clamp-1">
- {goods.instructions || '优质商品,值得信赖'}
- </Text>
-
- <View className="flex items-center justify-between">
- <View>
- <Text className="text-red-500 text-lg font-bold">
- ¥{goods.price.toFixed(2)}
- </Text>
- <Text className="text-xs text-gray-400 ml-1">
- 已售{goods.salesNum}
- </Text>
- </View>
-
- <Button
- size="sm"
- className={`!w-8 !h-8 !p-0 rounded-full transition-all ${
- goods.stock > 0
- ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-md active:shadow-lg'
- : 'bg-gray-200 text-gray-400'
- }`}
- onClick={(e) => {
- e.stopPropagation()
- if (goods.stock > 0) {
- handleAddToCart(goods)
- }
- }}
- disabled={goods.stock <= 0}
- >
- <View className={`w-4 h-4 ${goods.stock > 0 ? 'i-heroicons-plus-20-solid' : 'i-heroicons-minus-20-solid'}`} />
- </Button>
- </View>
- </View>
- </Card>
- ))}
- </View>
-
- {isFetchingNextPage && (
- <View className="flex justify-center py-4 mt-4">
- <View className="i-heroicons-arrow-path-20-solid animate-spin w-6 h-6 text-blue-500" />
- <Text className="ml-2 text-sm text-gray-500">加载更多...</Text>
- </View>
- )}
-
- {!hasNextPage && allGoods.length > 0 && (
- <View className="text-center py-8 text-sm text-gray-400">
- <View className="flex items-center justify-center">
- <View className="i-heroicons-check-circle-20-solid w-4 h-4 mr-1" />
- 已经到底啦
- </View>
- </View>
- )}
-
- {!isLoading && allGoods.length === 0 && (
- <View className="flex flex-col items-center py-10">
- <View className="i-heroicons-inbox-20-solid w-16 h-16 text-gray-300 mb-4" />
- <Text className="text-gray-500 text-lg mb-2">暂无商品</Text>
- <Text className="text-gray-400 text-sm">换个关键词试试吧</Text>
- </View>
- )}
- </>
- )}
- </View>
- </ScrollView>
- </TabBarLayout>
- )
- }
|