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

324 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'
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={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