- 个人信息页新增门店名称和门店地址展示,支持异步加载与只读显示 - 用户信息卡片整体改为绿色渐变背景,添加光晕和白色边框样式 - 新增升级VIP会员入口,突出展示并添加推荐标签 - 新建VIP会员升级页面,包含门店信息表单和VIP权益预览 - 提交升级申请调用新增api,支持状态检测表单只读 - 门店中心页面重构,简化订单管理,新增功能卡片及VIP审核入口 - 页面加载校验店员身份,非店员拒绝访问并提示 - 购物相关页面和组件全面支持VIP会员价格优先显示dealerPrice - 新增ShopDealerApply模型门店相关字段,丰富会员申请数据记录
589 lines
23 KiB
TypeScript
589 lines
23 KiB
TypeScript
import React, { useState, useEffect, useMemo } from 'react'
|
||
import { View, Text, Image, ScrollView, Input } from '@tarojs/components'
|
||
import Taro, { useRouter, useDidShow } from '@tarojs/taro'
|
||
import { useCartContext } from '@/contexts/CartContext'
|
||
import { useAddress } from '@/hooks/useAddress'
|
||
import { useCoupon } from '@/hooks/useCoupon'
|
||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||
import { getMyAvailableCoupons } from '@/api/shop/shopUserCoupon'
|
||
import AddressCard from '@/components/business/AddressCard'
|
||
import CouponCard from '@/components/business/CouponCard'
|
||
import { createOrder } from '@/api/shop/shopOrder'
|
||
import { listShopGoods } from '@/api/shop/shopGoods'
|
||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||
import type { ShopUserCoupon } from '@/api/shop/shopUserCoupon/model'
|
||
import type { OrderGoodsItem, OrderCreateRequest } from '@/api/shop/shopOrder/model'
|
||
import { isVipMember } from '@/utils/vip'
|
||
|
||
// 满减门槛配置
|
||
const THRESHOLDS = [
|
||
{ target: 99, discount: 10 },
|
||
{ target: 199, discount: 20 },
|
||
{ target: 299, discount: 30 },
|
||
{ target: 499, discount: 50 },
|
||
]
|
||
|
||
// 解析可能为JSON字符串的规格值
|
||
const parseSpecValue = (value: string | undefined): string => {
|
||
if (!value) return ''
|
||
const trimmed = value.trim()
|
||
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
|
||
try {
|
||
return JSON.parse(trimmed)
|
||
} catch {
|
||
return trimmed.slice(1, -1)
|
||
}
|
||
}
|
||
return trimmed
|
||
}
|
||
|
||
// 格式化规格信息,展示规格名称和值
|
||
const formatSpecInfo = (item: any): { specList: Array<{ name: string; value: string }>; specText: string } => {
|
||
const specList: Array<{ name: string; value: string }> = []
|
||
|
||
// 防御性检查:确保 goodsSpecs 是数组
|
||
const goodsSpecs = item.product?.goodsSpecs
|
||
if (Array.isArray(goodsSpecs) && goodsSpecs.length > 0) {
|
||
const specGroups = new Map<number, { name: string; values: string[] }>()
|
||
|
||
goodsSpecs.forEach((spec: any) => {
|
||
const specId = spec?.specId || 0
|
||
if (!specGroups.has(specId)) {
|
||
specGroups.set(specId, {
|
||
name: spec?.specName || `规格${specGroups.size + 1}`,
|
||
values: []
|
||
})
|
||
}
|
||
const group = specGroups.get(specId)
|
||
if (group) {
|
||
group.values.push(parseSpecValue(spec?.specValue))
|
||
}
|
||
})
|
||
|
||
// 2. 如果选择了 SKU,根据 sku.sku 匹配规格值
|
||
const skuStr = item.sku?.sku
|
||
if (typeof skuStr === 'string' && skuStr) {
|
||
const selectedValues = skuStr.split('|').map((v: string) => parseSpecValue(v.trim())).sort()
|
||
|
||
specGroups.forEach((group) => {
|
||
// 防御性检查:确保 values 是数组
|
||
if (!Array.isArray(group.values)) return
|
||
// 查找匹配的规格值
|
||
const matchedValue = group.values.find(v => selectedValues.includes(v))
|
||
if (matchedValue) {
|
||
specList.push({ name: group.name, value: matchedValue })
|
||
}
|
||
})
|
||
}
|
||
|
||
// 如果没有 SKU 但有 skuSpec 字符串
|
||
if (specList.length === 0 && item.skuSpec) {
|
||
const values = String(item.skuSpec).split(',').map(v => v.trim()).filter(Boolean)
|
||
let idx = 0
|
||
specGroups.forEach((group) => {
|
||
if (values[idx]) {
|
||
specList.push({ name: group.name, value: values[idx] })
|
||
}
|
||
idx++
|
||
})
|
||
}
|
||
}
|
||
|
||
// 3. 如果没有完整的规格定义,但有 skuSpec 字符串
|
||
if (specList.length === 0 && item.skuSpec) {
|
||
const parts = String(item.skuSpec).split(/[,,]/).map(v => v.trim()).filter(Boolean)
|
||
if (parts.length > 0) {
|
||
specList.push({ name: '规格', value: parts.join(', ') })
|
||
}
|
||
}
|
||
|
||
return {
|
||
specList,
|
||
specText: specList.map(s => `${s.name}: ${s.value}`).join(' | ') || item.skuSpec || ''
|
||
}
|
||
}
|
||
|
||
definePageConfig({
|
||
navigationBarTitleText: '确认订单',
|
||
})
|
||
|
||
const CheckoutPage: React.FC = () => {
|
||
const router = useRouter()
|
||
const fromBuyNow = router.params.from === 'buyNow'
|
||
|
||
const { selectedItems, refresh: refreshCart, removeSelected } = useCartContext()
|
||
const { defaultAddress, loadAddresses } = useAddress()
|
||
const maxPopupHeight = useScrollHeight(0)
|
||
// 底部提交栏高度约 60px,动态计算 ScrollView 可用高度
|
||
const scrollHeight = useScrollHeight(44)
|
||
const [coupons, setCoupons] = useState<ShopUserCoupon[]>([])
|
||
const [selectedCoupon, setSelectedCoupon] = useState<ShopUserCoupon | null>(null)
|
||
const [couponVisible, setCouponVisible] = useState(false)
|
||
const [remarks, setRemarks] = useState('')
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [rushBuyProducts, setRushBuyProducts] = useState<ShopGoods[]>([])
|
||
|
||
// 支付方式相关状态
|
||
const [payType, setPayType] = useState<number>(0) // 0: 货到付款
|
||
|
||
// 从本地存储获取立即购买的数据
|
||
const buyNowItems = useMemo(() => {
|
||
if (fromBuyNow) {
|
||
const data = Taro.getStorageSync('buy_now')
|
||
if (data) {
|
||
try {
|
||
const parsed = JSON.parse(data)
|
||
return Array.isArray(parsed) ? parsed : []
|
||
} catch { /* ignore */ }
|
||
}
|
||
}
|
||
return null
|
||
}, [fromBuyNow])
|
||
|
||
// 使用的商品列表
|
||
const items = buyNowItems || selectedItems
|
||
|
||
// 计算金额
|
||
const goodsPrice = useMemo(() => {
|
||
if (!items || items.length === 0) return 0
|
||
const vip = isVipMember()
|
||
return items.reduce((sum, item) => {
|
||
// VIP 会员优先使用 dealerPrice
|
||
const dealerPrice = vip ? (item.product as any)?.dealerPrice : null
|
||
const unitPrice = dealerPrice || item.skuPrice || item.sku?.price || item.product?.salePrice || item.product?.price || 0
|
||
return sum + Number(unitPrice) * (item.quantity || item.num || 1)
|
||
}, 0)
|
||
}, [buyNowItems, selectedItems])
|
||
|
||
const couponDiscount = selectedCoupon?.reducePrice ? Number(selectedCoupon.reducePrice) : 0
|
||
const totalPrice = Math.max(0, goodsPrice - couponDiscount).toFixed(2)
|
||
|
||
// 计算凑单信息
|
||
const rushBuyInfo = useMemo(() => {
|
||
if (goodsPrice === 0) return null
|
||
|
||
// 找出最近的满减门槛
|
||
for (const threshold of THRESHOLDS) {
|
||
if (goodsPrice < threshold.target) {
|
||
const needAmount = threshold.target - goodsPrice
|
||
// 只在需要凑的金额在 5-80 元之间时显示推荐
|
||
if (needAmount <= 80) {
|
||
return {
|
||
target: threshold.target,
|
||
discount: threshold.discount,
|
||
needAmount: needAmount.toFixed(2)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return null
|
||
}, [goodsPrice])
|
||
|
||
// 加载优惠券和凑单商品
|
||
useEffect(() => {
|
||
loadCoupons()
|
||
loadRushBuyProducts()
|
||
}, [])
|
||
|
||
// 每次页面显示时刷新地址(从地址列表选择后返回)
|
||
useDidShow(() => {
|
||
loadAddresses()
|
||
})
|
||
|
||
const loadCoupons = async () => {
|
||
try {
|
||
const data = await getMyAvailableCoupons()
|
||
setCoupons(data || [])
|
||
} catch (e) {
|
||
console.error('加载优惠券失败:', e)
|
||
// 不设置任何内容,让页面继续显示
|
||
}
|
||
}
|
||
|
||
// 加载凑单推荐商品
|
||
const loadRushBuyProducts = async () => {
|
||
try {
|
||
// 获取热销/推荐商品
|
||
const data = await listShopGoods({ page: 1, limit: 10, isShow: 1 })
|
||
// 防御性检查:确保返回的是数组
|
||
const safeData = Array.isArray(data) ? data : []
|
||
if (safeData.length > 0) {
|
||
// 过滤掉已在购物车的商品
|
||
const currentItems = buyNowItems || selectedItems
|
||
const cartGoodsIds = Array.isArray(currentItems) ? currentItems.map(item => item.goodsId) : []
|
||
const filtered = safeData.filter(p => !cartGoodsIds.includes(p.goodsId))
|
||
setRushBuyProducts(filtered.slice(0, 4))
|
||
}
|
||
} catch (e) {
|
||
console.error('加载凑单商品失败:', e)
|
||
// 不设置任何内容,让页面继续显示
|
||
}
|
||
}
|
||
|
||
// 选择地址
|
||
const handleSelectAddress = () => {
|
||
Taro.navigateTo({
|
||
url: '/pages/user/address-list?from=checkout',
|
||
events: {
|
||
refresh: () => loadAddresses()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 选择优惠券
|
||
const handleSelectCoupon = () => {
|
||
setCouponVisible(true)
|
||
}
|
||
|
||
const handleCouponConfirm = (coupon: ShopUserCoupon | null) => {
|
||
setSelectedCoupon(coupon)
|
||
setCouponVisible(false)
|
||
}
|
||
|
||
// 提交订单
|
||
const handleSubmit = async () => {
|
||
if (!defaultAddress) {
|
||
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
if (items.length === 0) {
|
||
Taro.showToast({ title: '请选择商品', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
setSubmitting(true)
|
||
try {
|
||
// 构建商品列表
|
||
const goodsItems: OrderGoodsItem[] = items.map(item => ({
|
||
goodsId: item.goodsId,
|
||
skuId: item.skuId && item.skuId > 0 ? item.skuId : undefined,
|
||
quantity: item.quantity || item.num || 1,
|
||
specInfo: item.skuSpec || item.sku?.sku || item.product?.specName,
|
||
}))
|
||
|
||
// 格式化当前时间:yyyy-MM-dd HH:mm:ss(与后端 LocalDateTime 兼容)
|
||
const now = new Date()
|
||
const pad = (n: number) => n.toString().padStart(2, '0')
|
||
const payTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
||
|
||
// 创建订单
|
||
// 货到付款:直接设置 payStatus=true + payTime,防止后端定时任务自动删除未付款订单
|
||
const orderParams: OrderCreateRequest = {
|
||
goodsItems,
|
||
addressId: defaultAddress.id,
|
||
payType,
|
||
payStatus: true,
|
||
payTime,
|
||
couponId: selectedCoupon?.id ? Number(selectedCoupon.id) : undefined,
|
||
comments: remarks,
|
||
deliveryType: 0, // 快递配送
|
||
}
|
||
|
||
const res = await createOrder(orderParams)
|
||
|
||
// 清理数据
|
||
if (fromBuyNow) {
|
||
Taro.removeStorageSync('buy_now')
|
||
} else {
|
||
await removeSelected()
|
||
}
|
||
|
||
// 显示成功提示
|
||
Taro.showToast({ title: '订单已提交', icon: 'success' })
|
||
setTimeout(() => {
|
||
Taro.switchTab({ url: '/pages/order/list' })
|
||
}, 1500)
|
||
} catch (err: any) {
|
||
Taro.showToast({ title: err.message || '提交失败', icon: 'none' })
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
// 安全的数字格式化
|
||
const formatPrice = (price: number | string): string => {
|
||
const num = typeof price === 'string' ? parseFloat(price) : price
|
||
if (isNaN(num)) return '0.00'
|
||
return num.toFixed(2)
|
||
}
|
||
|
||
return (
|
||
<View className='bg-gray-50 flex flex-col' style={{ height: '100vh' }}>
|
||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||
<View className='p-3'>
|
||
{/* 地址 */}
|
||
{defaultAddress ? (
|
||
<AddressCard
|
||
address={defaultAddress}
|
||
onClick={handleSelectAddress}
|
||
/>
|
||
) : (
|
||
<View className='bg-white rounded-lg p-3' onClick={handleSelectAddress}>
|
||
<View className='flex items-center justify-between'>
|
||
<View className='flex items-center gap-2'>
|
||
<Text className='text-gray-400 text-sm'>📍</Text>
|
||
<View>
|
||
<Text className='text-sm font-medium text-gray-800 block'>请选择收货地址</Text>
|
||
</View>
|
||
</View>
|
||
<Text className='text-gray-400 text-sm'>›</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 商品列表 */}
|
||
<View className='bg-white rounded-lg mt-3 p-3'>
|
||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>
|
||
商品 ({(items || []).length})
|
||
</Text>
|
||
{items && items.length > 0 ? (
|
||
items.map((item, idx) => {
|
||
const { specList } = formatSpecInfo(item)
|
||
return (
|
||
<View key={`${item.goodsId}-${item.skuId}-${idx}`} className='flex gap-3 py-3 border-b border-gray-50'>
|
||
<Image
|
||
className='w-16 h-16 rounded-md bg-gray-100'
|
||
src={item.goodsImage || item.product?.image || item.sku?.image || ''}
|
||
mode='aspectFill'
|
||
/>
|
||
<View className='flex-1'>
|
||
<Text className='text-sm text-gray-700 block line-clamp-2'>
|
||
{item.goodsName || item.product?.name || item.product?.goodsName}
|
||
</Text>
|
||
{/* 多规格属性展示 */}
|
||
{specList.length > 0 ? (
|
||
<View className='mt-1'>
|
||
{specList.map((spec, specIdx) => (
|
||
<View key={specIdx} className='flex items-center text-xs'>
|
||
<Text className='text-gray-400'>{spec.name}:</Text>
|
||
<Text className='text-gray-600 ml-1'>{spec.value}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
) : item.skuSpec ? (
|
||
<Text className='text-xs text-gray-400 mt-1'>{item.skuSpec}</Text>
|
||
) : null}
|
||
<View className='flex justify-between items-center mt-1'>
|
||
<Text className='text-xs text-gray-500'>x{item.quantity || item.num || 1}</Text>
|
||
<Text className='text-sm font-medium text-gray-800'>
|
||
¥{isVipMember() && (item.product as any)?.dealerPrice ? (item.product as any).dealerPrice : (item.skuPrice || item.sku?.salePrice || item.sku?.price || item.product?.salePrice || item.product?.price || '0')}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)
|
||
})
|
||
) : (
|
||
<View className='py-10 text-center'>
|
||
<Text className='text-gray-400 text-sm'>购物车为空,请先添加商品</Text>
|
||
<View
|
||
className='mt-3 px-6 py-2 rounded-full text-white text-sm inline-block'
|
||
style={{ backgroundColor: '#0e932e' }}
|
||
onClick={() => Taro.switchTab({ url: '/pages/shop/index' })}
|
||
>
|
||
去购物
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 优惠券 */}
|
||
<View className='bg-white rounded-lg mt-3 p-3' onClick={handleSelectCoupon}>
|
||
<View className='flex justify-between items-center'>
|
||
<Text className='text-sm text-gray-700'>优惠券</Text>
|
||
<View className='flex items-center gap-2'>
|
||
{selectedCoupon ? (
|
||
<Text className='text-sm text-red-500'>-{selectedCoupon.reducePrice}元</Text>
|
||
) : coupons.length > 0 ? (
|
||
<Text className='text-sm text-gray-400'>有{coupons.length}张可用</Text>
|
||
) : (
|
||
<Text className='text-sm text-gray-400'>暂无可用</Text>
|
||
)}
|
||
<Text className='text-gray-400'>›</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 备注 */}
|
||
<View className='bg-white rounded-lg mt-3 p-3'>
|
||
<View className='flex justify-between items-center'>
|
||
<Text className='text-sm text-gray-700'>订单备注</Text>
|
||
<Text className='text-xs text-gray-400'>选填</Text>
|
||
</View>
|
||
<View className='mt-2'>
|
||
<Input
|
||
className='bg-gray-50 rounded-lg px-3 py-2 text-sm'
|
||
placeholder='点击添加备注...'
|
||
value={remarks}
|
||
onInput={(e: any) => setRemarks(e.detail.value)}
|
||
maxlength={200}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 金额明细 */}
|
||
<View className='bg-white rounded-lg mt-3 p-3'>
|
||
<View className='flex justify-between mb-2'>
|
||
<Text className='text-sm text-gray-500'>商品合计</Text>
|
||
<Text className='text-sm text-gray-700'>¥{formatPrice(goodsPrice)}</Text>
|
||
</View>
|
||
<View className='flex justify-between mb-2'>
|
||
<Text className='text-sm text-gray-500'>运费</Text>
|
||
<Text className='text-sm text-green-600'>免运费</Text>
|
||
</View>
|
||
{couponDiscount > 0 && (
|
||
<View className='flex justify-between mb-2'>
|
||
<Text className='text-sm text-gray-500'>优惠券</Text>
|
||
<Text className='text-sm text-red-500'>-¥{formatPrice(couponDiscount)}</Text>
|
||
</View>
|
||
)}
|
||
<View className='flex justify-between pt-2 border-t border-gray-100'>
|
||
<Text className='text-sm font-medium'>实付金额</Text>
|
||
<Text className='text-lg font-bold text-red-500'>¥{totalPrice}</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 支付方式选择 */}
|
||
<View className='bg-white rounded-lg mt-3 p-3'>
|
||
<Text className='text-sm font-medium text-gray-800 mb-3 block'>支付方式</Text>
|
||
|
||
{/* 货到付款 */}
|
||
<View
|
||
className='flex items-center py-3 px-2 rounded-lg'
|
||
style={{ backgroundColor: '#f0fdf4' }}
|
||
>
|
||
<View className='w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center mr-3'>
|
||
<Text className='text-sm'>货</Text>
|
||
</View>
|
||
<View className='flex-1'>
|
||
<Text className='text-sm text-gray-800 block'>货到付款</Text>
|
||
<Text className='text-xs text-gray-400 block'>送达时支付</Text>
|
||
</View>
|
||
<View
|
||
className='w-5 h-5 rounded-full border-2 flex items-center justify-center'
|
||
style={{ borderColor: '#22c55e' }}
|
||
>
|
||
<View className='w-2 h-2 rounded-full' style={{ backgroundColor: '#22c55e' }} />
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 凑单推荐 */}
|
||
{rushBuyInfo && rushBuyProducts.length > 0 && (
|
||
<View className='rounded-lg mt-3 p-3 border border-red-100' style={{ background: 'linear-gradient(to right, #fef2f2, #fff7ed)', display: 'none' }}>
|
||
<View className='flex items-center justify-between mb-3'>
|
||
<View className='flex items-center gap-2'>
|
||
<Text className='text-red-500 text-lg'>🎯</Text>
|
||
<Text className='text-sm font-medium text-red-600'>
|
||
再买 ¥{rushBuyInfo.needAmount} 减 ¥{rushBuyInfo.discount}
|
||
</Text>
|
||
</View>
|
||
<Text className='text-xs text-gray-400'>凑单更优惠</Text>
|
||
</View>
|
||
<ScrollView scrollX className='flex-row'>
|
||
<View className='flex flex-row gap-2'>
|
||
{rushBuyProducts.map(product => (
|
||
<View
|
||
key={product.goodsId}
|
||
className='w-24 bg-white rounded-lg p-2'
|
||
onClick={() => {
|
||
Taro.navigateTo({
|
||
url: `/pages/shop/product-detail?id=${product.goodsId}&from=rushbuy`
|
||
})
|
||
}}
|
||
>
|
||
<Image
|
||
className='w-full h-20 rounded bg-gray-100'
|
||
src={product.image || ''}
|
||
mode='aspectFill'
|
||
/>
|
||
<Text className='text-xs text-gray-700 block line-clamp-1 mt-1'>
|
||
{product.name}
|
||
</Text>
|
||
<View className='flex items-center justify-between mt-1'>
|
||
<Text className='text-xs font-medium text-red-500'>
|
||
¥{product.salePrice || product.price}
|
||
</Text>
|
||
<Text className='text-xs text-gray-400'>+加购</Text>
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</ScrollView>
|
||
</View>
|
||
)}
|
||
{/* 底部留白(固定栏高度 + 安全区) */}
|
||
<View style={{ height: '80px' }} />
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{/* 底部提交栏 - 固定吸底 */}
|
||
<View
|
||
className='bg-white border-t border-gray-100 px-3'
|
||
style={{
|
||
position: 'fixed',
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
paddingBottom: 'env(safe-area-inset-bottom)',
|
||
zIndex: 100,
|
||
}}
|
||
>
|
||
<View className='flex items-center justify-between py-3'>
|
||
<View className='flex items-center gap-2'>
|
||
<Text className='text-sm text-gray-500'>合计:</Text>
|
||
<Text className='text-lg font-bold text-red-500'>¥{totalPrice}</Text>
|
||
</View>
|
||
<View
|
||
className='px-8 py-2 rounded-full text-white text-sm font-medium'
|
||
style={{ backgroundColor: submitting ? '#ccc' : '#0e932e' }}
|
||
onClick={submitting ? undefined : handleSubmit}
|
||
>
|
||
<Text>{submitting ? '提交中...' : '提交订单'}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 优惠券选择弹窗 */}
|
||
{couponVisible && (
|
||
<View className='absolute flex items-end' style={{ top: 0, right: 0, bottom: 0, left: 0, backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 110 }}>
|
||
<View className='w-full bg-white rounded-t-xl overflow-hidden' style={{ maxHeight: maxPopupHeight }}>
|
||
<View className='p-4 border-b border-gray-100 flex justify-between items-center'>
|
||
<Text className='text-base font-medium'>选择优惠券</Text>
|
||
<Text className='text-gray-400' onClick={() => setCouponVisible(false)}>✕</Text>
|
||
</View>
|
||
<ScrollView scrollY className='flex-1'>
|
||
<View className='p-3'>
|
||
{/* 不使用优惠券 */}
|
||
<View
|
||
className='bg-white rounded-lg p-3 mb-3 border border-gray-200'
|
||
onClick={() => handleCouponConfirm(null)}
|
||
>
|
||
<Text className='text-sm text-gray-600'>不使用优惠券</Text>
|
||
</View>
|
||
{/* 可用优惠券列表 */}
|
||
{coupons.map(coupon => (
|
||
<CouponCard
|
||
key={coupon.id}
|
||
coupon={coupon}
|
||
selected={selectedCoupon?.id === coupon.id}
|
||
onClick={() => handleCouponConfirm(coupon)}
|
||
/>
|
||
))}
|
||
{coupons.length === 0 && (
|
||
<View className='py-10 text-center'>
|
||
<Text className='text-gray-400 text-sm'>暂无可用优惠券</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default CheckoutPage
|