- 将原来 switchTab 跳转“订单”页改为 navigateTo,以适配非 tabBar 页面 - 统一更新多个文件中关于订单页的跳转方式共计 6 处 - 更新 tabBar 配置,从包含“订单”页改为实际的四个 tabBar 页面 - 修正 isTabBarUrl 相关判断,去除不存在或非 tabBar 路径 - 调整多个 tabBar 判断列表,保证逻辑一致性与正确性 - 修改购物车页图片压缩宽度加入限制,优化资源加载 - 更新订单列表页注释,去除“tabBar 页”描述 - 备注遗留问题,部分积分页跳转仍用错误方式,待进一步修复
626 lines
25 KiB
TypeScript
626 lines
25 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 { useVipStatus } from '@/hooks/useVipStatus'
|
||
import { getCompressedImageUrl } from '@/utils/image'
|
||
|
||
// 满减门槛配置
|
||
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[]>([])
|
||
|
||
// VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染
|
||
const { isVip } = useVipStatus()
|
||
|
||
// 支付方式相关状态
|
||
const [payType, setPayType] = useState<number>(9) // 9: 线下付款(默认),8: 货到付款(已注释)
|
||
|
||
// 支付方式选项
|
||
const PAYMENT_OPTIONS = [
|
||
// { id: 8, name: '货到付款', desc: '送达时支付', icon: '货', bgColor: '#f0fdf4', iconBg: '#dbeafe', iconColor: '#2563eb', borderColor: '#22c55e' },
|
||
{ id: 9, name: '线下付款', desc: '微信转账,商家确认后发货', icon: '线', bgColor: '#fffbeb', iconBg: '#fef3c7', iconColor: '#d97706', borderColor: '#f59e0b' },
|
||
]
|
||
|
||
// 从本地存储获取立即购买的数据
|
||
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 = isVip
|
||
return items.reduce((sum, item) => {
|
||
// VIP 会员优先使用 dealerPrice(后端关联字段 > product 嵌套对象)
|
||
const dealerPrice = vip ? (item.dealerPrice || (item.product as any)?.dealerPrice) : null
|
||
const unitPrice = dealerPrice || item.skuPrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || 0
|
||
return sum + Number(unitPrice) * (item.quantity || item.num || 1)
|
||
}, 0)
|
||
}, [buyNowItems, selectedItems, isVip])
|
||
|
||
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 => {
|
||
// VIP 会员优先使用 dealerPrice 作为下单单价
|
||
const dealerPrice = isVip ? (item.dealerPrice || (item.product as any)?.dealerPrice) : null
|
||
const unitPrice = dealerPrice || undefined
|
||
return {
|
||
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,
|
||
price: unitPrice ? String(unitPrice) : undefined,
|
||
}
|
||
})
|
||
|
||
// 货到付款(8):后端会自动设置 payStatus=true,无需前端传递 payStatus/payTime
|
||
// 线下付款(9):后端会设置 payStatus=false,等待商家确认收款后才设为 true
|
||
const orderParams: OrderCreateRequest = {
|
||
goodsItems,
|
||
addressId: defaultAddress.id,
|
||
payType,
|
||
couponId: selectedCoupon?.id ? Number(selectedCoupon.id) : undefined,
|
||
buyerRemarks: remarks,
|
||
deliveryType: 0, // 快递配送
|
||
}
|
||
|
||
const res = await createOrder(orderParams)
|
||
|
||
// 清理数据
|
||
if (fromBuyNow) {
|
||
Taro.removeStorageSync('buy_now')
|
||
} else {
|
||
await removeSelected()
|
||
}
|
||
|
||
// 显示成功提示
|
||
Taro.showToast({ title: '订单已提交', icon: 'success' })
|
||
setTimeout(() => {
|
||
Taro.navigateTo({ url: '/pages/order/list' })
|
||
}, 1500)
|
||
} catch (err: any) {
|
||
const message = err.message || '提交失败'
|
||
// 线下付款(payType=9)下单时,后端如果仍尝试创建微信支付订单,
|
||
// 会因支付配置缺失应用ID而报错。这里给出更明确的提示。
|
||
if (payType === 9 && /应用ID|appid|appId|支付配置|微信支付订单|创建支付订单失败/.test(message)) {
|
||
Taro.showToast({ title: '线下支付暂不可用:微信支付配置缺失', icon: 'none', duration: 3000 })
|
||
console.error('[checkout] 线下付款下单失败,后端支付配置问题:', message)
|
||
} else {
|
||
Taro.showToast({ title: 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={getCompressedImageUrl(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'>
|
||
¥{isVip && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.salePrice || item.sku?.price || item.salePrice || 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>
|
||
|
||
{PAYMENT_OPTIONS.map((option) => {
|
||
const selected = payType === option.id
|
||
return (
|
||
<View
|
||
key={option.id}
|
||
className='flex items-center py-3 px-2 rounded-lg mb-2'
|
||
style={{ backgroundColor: selected ? option.bgColor : '#f9fafb' }}
|
||
onClick={() => setPayType(option.id)}
|
||
>
|
||
<View
|
||
className='w-8 h-8 rounded-full flex items-center justify-center mr-3'
|
||
style={{ backgroundColor: option.iconBg }}
|
||
>
|
||
<Text className='text-sm' style={{ color: option.iconColor }}>{option.icon}</Text>
|
||
</View>
|
||
<View className='flex-1'>
|
||
<Text className='text-sm text-gray-800 block'>{option.name}</Text>
|
||
<Text className='text-xs text-gray-400 block'>{option.desc}</Text>
|
||
</View>
|
||
<View
|
||
className='w-5 h-5 rounded-full border-2 flex items-center justify-center'
|
||
style={{ borderColor: selected ? option.borderColor : '#d1d5db' }}
|
||
>
|
||
{selected && (
|
||
<View className='w-2 h-2 rounded-full' style={{ backgroundColor: option.borderColor }} />
|
||
)}
|
||
</View>
|
||
</View>
|
||
)
|
||
})}
|
||
|
||
{/* 线下付款选中时显示提示 */}
|
||
{payType === 9 && (
|
||
<View className='mt-2 p-2 rounded-lg' style={{ backgroundColor: '#fef3c7' }}>
|
||
<Text className='text-xs text-orange-600'>
|
||
请下单后通过微信转账付款,商家确认收款后发货。
|
||
</Text>
|
||
</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={getCompressedImageUrl(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
|