Files
xinlong-shop-taro/src/pages/seckill-detail.tsx
赵忠林 e72ca89b2b feat(user): 完善提现规则展示与秒杀购买交互
- 新增提现规则卡片,展示提现额度、最低提现、单次限额、每日限额、每日提现次数、提现时间、到账时间、手续费等
- 添加提现规则说明文字,提示审核时间、提现次数限制、金额范围和提现时间段
- 优化秒杀详情和秒杀页面购买按钮交互,增加秒杀状态提示(未开始、已结束、已抢光)
- 统一秒杀购买事件处理函数名为 handleBuyClick,取消按钮条件绑定,增强点击响应体验
- 新增提现规则相关样式,确保提现规则卡和说明区块视觉效果一致
2026-06-23 16:52:21 +08:00

295 lines
9.6 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 { getShopSeckill, createSeckillOrder } from '@/api/shop/shopSeckill'
import { getShopGoods } from '@/api/shop/shopGoods'
import type { ShopSeckill } from '@/api/shop/shopSeckill/model'
import type { ShopGoods } from '@/api/shop/shopGoods/model'
import type { ShopGoodsSku } from '@/api/shop/shopGoodsSku/model'
import SkuSelector from '@/components/business/SkuSelector'
definePageConfig({
navigationBarTitleText: '秒杀详情',
})
const SeckillDetailPage: React.FC = () => {
const router = useRouter()
const seckillId = Number(router.params.seckillId || 0)
const [seckill, setSeckill] = useState<ShopSeckill | null>(null)
const [product, setProduct] = useState<ShopGoods | null>(null)
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [currentTime, setCurrentTime] = useState(Date.now())
const [countdown, setCountdown] = useState('')
const [skuVisible, setSkuVisible] = useState(false)
useEffect(() => {
if (seckillId) {
fetchData()
}
const timer = setInterval(() => {
setCurrentTime(Date.now())
}, 1000)
return () => clearInterval(timer)
}, [seckillId])
useEffect(() => {
if (seckill) {
updateCountdown()
}
}, [currentTime, seckill])
const fetchData = async () => {
try {
setLoading(true)
const res = await getShopSeckill(seckillId)
if (res.code === 0 && res.data) {
const data = res.data
setSeckill(data)
// 加载商品完整信息(含规格/SKU
if (data.goodsId) {
fetchGoodsDetail(data.goodsId)
}
}
} 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 updateCountdown = () => {
if (!seckill?.endTime) return
const end = new Date(seckill.endTime).getTime()
const diff = end - currentTime
if (diff <= 0) {
setCountdown('已结束')
return
}
const hours = Math.floor(diff / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
setCountdown(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`)
}
const handleBuyClick = () => {
if (!seckill) return
if (seckill.status === 0) {
Taro.showToast({ title: '活动尚未开始,请耐心等待', icon: 'none' })
return
}
if (seckill.status === 2 || new Date(seckill.endTime).getTime() <= Date.now()) {
Taro.showToast({ title: '活动已结束,下次再来', icon: 'none' })
return
}
if (seckill.stock <= 0) {
Taro.showToast({ title: '已抢光,下次再来', icon: 'none' })
return
}
if (submitting) return
// 先拉取商品规格(如果没有的话)
if (!product && seckill.goodsId) {
Taro.showLoading({ title: '加载规格...' })
getShopGoods(seckill.goodsId)
.then(res => {
Taro.hideLoading()
if (res.code === 0 && res.data) {
setProduct(res.data)
setSkuVisible(true)
} else {
// 没有规格,直接下单
handleSeckillOrder(0, 1)
}
})
.catch(() => {
Taro.hideLoading()
handleSeckillOrder(0, 1)
})
} else {
setSkuVisible(true)
}
}
const handleSeckillOrder = async (skuId: number, quantity: number) => {
if (!seckill) return
setSubmitting(true)
try {
const res = await createSeckillOrder({
seckillId: seckill.id!,
goodsId: seckill.goodsId,
skuId,
quantity,
})
if (res.code === 0) {
Taro.showToast({ title: '抢购成功', icon: 'success' })
fetchData() // 刷新库存
}
} catch (err: any) {
Taro.showToast({ title: err?.message || '抢购失败', icon: 'none' })
} finally {
setSubmitting(false)
}
}
const handleSkuConfirm = (sku: ShopGoodsSku, quantity: number) => {
handleSeckillOrder(sku.id || 0, quantity)
}
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 (!seckill) {
return (
<View className="min-h-screen bg-gray-50 flex items-center justify-center">
<Text className="text-gray-400 text-sm"></Text>
</View>
)
}
const progress = seckill.stock === 0 ? 100 : Math.round((seckill.soldCount / (seckill.soldCount + seckill.stock)) * 100)
const isActive = seckill.status === 1 && seckill.stock > 0
return (
<View className="min-h-screen bg-gray-50 pb-20">
<ScrollView scrollY className="h-screen">
{/* 商品图片 */}
<View className="w-full h-72 bg-gray-100 flex items-center justify-center">
{seckill.goodsImage || seckill.product?.image ? (
<Image
className="w-full h-full"
src={seckill.goodsImage || seckill.product?.image}
mode="aspectFill"
/>
) : (
<Text className="text-gray-400"></Text>
)}
</View>
{/* 价格和倒计时 */}
<View className="px-4 py-4" style={{ background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' }}>
<View className="flex items-baseline gap-2">
<Text className="text-3xl font-bold text-white">
¥{seckill.seckillPrice}
</Text>
<Text className="text-sm text-red-200 line-through">
¥{seckill.product?.price || 0}
</Text>
</View>
<View className="flex items-center justify-between mt-3">
<View className="bg-white rounded px-2 py-1">
<Text className="text-red-500 text-sm font-bold">{countdown}</Text>
</View>
<Text className="text-white text-xs"></Text>
</View>
</View>
{/* 商品信息 */}
<View className="bg-white p-4">
<Text className="text-base text-gray-800 font-medium block">
{seckill.goodsName || seckill.product?.name || '商品名称'}
</Text>
<View className="flex items-center gap-2 mt-2">
<View className="px-2 py-1 rounded text-xs" style={{ backgroundColor: '#fef2f2', color: '#ef4444' }}>
{seckill.limitPerUser}
</View>
<Text className="text-xs text-gray-400">
{progress}%
</Text>
{isActive && <Text className="text-xs text-red-500 font-medium"></Text>}
{!isActive && <Text className="text-xs text-gray-400">{seckill.status === 0 ? '未开始' : '已结束'}</Text>}
</View>
<View className="mt-3 bg-gray-100 rounded-full h-2 overflow-hidden">
<View
className="h-full rounded-full"
style={{
width: `${progress}%`,
backgroundColor: '#ef4444',
}}
/>
</View>
<View className="mt-2 flex justify-between">
<Text className="text-xs text-gray-400"> {seckill.stock} </Text>
<Text className="text-xs text-gray-400"> {seckill.soldCount} </Text>
</View>
</View>
{/* 抢购提示 */}
<View className="bg-white mt-3 p-4">
<Text className="text-sm font-medium text-gray-800 mb-2 block"></Text>
<View className="flex flex-col" style={{ gap: '6px' }}>
<Text className="text-sm text-gray-500"> {seckill.limitPerUser}/</Text>
<Text className="text-sm text-gray-500"> 退</Text>
<Text className="text-sm text-gray-500"> 15</Text>
</View>
</View>
{/* 商品详情 */}
{seckill.product?.content && (
<View className="bg-white mt-3 p-4">
<Text className="text-sm font-medium text-gray-800 mb-3 block"></Text>
<View dangerouslySetInnerHTML={{ __html: seckill.product.content }} />
</View>
)}
</ScrollView>
{/* 底部抢购按钮 */}
<View className="bg-white border-t border-gray-100 px-4 py-3">
<View
className="w-full py-3 rounded-full text-white text-center text-base font-medium"
style={{
backgroundColor: isActive && !submitting ? '#ef4444' : '#d1d5db',
opacity: submitting ? 0.7 : 1,
}}
onClick={handleBuyClick}
>
<Text>
{submitting ? '抢购中...' : isActive ? (seckill.stock > 0 ? '立即抢购' : '已抢光') : seckill.status === 0 ? '即将开始' : '已结束'}
</Text>
</View>
</View>
{/* SKU 选择器 */}
<SkuSelector
visible={skuVisible}
product={product}
mode="buy"
onClose={() => setSkuVisible(false)}
onConfirm={handleSkuConfirm}
/>
</View>
)
}
export default SeckillDetailPage