feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
628
src_bak/pages/shop/checkout.tsx
Normal file
628
src_bak/pages/shop/checkout.tsx
Normal file
@@ -0,0 +1,628 @@
|
||||
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, type WxPayResult } from '@/api/shop/shopOrder'
|
||||
import type { ShopOrder } from '@/api/shop/shopOrder/model'
|
||||
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'
|
||||
|
||||
// 满减门槛配置
|
||||
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[]>([])
|
||||
|
||||
// 支付方式相关状态
|
||||
// payType: 1=微信支付, 0=货到付款(默认)
|
||||
const [payType, setPayType] = useState<number>(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
|
||||
return items.reduce((sum, item) => {
|
||||
return sum + Number(item.skuPrice || item.sku?.price || item.product?.salePrice || item.product?.price || 0) * (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,
|
||||
}))
|
||||
|
||||
// 创建订单
|
||||
const orderParams: OrderCreateRequest = {
|
||||
goodsItems,
|
||||
addressId: defaultAddress.id,
|
||||
payType,
|
||||
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()
|
||||
}
|
||||
|
||||
// 根据支付方式处理
|
||||
if (payType === 1) {
|
||||
// 微信支付 - res 是 WxPayResult
|
||||
await handleWxPay(res as WxPayResult)
|
||||
}
|
||||
|
||||
// 货到付款(payType=0)无需在线支付,直接下单成功
|
||||
Taro.showToast({ title: payType === 0 ? '下单成功' : '支付成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({ url: '/pages/order/list' })
|
||||
}, 1500)
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 微信支付
|
||||
const handleWxPay = async (payData: WxPayResult & { paid?: string }) => {
|
||||
// 后端返回已支付(回调丢失自动修复场景)
|
||||
if (payData.paid === 'true') {
|
||||
return // 直接视为支付成功
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
Taro.requestPayment({
|
||||
timeStamp: payData.timeStamp,
|
||||
nonceStr: payData.nonceStr,
|
||||
package: payData.package,
|
||||
signType: payData.signType,
|
||||
paySign: payData.paySign,
|
||||
success: () => resolve(),
|
||||
fail: (err) => reject(new Error('支付取消'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 安全的数字格式化
|
||||
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'>
|
||||
¥{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 mb-2'*/}
|
||||
{/* style={{ backgroundColor: payType === 1 ? '#f0fdf4' : '#f9fafb' }}*/}
|
||||
{/* onClick={() => setPayType(1)}*/}
|
||||
{/*>*/}
|
||||
{/* <View className='w-8 h-8 rounded-full bg-green-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: payType === 1 ? '#22c55e' : '#d1d5db' }}*/}
|
||||
{/* >*/}
|
||||
{/* {payType === 1 && (*/}
|
||||
{/* <View className='w-2 h-2 rounded-full' style={{ backgroundColor: '#22c55e' }} />*/}
|
||||
{/* )}*/}
|
||||
{/* </View>*/}
|
||||
{/*</View>*/}
|
||||
|
||||
{/* 货到付款 */}
|
||||
<View
|
||||
className='flex items-center py-3 px-2 rounded-lg'
|
||||
style={{ backgroundColor: payType === 0 ? '#f0fdf4' : '#f9fafb' }}
|
||||
onClick={() => setPayType(0)}
|
||||
>
|
||||
<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: payType === 0 ? '#22c55e' : '#d1d5db' }}
|
||||
>
|
||||
{payType === 0 && (
|
||||
<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
|
||||
Reference in New Issue
Block a user