Files
xinlong-shop-taro/src/pages/group-buy-detail.tsx
赵忠林 082646a613 feat(image): 全项目应用图片压缩优化
- 将图片显示统一替换为 getCompressedImageUrl 函数处理压缩
- 覆盖 35 个文件,包括 4 个组件和 31 个页面
- 主要组件:LazyImage、ProductCard、OrderCard、SkuSelector 内部自动压缩图片
- 主要页面:购物车、订单、积分、活动、拼团、秒杀、收藏、浏览历史等
- 跳过用户头像、二维码、商品详情大图及所有 Taro.previewImage 调用
- 调整压缩默认宽度为 300,保持质量90,启用压缩功能
2026-07-06 12:21:39 +08:00

325 lines
11 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, { 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