feat(user): 新增收货地址管理及售后申请页面

- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
2026-07-01 12:11:56 +08:00
parent bf6ed504cc
commit 1fa58040f3
636 changed files with 58878 additions and 716 deletions

View File

@@ -0,0 +1,185 @@
import React, { useEffect, useMemo } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import type { ShopUserCoupon } from '@/api/shop/shopUserCoupon/model'
interface CouponSelectProps {
visible: boolean
coupons: ShopUserCoupon[]
minAmount?: number
selectedCouponId?: string
onClose: () => void
onSelect: (coupon: ShopUserCoupon | null) => void
}
const CouponSelect: React.FC<CouponSelectProps> = ({
visible,
coupons,
minAmount = 0,
selectedCouponId,
onClose,
onSelect,
}) => {
const [showOverlay, setShowOverlay] = React.useState(false)
const [slideUp, setSlideUp] = React.useState(false)
// 控制动画
React.useEffect(() => {
if (visible) {
setShowOverlay(true)
setTimeout(() => setSlideUp(true), 10)
} else {
setSlideUp(false)
setTimeout(() => setShowOverlay(false), 300)
}
}, [visible])
const handleClose = () => {
setSlideUp(false)
setTimeout(() => {
setShowOverlay(false)
onClose()
}, 300)
}
const handleSelect = (coupon: ShopUserCoupon | null) => {
handleClose()
onSelect(coupon)
}
const availableCoupons = useMemo(() => {
return coupons.filter(c => {
if (c.status !== 0 && c.status !== undefined) return false
if (c.endTime && new Date(c.endTime) < new Date()) return false
return (Number(c.minPrice) || 0) <= minAmount
})
}, [coupons, minAmount])
if (!showOverlay) return null
return (
<View className='coupon-select-overlay'>
{/* 遮罩层 */}
<View
className={`absolute inset-0 bg-black/50 z-50 ${slideUp ? 'opacity-100' : 'opacity-0'}`}
onClick={handleClose}
/>
{/* 弹窗内容 */}
<View
className={`absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl z-50 ${slideUp ? '' : 'hidden'}`}
style={{ maxHeight: '60vh' }}
>
<View className='p-4'>
{/* 关闭按钮 */}
<View className='flex justify-end mb-2'>
<View
className='w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center'
onClick={handleClose}
>
<Text className='text-gray-400 text-sm'>×</Text>
</View>
</View>
<View className='flex justify-between items-center mb-4'>
<Text className='text-base font-medium'></Text>
<Text
className='text-sm text-green-600'
onClick={() => handleSelect(null)}
>
使
</Text>
</View>
{availableCoupons.length === 0 ? (
<View className='py-8 text-center'>
<Text className='text-sm text-gray-400'></Text>
</View>
) : (
<ScrollView scrollY style={{ maxHeight: '50vh' }}>
{availableCoupons.map((coupon) => {
const isSelected = selectedCouponId === coupon.id
return (
<View
key={coupon.id}
className='flex rounded-lg border mb-2 overflow-hidden'
style={{
borderColor: isSelected ? (
coupon.type === 40 ? '#3b82f6' :
coupon.type === 50 ? '#06b6d4' :
'#0e932e'
) : '#f0f0f0',
backgroundColor: isSelected ? (
coupon.type === 40 ? '#eff6ff' :
coupon.type === 50 ? '#ecfeff' :
'#f0fdf4'
) : '#fff',
}}
onClick={() => handleSelect(coupon)}
>
<View className='w-24 flex flex-col items-center justify-center py-3' style={{
backgroundColor: coupon.type === 40 ? '#eff6ff' :
coupon.type === 50 ? '#ecfeff' :
'#f0fdf4'
}}>
{coupon.type === 20 ? (
<View className='text-center'>
<Text className='text-lg font-bold text-green-600 block'>
{coupon.discount || 0}
</Text>
<Text className='text-xs text-gray-500'>
{coupon.minPrice && Number(coupon.minPrice) > 0 ? `${coupon.minPrice}可用` : '无门槛'}
</Text>
</View>
) : coupon.type === 50 ? (
<View className='text-center'>
<Text className='text-lg font-bold text-teal-600 block'>
{coupon.useCount && coupon.useCount > 0 ? `${coupon.useCount}` : '不限次'}
</Text>
<Text className='text-xs text-gray-500'>
{coupon.useDuration && coupon.useDuration > 0 ? `${coupon.useDuration}分钟` : '场地使用'}
</Text>
</View>
) : (
<View className='text-center'>
<Text className='text-xs font-medium block' style={{
color: coupon.type === 40 ? '#3b82f6' : '#16a34a'
}}>¥</Text>
<Text className='text-xl font-bold block' style={{
color: coupon.type === 40 ? '#3b82f6' : '#16a34a'
}}>
{coupon.reducePrice || 0}
</Text>
<Text className='text-xs text-gray-500'>
{coupon.type === 40 ? '无门槛' :
(coupon.minPrice && Number(coupon.minPrice) > 0 ? `${coupon.minPrice}可用` : '无门槛')}
</Text>
</View>
)}
</View>
<View className='flex-1 p-3 flex flex-col justify-between'>
<Text className='text-sm text-gray-800 font-medium'>
{coupon.name || coupon.description || '优惠券'}
</Text>
<Text className='text-xs text-gray-400'>
{coupon.startTime?.slice(0, 10)} ~ {coupon.endTime?.slice(0, 10)}
</Text>
</View>
{isSelected && (
<View className='flex items-center pr-3'>
<Text className='text-green-500 text-lg'>{'✓'}</Text>
</View>
)}
</View>
)
})}
</ScrollView>
)}
</View>
</View>
</View>
)
}
export default CouponSelect