Files
xinlong-shop-taro/src_bak/pages/seckill-detail.tsx
赵忠林 1fa58040f3 feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
2026-07-01 12:11:56 +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