Files
xinlong-shop-taro/src/pages/shop/index.tsx
赵忠林 1461a0e563 fix(shop): 修正购物车页面跳转为切换标签页
- 将购物车按钮点击事件由 navigateTo 改为 switchTab
- 统一购物车跳转逻辑,提升用户体验
- 修复商品详情页跳转购物车方式错误问题
2026-07-16 21:58:54 +08:00

399 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 { useVipStatus } from '@/hooks/useVipStatus'
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()
// VIP 状态异步校验并更新缓存isVip 变化时触发重渲染
const { isVip } = useVipStatus()
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 (isVip && 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={
isVip && 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.switchTab({ 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