- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
186 lines
6.8 KiB
TypeScript
186 lines
6.8 KiB
TypeScript
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
|