- 将图片显示统一替换为 getCompressedImageUrl 函数处理压缩 - 覆盖 35 个文件,包括 4 个组件和 31 个页面 - 主要组件:LazyImage、ProductCard、OrderCard、SkuSelector 内部自动压缩图片 - 主要页面:购物车、订单、积分、活动、拼团、秒杀、收藏、浏览历史等 - 跳过用户头像、二维码、商品详情大图及所有 Taro.previewImage 调用 - 调整压缩默认宽度为 300,保持质量90,启用压缩功能
325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||
import Taro, { useRouter } from '@tarojs/taro'
|
||
import { getShopGroupBuy, listShopGroupBuyRecords, joinGroupBuy, createGroupBuy } from '@/api/shop/shopGroupBuy'
|
||
import { getShopGoods } from '@/api/shop/shopGoods'
|
||
import type { ShopGroupBuy, ShopGroupBuyRecord } from '@/api/shop/shopGroupBuy/model'
|
||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||
import type { ShopGoodsSku } from '@/api/shop/shopGoodsSku/model'
|
||
import SkuSelector from '@/components/business/SkuSelector'
|
||
import { getCompressedImageUrl } from '@/utils/image'
|
||
|
||
definePageConfig({
|
||
navigationBarTitleText: '拼团详情',
|
||
})
|
||
|
||
const GroupBuyDetailPage: React.FC = () => {
|
||
const router = useRouter()
|
||
const groupBuyId = Number(router.params.groupBuyId || 0)
|
||
|
||
const [groupBuy, setGroupBuy] = useState<ShopGroupBuy | null>(null)
|
||
const [product, setProduct] = useState<ShopGoods | null>(null)
|
||
const [records, setRecords] = useState<ShopGroupBuyRecord[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [skuVisible, setSkuVisible] = useState(false)
|
||
const [pendingRecordId, setPendingRecordId] = useState<number | null>(null)
|
||
|
||
useEffect(() => {
|
||
if (groupBuyId) {
|
||
fetchData()
|
||
}
|
||
}, [groupBuyId])
|
||
|
||
const fetchData = async () => {
|
||
try {
|
||
setLoading(true)
|
||
const [res1, res2] = await Promise.all([
|
||
getShopGroupBuy(groupBuyId),
|
||
listShopGroupBuyRecords(groupBuyId),
|
||
])
|
||
|
||
if (res1.code === 0 && res1.data) {
|
||
const data = res1.data
|
||
setGroupBuy(data)
|
||
|
||
// 加载商品完整信息(含规格/SKU)
|
||
if (data.goodsId) {
|
||
fetchGoodsDetail(data.goodsId)
|
||
}
|
||
}
|
||
|
||
if (res2.code === 0 && res2.data) {
|
||
setRecords(res2.data)
|
||
}
|
||
} catch (err) {
|
||
console.error('获取拼团详情失败', err)
|
||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const fetchGoodsDetail = async (goodsId: number) => {
|
||
try {
|
||
const res = await getShopGoods(goodsId)
|
||
if (res.code === 0 && res.data) {
|
||
setProduct(res.data)
|
||
}
|
||
} catch (err) {
|
||
console.error('获取商品详情失败', err)
|
||
}
|
||
}
|
||
|
||
// 准备商品规格并显示选择器(开团)
|
||
const handleCreateGroup = () => {
|
||
if (!groupBuy) return
|
||
setPendingRecordId(null) // 标记为开团
|
||
|
||
if (!product) {
|
||
Taro.showLoading({ title: '加载规格...' })
|
||
getShopGoods(groupBuy.goodsId)
|
||
.then(res => {
|
||
Taro.hideLoading()
|
||
if (res.code === 0 && res.data) {
|
||
setProduct(res.data)
|
||
setSkuVisible(true)
|
||
} else {
|
||
Taro.showToast({ title: '商品信息加载失败', icon: 'none' })
|
||
}
|
||
})
|
||
.catch(() => {
|
||
Taro.hideLoading()
|
||
Taro.showToast({ title: '商品信息加载失败', icon: 'none' })
|
||
})
|
||
} else {
|
||
setSkuVisible(true)
|
||
}
|
||
}
|
||
|
||
// 去凑单(参团)
|
||
const handleJoinGroup = (recordId: number) => {
|
||
if (!groupBuy) return
|
||
setPendingRecordId(recordId)
|
||
|
||
if (!product) {
|
||
Taro.showLoading({ title: '加载规格...' })
|
||
getShopGoods(groupBuy.goodsId)
|
||
.then(res => {
|
||
Taro.hideLoading()
|
||
if (res.code === 0 && res.data) {
|
||
setProduct(res.data)
|
||
setSkuVisible(true)
|
||
} else {
|
||
Taro.showToast({ title: '商品信息加载失败', icon: 'none' })
|
||
}
|
||
})
|
||
.catch(() => {
|
||
Taro.hideLoading()
|
||
Taro.showToast({ title: '商品信息加载失败', icon: 'none' })
|
||
})
|
||
} else {
|
||
setSkuVisible(true)
|
||
}
|
||
}
|
||
|
||
const handleSkuConfirm = async (sku: ShopGoodsSku, quantity: number) => {
|
||
if (!groupBuy) return
|
||
|
||
setSubmitting(true)
|
||
try {
|
||
let res: any
|
||
if (pendingRecordId && pendingRecordId > 0) {
|
||
// 参团
|
||
res = await joinGroupBuy({
|
||
recordId: pendingRecordId,
|
||
goodsId: groupBuy.goodsId,
|
||
skuId: sku.id || 0,
|
||
quantity,
|
||
})
|
||
Taro.showToast({ title: '参团成功', icon: 'success' })
|
||
} else {
|
||
// 开团
|
||
res = await createGroupBuy({
|
||
groupBuyId: groupBuy.id!,
|
||
goodsId: groupBuy.goodsId,
|
||
skuId: sku.id || 0,
|
||
quantity,
|
||
})
|
||
Taro.showToast({ title: '开团成功', icon: 'success' })
|
||
}
|
||
|
||
if (res.code === 0) {
|
||
fetchData() // 刷新数据
|
||
}
|
||
} catch (err: any) {
|
||
Taro.showToast({ title: err?.message || '操作失败', icon: 'none' })
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<View className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||
<Text className="text-gray-400 text-sm">加载中...</Text>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
if (!groupBuy) {
|
||
return (
|
||
<View className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||
<Text className="text-gray-400 text-sm">拼团活动不存在</Text>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
const remaining = groupBuy.groupSize - groupBuy.currentSize
|
||
const isActive = groupBuy.status === 1
|
||
|
||
return (
|
||
<View className="min-h-screen bg-gray-50 pb-20">
|
||
<ScrollView scrollY className="h-screen">
|
||
{/* 商品信息 */}
|
||
<View className="bg-white p-4 flex gap-3">
|
||
<Image
|
||
className="w-24 h-24 rounded-md bg-gray-100"
|
||
src={getCompressedImageUrl(groupBuy.goodsImage || groupBuy.product?.image || '')}
|
||
mode="aspectFill"
|
||
/>
|
||
<View className="flex-1 flex flex-col justify-between">
|
||
<Text className="text-sm text-gray-700 font-medium line-clamp-2">
|
||
{groupBuy.goodsName || groupBuy.product?.name}
|
||
</Text>
|
||
<View className="flex items-baseline gap-2">
|
||
<Text className="text-2xl font-bold text-red-500">
|
||
¥{groupBuy.groupPrice}
|
||
</Text>
|
||
<Text className="text-xs text-gray-400 line-through">
|
||
¥{groupBuy.product?.price || 0}
|
||
</Text>
|
||
</View>
|
||
<View className="flex items-center gap-2 mt-1">
|
||
{isActive && remaining > 0 && (
|
||
<Text className="text-xs text-red-500 font-medium">拼团进行中</Text>
|
||
)}
|
||
{remaining === 0 && (
|
||
<Text className="text-xs text-green-500 font-medium">已成团</Text>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 拼团进度 */}
|
||
<View className="bg-white mt-3 p-4">
|
||
<Text className="text-base font-medium text-gray-800 mb-3 block">拼团进度</Text>
|
||
<View className="flex items-center justify-between">
|
||
<Text className="text-sm text-gray-600">成团人数</Text>
|
||
<Text className="text-sm font-medium text-gray-800">{groupBuy.groupSize}人</Text>
|
||
</View>
|
||
<View className="flex items-center justify-between mt-2">
|
||
<Text className="text-sm text-gray-600">已参团</Text>
|
||
<Text className="text-sm font-medium text-red-500">{groupBuy.currentSize}人</Text>
|
||
</View>
|
||
<View className="mt-3 bg-gray-100 rounded-full h-2 overflow-hidden">
|
||
<View
|
||
className="h-full rounded-full"
|
||
style={{
|
||
width: `${Math.min((groupBuy.currentSize / groupBuy.groupSize) * 100, 100)}%`,
|
||
backgroundColor: '#ef4444',
|
||
}}
|
||
/>
|
||
</View>
|
||
{remaining > 0 && (
|
||
<Text className="text-xs text-gray-400 mt-2 block">
|
||
还差{remaining}人成团,快来参与吧!
|
||
</Text>
|
||
)}
|
||
{remaining === 0 && (
|
||
<Text className="text-xs text-green-500 mt-2 block">拼团已成功,人数已满</Text>
|
||
)}
|
||
</View>
|
||
|
||
{/* 正在拼团的列表 */}
|
||
{records.length > 0 && (
|
||
<View className="bg-white mt-3 p-4">
|
||
<Text className="text-base font-medium text-gray-800 mb-3 block">
|
||
以下小伙伴正在拼团,可直接参与
|
||
</Text>
|
||
{records.map(record => (
|
||
<View key={record.id} className="flex items-center justify-between py-3 border-b border-gray-50">
|
||
<View className="flex items-center gap-3">
|
||
<View className="w-10 h-10 rounded-full flex items-center justify-center" style={{ backgroundColor: '#fef2f2' }}>
|
||
<Text className="text-xs text-red-500">
|
||
{record.isLeader ? '团长' : '成员'}
|
||
</Text>
|
||
</View>
|
||
<View>
|
||
<Text className="text-sm text-gray-700">
|
||
{record.isLeader ? '团长' : `团员${record.id}`}
|
||
</Text>
|
||
<Text className="text-xs text-gray-400">
|
||
{record.groupSize - record.memberCount > 0
|
||
? `还差${record.groupSize - record.memberCount}人`
|
||
: '已成团'}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
{isActive && record.status === 1 && (
|
||
<View
|
||
className="px-4 py-1 rounded-full text-white text-xs"
|
||
style={{ backgroundColor: '#ef4444' }}
|
||
onClick={() => handleJoinGroup(record.id)}
|
||
>
|
||
<Text className="text-white text-xs">去凑单</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
))}
|
||
</View>
|
||
)}
|
||
|
||
{/* 拼团规则 */}
|
||
<View className="bg-white mt-3 p-4 mb-4">
|
||
<Text className="text-base font-medium text-gray-800 mb-3 block">拼团规则</Text>
|
||
<View className="flex flex-col" style={{ gap: '8px' }}>
|
||
<Text className="text-sm text-gray-500">• 支付开团或参加拼团后,需在24小时内凑齐人数</Text>
|
||
<Text className="text-sm text-gray-500">• 拼团失败,系统会自动退款</Text>
|
||
<Text className="text-sm text-gray-500">• 拼团商品享受拼团价格,不支持7天无理由退货</Text>
|
||
</View>
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{/* 底部操作栏 */}
|
||
<View className="bg-white border-t border-gray-100 px-4 py-3 flex items-center justify-between">
|
||
<View
|
||
className="flex-1 mr-2 py-3 rounded-full text-center text-sm font-medium border-2"
|
||
style={{ borderColor: '#ef4444', color: '#ef4444' }}
|
||
onClick={handleCreateGroup}
|
||
>
|
||
<Text>单独购买</Text>
|
||
</View>
|
||
<View
|
||
className="flex-1 py-3 rounded-full text-white text-sm font-medium text-center"
|
||
style={{ backgroundColor: isActive ? '#ef4444' : '#d1d5db' }}
|
||
onClick={isActive ? handleCreateGroup : undefined}
|
||
>
|
||
<Text>{submitting ? '处理中...' : '立即拼团'}</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* SKU 选择器 */}
|
||
<SkuSelector
|
||
visible={skuVisible}
|
||
product={product}
|
||
mode="buy"
|
||
onClose={() => setSkuVisible(false)}
|
||
onConfirm={handleSkuConfirm}
|
||
/>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default GroupBuyDetailPage
|