- 修正赚取积分、重量和销量条件渲染,避免数字0被渲染成文本节点 - 赚取积分判断改为仅判断数值是否大于0,防止0泄漏 - 重量同样改为判断数值大于0避免显示无效数据 - 销量为0时隐藏销量信息,符合用户需求 - 调整加购按钮尺寸,使其视觉更协调
397 lines
14 KiB
TypeScript
397 lines
14 KiB
TypeScript
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||
import { View, Text, ScrollView, Image, Input } from '@tarojs/components'
|
||
import Taro from '@tarojs/taro'
|
||
import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory'
|
||
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||
import { useCartContext } from '@/contexts/CartContext'
|
||
import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model'
|
||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||
import { isGuest } from '@/utils/auth'
|
||
import { isVipMember } from '@/utils/vip'
|
||
import { getCompressedImageUrl } from '@/utils/image'
|
||
import { requireLogin } from '@/utils/login-guard'
|
||
import { useShare } from '@/hooks/useShare'
|
||
import EmptyState from '@/components/common/EmptyState'
|
||
import LoadMore from '@/components/common/LoadMore'
|
||
import Price from '@/components/common/Price'
|
||
import { CART_ICON_WHITE } from '@/assets/icons'
|
||
|
||
definePageConfig({
|
||
navigationBarTitleText: '商品分类',
|
||
})
|
||
|
||
// 预置分类
|
||
const PRESET_CATEGORIES = [
|
||
{ id: 0, title: '全部' },
|
||
{ id: -1, title: '推荐' },
|
||
]
|
||
|
||
// ─── 排序选项 ──────────────────────────────────────────────────
|
||
type SortKey = 'default' | 'sales' | 'newest' | 'price'
|
||
|
||
const SORT_TABS: { key: SortKey; label: string }[] = [
|
||
{ key: 'default', label: '默认' },
|
||
{ key: 'sales', label: '销量' },
|
||
{ key: 'newest', label: '最新' },
|
||
{ key: 'price', label: '价格' },
|
||
]
|
||
|
||
const ShopPage: React.FC = () => {
|
||
const { addItem, totalCount } = useCartContext()
|
||
const [categoryList, setCategoryList] = useState<any[]>(PRESET_CATEGORIES)
|
||
const [activeCategory, setActiveCategory] = useState(0)
|
||
|
||
// 商品列表
|
||
const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [page, setPage] = useState(1)
|
||
const [finished, setFinished] = useState(false)
|
||
const loadingRef = useRef(false)
|
||
|
||
// 排序
|
||
const [sortKey, setSortKey] = useState<SortKey>('default')
|
||
const [priceAsc, setPriceAsc] = useState(true)
|
||
|
||
// 搜索
|
||
const [keyword, setKeyword] = useState('')
|
||
const searchTimerRef = useRef<any>(null)
|
||
|
||
// 分享(tabBar 页支持好友分享 + 朋友圈 + 复制链接)
|
||
useShare({
|
||
title: '购物商城 - 精选好物,实惠到家,等你来逛~',
|
||
path: '/pages/shop/index',
|
||
enableTimeline: true,
|
||
enableCopyUrl: true,
|
||
})
|
||
|
||
useEffect(() => {
|
||
loadCategories()
|
||
}, [])
|
||
|
||
const loadCategories = async () => {
|
||
try {
|
||
const list = await listShopGoodsCategory()
|
||
if (list && list.length > 0) {
|
||
const filtered = list.filter((c: any) => c.status === 0)
|
||
setCategoryList([...PRESET_CATEGORIES, ...filtered])
|
||
}
|
||
} catch { /* ignore */ }
|
||
}
|
||
|
||
/** 构建排序参数 */
|
||
const buildSortParams = useCallback((): Partial<ShopGoodsParam> => {
|
||
if (sortKey === 'sales') return { sort: 'sales', order: 'desc' }
|
||
if (sortKey === 'newest') return { sort: 'create_time', order: 'desc' }
|
||
if (sortKey === 'price') return { sort: 'price', order: priceAsc ? 'asc' : 'desc' }
|
||
return {}
|
||
}, [sortKey, priceAsc])
|
||
|
||
/** 加载商品列表 */
|
||
const loadGoods = useCallback(async (p: number) => {
|
||
if (loadingRef.current) return
|
||
loadingRef.current = true
|
||
setLoading(true)
|
||
try {
|
||
const params: ShopGoodsParam = { page: p, limit: 10, status: 0, ...buildSortParams() }
|
||
if (activeCategory > 0) params.categoryId = activeCategory
|
||
if (activeCategory === -1) params.recommend = 1
|
||
if (keyword.trim()) params.keywords = keyword.trim()
|
||
const res = await pageShopGoods(params)
|
||
const newList = res?.list || []
|
||
if (p === 1) {
|
||
setGoodsList(newList)
|
||
} else {
|
||
setGoodsList(prev => [...prev, ...newList])
|
||
}
|
||
setFinished(newList.length < 10)
|
||
setPage(p)
|
||
} catch { /* ignore */ }
|
||
loadingRef.current = false
|
||
setLoading(false)
|
||
}, [activeCategory, buildSortParams, keyword])
|
||
|
||
// 分类或排序变化时重新加载
|
||
useEffect(() => {
|
||
setGoodsList([])
|
||
setFinished(false)
|
||
setPage(1)
|
||
loadGoods(1)
|
||
}, [activeCategory, sortKey, priceAsc])
|
||
|
||
const handleLoadMore = () => {
|
||
if (!finished && !loading) {
|
||
loadGoods(page + 1)
|
||
}
|
||
}
|
||
|
||
/** 排序切换 */
|
||
const handleSortChange = (key: SortKey) => {
|
||
if (key === 'price') {
|
||
if (sortKey === 'price') {
|
||
setPriceAsc(!priceAsc)
|
||
} else {
|
||
setSortKey('price')
|
||
setPriceAsc(true)
|
||
}
|
||
} else {
|
||
setSortKey(key)
|
||
}
|
||
}
|
||
|
||
/** 获取显示价格 */
|
||
const getDisplayPrice = (product: ShopGoods) => {
|
||
if (isVipMember() && product.dealerPrice) return product.dealerPrice
|
||
return product.price || '0'
|
||
}
|
||
|
||
/** 加入购物车 */
|
||
const handleAddToCart = (e: any, product: ShopGoods) => {
|
||
e.stopPropagation()
|
||
if (!requireLogin({ action: 'addToCart' })) return
|
||
addItem(product, undefined, product.step || 1).catch(() => {})
|
||
}
|
||
|
||
/** 搜索输入(防抖 500ms) */
|
||
const handleSearchInput = (e: any) => {
|
||
const val = e.detail.value
|
||
setKeyword(val)
|
||
if (searchTimerRef.current) clearTimeout(searchTimerRef.current)
|
||
searchTimerRef.current = setTimeout(() => {
|
||
setGoodsList([])
|
||
setFinished(false)
|
||
setPage(1)
|
||
loadGoods(1)
|
||
}, 500)
|
||
}
|
||
|
||
/** 跳转商品详情 */
|
||
const goDetail = (product: ShopGoods) => {
|
||
Taro.navigateTo({ url: `/pages/shop/product-detail?id=${product.goodsId}` })
|
||
}
|
||
|
||
return (
|
||
<View className='flex flex-col h-screen bg-gray-50'>
|
||
{/* 顶部搜索栏 */}
|
||
<View className='bg-white px-3 py-2 flex-shrink-0 border-b border-gray-100'>
|
||
<View className='flex items-center bg-gray-100 rounded-full px-3 py-1.5'>
|
||
<Text className='text-gray-400 text-sm mr-1'>🔍</Text>
|
||
<Input
|
||
type='text'
|
||
value={keyword}
|
||
placeholder='搜索商品'
|
||
placeholderClass='text-gray-400'
|
||
confirmType='search'
|
||
className='flex-1 text-sm text-gray-700'
|
||
onInput={handleSearchInput}
|
||
onConfirm={() => {
|
||
setGoodsList([])
|
||
setFinished(false)
|
||
setPage(1)
|
||
loadGoods(1)
|
||
}}
|
||
/>
|
||
{keyword ? (
|
||
<Text
|
||
className='text-gray-400 text-sm pl-1'
|
||
onClick={() => {
|
||
setKeyword('')
|
||
setGoodsList([])
|
||
setFinished(false)
|
||
setPage(1)
|
||
loadGoods(1)
|
||
}}
|
||
>
|
||
✕
|
||
</Text>
|
||
) : null}
|
||
</View>
|
||
</View>
|
||
|
||
{/* 左右分栏 */}
|
||
<View className='flex flex-1 min-h-0'>
|
||
{/* 左侧分类栏 */}
|
||
<ScrollView scrollY className='w-20 bg-gray-100 h-full flex-shrink-0'>
|
||
{categoryList.map(cat => (
|
||
<View
|
||
key={cat.categoryId || cat.id}
|
||
className={`py-3 px-2 text-center text-xs ${
|
||
(cat.categoryId || cat.id) === activeCategory
|
||
? 'bg-white text-green-600 font-medium border-l-2 border-green-500'
|
||
: 'text-gray-600'
|
||
}`}
|
||
onClick={() => setActiveCategory(cat.categoryId || cat.id)}
|
||
>
|
||
<Text>{cat.title}</Text>
|
||
</View>
|
||
))}
|
||
</ScrollView>
|
||
|
||
{/* 右侧:排序栏 + 商品列表 */}
|
||
<View className='flex-1 flex flex-col h-full min-w-0'>
|
||
{/* 排序栏 */}
|
||
<View className='flex items-center bg-white py-2 border-b border-gray-100 flex-shrink-0'>
|
||
{SORT_TABS.map(tab => (
|
||
<View
|
||
key={tab.key}
|
||
className='flex-1 flex items-center justify-center'
|
||
onClick={() => handleSortChange(tab.key)}
|
||
>
|
||
<Text
|
||
className={`text-xs ${
|
||
sortKey === tab.key ? 'text-green-600 font-medium' : 'text-gray-500'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
</Text>
|
||
{tab.key === 'price' && sortKey === 'price' && (
|
||
<Text className='text-xs text-green-600 ml-0.5'>
|
||
{priceAsc ? '↑' : '↓'}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
))}
|
||
<View className='flex-1 flex items-center justify-center'>
|
||
<Text className='text-xs text-gray-400'>筛选</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 商品列表 */}
|
||
<ScrollView
|
||
scrollY
|
||
className='flex-1 min-h-0'
|
||
onScrollToLower={handleLoadMore}
|
||
lowerThreshold={100}
|
||
>
|
||
<View className='p-2'>
|
||
{goodsList.length > 0 ? (
|
||
<View className='flex flex-wrap'>
|
||
{goodsList.map(item => (
|
||
<View
|
||
key={item.goodsId}
|
||
className='w-1/2 p-1'
|
||
onClick={() => goDetail(item)}
|
||
>
|
||
<View className='bg-white rounded-lg overflow-hidden flex flex-col'>
|
||
{/* 商品图片 */}
|
||
<View className='w-full h-36 bg-gray-100 overflow-hidden'>
|
||
<Image
|
||
className='w-full h-full'
|
||
src={getCompressedImageUrl(item.image || item.files || '')}
|
||
mode='aspectFit'
|
||
lazyLoad
|
||
/>
|
||
</View>
|
||
|
||
{/* 商品信息 */}
|
||
<View className='p-2 flex flex-col gap-1'>
|
||
{/* 标题 + 积分标签 */}
|
||
<View>
|
||
<Text className='text-sm text-gray-800 line-clamp-2 leading-5'>
|
||
{item.name || item.goodsName}
|
||
</Text>
|
||
{item.gainIntegral && item.gainIntegral > 0 ? (
|
||
<View className='inline-block mt-1'>
|
||
<Text className='text-xs text-orange-500 bg-orange-50 px-1 py-0.5 rounded'>
|
||
+{item.gainIntegral}积分
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
</View>
|
||
|
||
{/* 价格 + 销量 + 加购 */}
|
||
<View className='flex items-end justify-between'>
|
||
<View className='flex-1 min-w-0'>
|
||
<Price
|
||
price={getDisplayPrice(item)}
|
||
original={
|
||
isVipMember() && item.dealerPrice
|
||
? item.price
|
||
: item.salePrice && item.salePrice !== item.price
|
||
? item.salePrice
|
||
: undefined
|
||
}
|
||
size='small'
|
||
loginMask
|
||
/>
|
||
{item.unitName && !isGuest() ? (
|
||
<Text className='text-xs text-gray-400'>/{item.unitName}</Text>
|
||
) : null}
|
||
{/* 销量 */}
|
||
{item.sales !== undefined && item.sales > 0 && (
|
||
<Text className='text-xs text-gray-400 block mt-0.5'>
|
||
已售{item.sales}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
{/* 加购按钮 */}
|
||
<View
|
||
className='w-6 h-6 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0 ml-1'
|
||
onClick={(e) => handleAddToCart(e, item)}
|
||
>
|
||
<Text className='text-white text-lg leading-none'>+</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
) : (
|
||
!loading && <EmptyState text='暂无商品' />
|
||
)}
|
||
<LoadMore loading={loading} finished={finished} />
|
||
</View>
|
||
</ScrollView>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 浮动购物车按钮 */}
|
||
<View
|
||
className='flex items-center justify-center'
|
||
style={{
|
||
position: 'fixed',
|
||
right: '16px',
|
||
bottom: '80px',
|
||
width: '48px',
|
||
height: '48px',
|
||
borderRadius: '50%',
|
||
backgroundColor: '#0e932e',
|
||
zIndex: 999,
|
||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||
}}
|
||
onClick={() => Taro.navigateTo({ url: '/pages/shop/cart' })}
|
||
>
|
||
<Image
|
||
className='w-7 h-7'
|
||
src={CART_ICON_WHITE}
|
||
mode='aspectFit'
|
||
/>
|
||
{totalCount > 0 && (
|
||
<View
|
||
className='flex items-center justify-center'
|
||
style={{
|
||
position: 'absolute',
|
||
top: '-4px',
|
||
right: '-4px',
|
||
minWidth: '18px',
|
||
height: '18px',
|
||
borderRadius: '9px',
|
||
backgroundColor: '#ef4444',
|
||
borderWidth: '1.5px',
|
||
borderStyle: 'solid',
|
||
borderColor: '#ffffff',
|
||
paddingHorizontal: '4px',
|
||
}}
|
||
>
|
||
<Text className='text-white text-xs font-bold leading-none'>
|
||
{totalCount > 99 ? '99+' : totalCount}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default ShopPage
|