feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
@@ -8,7 +8,7 @@ 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 { createOrder } 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'
|
||||
@@ -124,8 +124,7 @@ const CheckoutPage: React.FC = () => {
|
||||
const [rushBuyProducts, setRushBuyProducts] = useState<ShopGoods[]>([])
|
||||
|
||||
// 支付方式相关状态
|
||||
// payType: 1=微信支付, 0=货到付款(默认)
|
||||
const [payType, setPayType] = useState<number>(0)
|
||||
const [payType, setPayType] = useState<number>(0) // 0: 货到付款
|
||||
|
||||
// 从本地存储获取立即购买的数据
|
||||
const buyNowItems = useMemo(() => {
|
||||
@@ -217,7 +216,7 @@ const CheckoutPage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载凑单推荐商品
|
||||
// 选择地址
|
||||
const handleSelectAddress = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/pages/user/address-list?from=checkout',
|
||||
@@ -271,21 +270,18 @@ const CheckoutPage: React.FC = () => {
|
||||
|
||||
const res = await createOrder(orderParams)
|
||||
|
||||
// 清理购物车数据(无论哪种支付方式都需清理)
|
||||
// 货到付款 - 只需创建订单,无需调用支付接口
|
||||
console.log('订单创建成功', res)
|
||||
|
||||
// 清理数据
|
||||
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' })
|
||||
// 显示成功提示
|
||||
Taro.showToast({ title: '订单已提交', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({ url: '/pages/order/list' })
|
||||
}, 1500)
|
||||
@@ -296,25 +292,6 @@ const CheckoutPage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 微信支付
|
||||
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
|
||||
@@ -462,49 +439,23 @@ const CheckoutPage: React.FC = () => {
|
||||
<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)}
|
||||
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>
|
||||
<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' }}
|
||||
style={{ borderColor: '#22c55e' }}
|
||||
>
|
||||
{payType === 0 && (
|
||||
<View className='w-2 h-2 rounded-full' style={{ backgroundColor: '#22c55e' }} />
|
||||
)}
|
||||
<View className='w-2 h-2 rounded-full' style={{ backgroundColor: '#22c55e' }} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { ShopGroupBuy, ShopGroupBuyRecord } from '@/api/shop/shopGroupBuy/m
|
||||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||
import type { ShopGoodsSku } from '@/api/shop/shopGoodsSku/model'
|
||||
import SkuSelector from '@/components/business/SkuSelector'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '拼团详情',
|
||||
@@ -193,12 +192,12 @@ const GroupBuyDetailPage: React.FC = () => {
|
||||
{groupBuy.goodsName || groupBuy.product?.name}
|
||||
</Text>
|
||||
<View className="flex items-baseline gap-2">
|
||||
<Price
|
||||
price={groupBuy.groupPrice}
|
||||
original={groupBuy.product?.price || 0}
|
||||
size='large'
|
||||
loginMask
|
||||
/>
|
||||
<Text className="text-2xl font-bold text-red-500">
|
||||
¥{groupBuy.groupPrice}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-400 line-through">
|
||||
¥{groupBuy.product?.price || 0}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex items-center gap-2 mt-1">
|
||||
{isActive && remaining > 0 && (
|
||||
|
||||
@@ -4,7 +4,6 @@ import Taro from '@tarojs/taro'
|
||||
import { pageShopGroupBuy } from '@/api/shop/shopGroupBuy'
|
||||
import type { ShopGroupBuy } from '@/api/shop/shopGroupBuy/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '拼团活动',
|
||||
@@ -92,12 +91,12 @@ const GroupBuyListPage: React.FC = () => {
|
||||
|
||||
<View className='flex items-center justify-between mt-1'>
|
||||
<View className='flex items-baseline gap-1'>
|
||||
<Price
|
||||
price={item.groupPrice}
|
||||
original={item.product?.price || 0}
|
||||
size='small'
|
||||
loginMask
|
||||
/>
|
||||
<Text className='text-lg font-bold text-red-500'>
|
||||
{'\u00A5'}{item.groupPrice}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400 line-through'>
|
||||
{'\u00A5'}{item.product?.price || 0}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
className='px-3 py-1 rounded-full text-white text-xs'
|
||||
|
||||
@@ -85,15 +85,8 @@ const ProductDetailPage: React.FC = () => {
|
||||
setIsFavorite(true)
|
||||
Taro.showToast({ title: '收藏成功', icon: 'success' })
|
||||
}
|
||||
} catch (err: any) {
|
||||
// 接口报错时仍更新UI状态,避免状态不一致
|
||||
console.error('[ProductDetail] 收藏操作失败:', err?.message || err)
|
||||
if (isFavorite) {
|
||||
setIsFavorite(false)
|
||||
} else {
|
||||
setIsFavorite(true)
|
||||
}
|
||||
// 不再额外弹toast(handleError已经弹了后端返回的错误信息)
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e?.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,12 +158,10 @@ const ProductDetailPage: React.FC = () => {
|
||||
}
|
||||
|
||||
const handleGoCart = () => {
|
||||
// 购物车不是 tabBar 页面,使用 navigateTo;路径与 app.config.ts 中注册的 'pages/shop/cart' 对应
|
||||
Taro.navigateTo({ url: '/pages/shop/cart' })
|
||||
}
|
||||
|
||||
const handleContactService = () => {
|
||||
// 直接跳转到在线客服页面(已实现完整功能:微信客服按钮 + 历史消息 + 热线 + 微信留言)
|
||||
Taro.navigateTo({ url: '/pages/user/customer-service/index' })
|
||||
}
|
||||
|
||||
@@ -214,7 +205,6 @@ const ProductDetailPage: React.FC = () => {
|
||||
'<img src="$2" mode="widthFix" style="max-width:100%;display:block;" />'
|
||||
)
|
||||
// 兜底:如果内容里直接包含 http(s) 图片链接(非 img 标签包裹的),也尝试转成图片
|
||||
// 匹配独立的 https://xxx.jpg/png/gif 链接
|
||||
if (html.includes('http') && !html.includes('<img')) {
|
||||
html = html.replace(
|
||||
/(https?:\/\/[^\s\)]+\.(jpg|jpeg|png|gif|webp)(\?[^\s\)]*)?)/gi,
|
||||
@@ -229,44 +219,44 @@ const ProductDetailPage: React.FC = () => {
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
|
||||
{/* 图片轮播 */}
|
||||
<Swiper
|
||||
className='w-full'
|
||||
style={{ height: `${swiperHeight}px` }}
|
||||
indicatorDots
|
||||
indicatorColor='#e5e7eb'
|
||||
indicatorActiveColor='#0e932e'
|
||||
autoplay
|
||||
circular
|
||||
>
|
||||
{images.map((img, idx) => (
|
||||
<SwiperItem key={idx}>
|
||||
<Image
|
||||
className='w-full'
|
||||
style={{ display: 'block' }}
|
||||
src={img}
|
||||
mode='widthFix'
|
||||
onLoad={(e: any) => {
|
||||
// 根据第一张图片的实际宽高比动态设置 Swiper 高度
|
||||
if (idx === 0) {
|
||||
const { width, height } = e.detail
|
||||
if (width > 0) {
|
||||
const screenWidth = Taro.getSystemInfoSync().windowWidth
|
||||
setSwiperHeight(Math.round((height / width) * screenWidth))
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={() => Taro.previewImage({ current: img, urls: images })}
|
||||
/>
|
||||
</SwiperItem>
|
||||
))}
|
||||
{images.length === 0 && (
|
||||
<SwiperItem>
|
||||
<View className='w-full h-full bg-gray-100 flex items-center justify-center'>
|
||||
<Text className='text-gray-300 text-sm'>暂无图片</Text>
|
||||
</View>
|
||||
</SwiperItem>
|
||||
)}
|
||||
</Swiper>
|
||||
<Swiper
|
||||
className='w-full'
|
||||
style={{ height: `${swiperHeight}px` }}
|
||||
indicatorDots
|
||||
indicatorColor='#e5e7eb'
|
||||
indicatorActiveColor='#0e932e'
|
||||
autoplay
|
||||
circular
|
||||
>
|
||||
{images.map((img, idx) => (
|
||||
<SwiperItem key={idx}>
|
||||
<Image
|
||||
className='w-full'
|
||||
style={{ display: 'block' }}
|
||||
src={img}
|
||||
mode='widthFix'
|
||||
onLoad={(e: any) => {
|
||||
// 根据第一张图片的实际宽高比动态设置 Swiper 高度
|
||||
if (idx === 0) {
|
||||
const { width, height } = e.detail
|
||||
if (width > 0) {
|
||||
const screenWidth = Taro.getSystemInfoSync().windowWidth
|
||||
setSwiperHeight(Math.round((height / width) * screenWidth))
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={() => Taro.previewImage({ current: img, urls: images })}
|
||||
/>
|
||||
</SwiperItem>
|
||||
))}
|
||||
{images.length === 0 && (
|
||||
<SwiperItem>
|
||||
<View className='w-full h-full bg-gray-100 flex items-center justify-center'>
|
||||
<Text className='text-gray-300 text-sm'>暂无图片</Text>
|
||||
</View>
|
||||
</SwiperItem>
|
||||
)}
|
||||
</Swiper>
|
||||
|
||||
{/* 价格区域 */}
|
||||
<View className='bg-white p-4'>
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { ShopSeckill } from '@/api/shop/shopSeckill/model'
|
||||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||
import type { ShopGoodsSku } from '@/api/shop/shopGoodsSku/model'
|
||||
import SkuSelector from '@/components/business/SkuSelector'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '秒杀详情',
|
||||
@@ -92,26 +91,14 @@ const SeckillDetailPage: React.FC = () => {
|
||||
setCountdown(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`)
|
||||
}
|
||||
|
||||
const handleBuyClick = () => {
|
||||
const handleBuyNow = () => {
|
||||
if (!seckill) return
|
||||
|
||||
if (seckill.status === 0) {
|
||||
Taro.showToast({ title: '活动尚未开始,请耐心等待', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (seckill.status === 2 || new Date(seckill.endTime).getTime() <= Date.now()) {
|
||||
Taro.showToast({ title: '活动已结束,下次再来', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (seckill.stock <= 0) {
|
||||
Taro.showToast({ title: '已抢光,下次再来', icon: 'none' })
|
||||
Taro.showToast({ title: '已抢光', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (submitting) return
|
||||
|
||||
// 先拉取商品规格(如果没有的话)
|
||||
if (!product && seckill.goodsId) {
|
||||
Taro.showLoading({ title: '加载规格...' })
|
||||
@@ -200,13 +187,12 @@ const SeckillDetailPage: React.FC = () => {
|
||||
{/* 价格和倒计时 */}
|
||||
<View className="px-4 py-4" style={{ background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' }}>
|
||||
<View className="flex items-baseline gap-2">
|
||||
<Price
|
||||
price={seckill.seckillPrice}
|
||||
original={seckill.product?.price || 0}
|
||||
size='large'
|
||||
color='#ffffff'
|
||||
loginMask
|
||||
/>
|
||||
<Text className="text-3xl font-bold text-white">
|
||||
¥{seckill.seckillPrice}
|
||||
</Text>
|
||||
<Text className="text-sm text-red-200 line-through">
|
||||
¥{seckill.product?.price || 0}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex items-center justify-between mt-3">
|
||||
<View className="bg-white rounded px-2 py-1">
|
||||
@@ -269,11 +255,8 @@ const SeckillDetailPage: React.FC = () => {
|
||||
<View className="bg-white border-t border-gray-100 px-4 py-3">
|
||||
<View
|
||||
className="w-full py-3 rounded-full text-white text-center text-base font-medium"
|
||||
style={{
|
||||
backgroundColor: isActive && !submitting ? '#ef4444' : '#d1d5db',
|
||||
opacity: submitting ? 0.7 : 1,
|
||||
}}
|
||||
onClick={handleBuyClick}
|
||||
style={{ backgroundColor: isActive ? '#ef4444' : '#d1d5db' }}
|
||||
onClick={isActive && !submitting ? handleBuyNow : undefined}
|
||||
>
|
||||
<Text>
|
||||
{submitting ? '抢购中...' : isActive ? (seckill.stock > 0 ? '立即抢购' : '已抢光') : seckill.status === 0 ? '即将开始' : '已结束'}
|
||||
|
||||
@@ -4,7 +4,6 @@ import Taro from '@tarojs/taro'
|
||||
import { pageShopSeckill } from '@/api/shop/shopSeckill'
|
||||
import type { ShopSeckill } from '@/api/shop/shopSeckill/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import Price from '@/components/common/Price'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '限时秒杀',
|
||||
@@ -122,12 +121,12 @@ const SeckillListPage: React.FC = () => {
|
||||
|
||||
<View className='flex items-center justify-between mt-1'>
|
||||
<View className='flex items-baseline gap-1'>
|
||||
<Price
|
||||
price={item.seckillPrice}
|
||||
original={item.product?.price || 0}
|
||||
size='small'
|
||||
loginMask
|
||||
/>
|
||||
<Text className='text-lg font-bold text-red-500'>
|
||||
{'\u00A5'}{item.seckillPrice}
|
||||
</Text>
|
||||
<Text className='text-xs text-gray-400 line-through'>
|
||||
{'\u00A5'}{item.product?.price || 0}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
className='px-3 py-1 rounded-full text-white text-xs'
|
||||
|
||||
Reference in New Issue
Block a user