Files
xinlong-shop-taro/src/pages/shop/index.tsx
赵忠林 f92e399d72 fix(shop): 修复商品详情页孤立“0”渲染问题
- 修正赚取积分、重量和销量条件渲染,避免数字0被渲染成文本节点
- 赚取积分判断改为仅判断数值是否大于0,防止0泄漏
- 重量同样改为判断数值大于0避免显示无效数据
- 销量为0时隐藏销量信息,符合用户需求
- 调整加购按钮尺寸,使其视觉更协调
2026-07-14 03:05:14 +08:00

397 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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