Files
xinlong-shop-taro/src/pages/shop/index.tsx
赵忠林 2f12a1fd27 feat(shop): 接入分类页分享功能支持好友、朋友圈及复制链接
- 在 `src/pages/shop/index.config.ts` 中启用分享转发配置
- 在 `src/pages/shop/index.tsx` 增加 `useShare` 钩子调用,支持分享标题、路径及朋友圈和复制链接功能
- 分类页作为 tabBar 页面,确保三种分享按钮均可用
- 通过类型检查,代码无错误保证功能稳定
2026-07-13 12:15:03 +08:00

359 lines
13 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 { addToCart } from '@/api/shop/shopCart'
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'
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 [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
addToCart({
goodsId: product.goodsId!,
num: product.step || 1,
}).then(() => {
Taro.showToast({ title: '已加入购物车', icon: 'success' })
}).catch(err => {
Taro.showToast({ title: err.message || '添加失败', icon: 'none' })
})
}
/** 搜索输入(防抖 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'>
<Text className='text-sm text-red-500 font-medium'>
¥{getDisplayPrice(item)}
</Text>
{item.unitName ? (
<Text className='text-xs text-gray-400'>/{item.unitName}</Text>
) : null}
{/* VIP 划掉原价 */}
{isVipMember() && item.dealerPrice ? (
<Text className='text-xs text-gray-400 line-through ml-1'>
¥{item.price}
</Text>
) : item.salePrice && item.salePrice !== item.price ? (
isGuest() ? null : (
<Text className='text-xs text-gray-400 line-through ml-1'>
¥{item.salePrice}
</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-7 h-7 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>
)
}
export default ShopPage