时里院子市集
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

8.2 KiB

🎯 订单确认页面优惠券功能完整实现

🚀 功能概述

已完成订单确认页面的优惠券功能从后台读取并实现完整的业务逻辑,包括:

  • 后端数据集成:从API获取用户优惠券
  • 数据转换:后端数据格式转换为前端组件格式
  • 智能排序:按优惠金额和可用性排序
  • 实时计算:动态计算优惠金额和可用性
  • 用户体验:完善的加载状态和错误处理

📊 核心功能实现

1. 数据模型更新

ShopUserCoupon 模型扩展

export interface ShopUserCoupon {
  // 原有字段...
  
  // 新增后端计算字段
  statusText?: string;        // 状态文本描述
  isExpiringSoon?: boolean;   // 是否即将过期
  daysRemaining?: number;     // 剩余天数
  hoursRemaining?: number;    // 剩余小时数
}

CouponCardProps 组件接口更新

export interface CouponCardProps {
  id?: string;                // 优惠券ID
  type?: 10 | 20 | 30;       // 类型:10-满减 20-折扣 30-免费
  statusText?: string;        // 后端返回的状态文本
  isExpiringSoon?: boolean;   // 是否即将过期
  daysRemaining?: number;     // 剩余天数
  hoursRemaining?: number;    // 剩余小时数
  // 其他字段...
}

2. 数据转换工具

核心转换函数

// 后端数据转换为前端格式
export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps

// 计算优惠券折扣金额
export const calculateCouponDiscount = (coupon: CouponCardProps, totalAmount: number): number

// 检查优惠券是否可用
export const isCouponUsable = (coupon: CouponCardProps, totalAmount: number): boolean

// 智能排序优惠券
export const sortCoupons = (coupons: CouponCardProps[], totalAmount: number): CouponCardProps[]

3. 业务逻辑实现

优惠券加载

const loadUserCoupons = async () => {
  try {
    setCouponLoading(true)
    
    // 获取用户可用优惠券
    const res = await listShopUserCoupon({
      status: 0,        // 只获取可用的
      validOnly: true   // 只获取有效的
    })

    // 数据转换和排序
    const transformedCoupons = res.map(transformCouponData)
    const sortedCoupons = sortCoupons(transformedCoupons, getGoodsTotal())
    
    setAvailableCoupons(sortedCoupons)
  } catch (error) {
    // 错误处理
  } finally {
    setCouponLoading(false)
  }
}

智能选择逻辑

const handleCouponSelect = (coupon: CouponCardProps) => {
  const total = getGoodsTotal()

  // 检查是否可用
  if (!isCouponUsable(coupon, total)) {
    const reason = getCouponUnusableReason(coupon, total)
    Taro.showToast({ title: reason, icon: 'none' })
    return
  }

  setSelectedCoupon(coupon)
  setCouponVisible(false)
  Taro.showToast({ title: '优惠券选择成功', icon: 'success' })
}

动态重新计算

const handleQuantityChange = (value: string | number) => {
  const finalQuantity = Math.max(1, Math.min(newQuantity, goods?.stock || 999))
  setQuantity(finalQuantity)
  
  // 数量变化时重新排序优惠券
  if (availableCoupons.length > 0) {
    const newTotal = parseFloat(goods?.price || '0') * finalQuantity
    const sortedCoupons = sortCoupons(availableCoupons, newTotal)
    setAvailableCoupons(sortedCoupons)
    
    // 检查当前选中的优惠券是否还可用
    if (selectedCoupon && !isCouponUsable(selectedCoupon, newTotal)) {
      setSelectedCoupon(null)
      Taro.showToast({
        title: '当前优惠券不满足使用条件,已自动取消',
        icon: 'none'
      })
    }
  }
}

4. 用户界面优化

优惠券弹窗

<Popup visible={couponVisible} position="bottom" style={{height: '60vh'}}>
  <View className="coupon-popup">
    <View className="coupon-popup__header">
      <Text>选择优惠券</Text>
      <Button onClick={() => setCouponVisible(false)}>关闭</Button>
    </View>

    <View className="coupon-popup__content">
      {couponLoading ? (
        <View className="coupon-popup__loading">
          <Text>加载优惠券中...</Text>
        </View>
      ) : (
        <>
          {/* 当前使用的优惠券 */}
          {selectedCoupon && (
            <View className="coupon-popup__current">
              <Text>当前使用</Text>
              <View className="coupon-popup__current-item">
                <Text>{selectedCoupon.title} -{calculateCouponDiscount(selectedCoupon, getGoodsTotal()).toFixed(2)}</Text>
                <Button onClick={handleCouponCancel}>取消使用</Button>
              </View>
            </View>
          )}

          {/* 可用优惠券列表 */}
          <CouponList
            title={`可用优惠券 (${usableCoupons.length})`}
            coupons={filterUsableCoupons(availableCoupons, getGoodsTotal())}
            layout="vertical"
            onCouponClick={handleCouponSelect}
            showEmpty={true}
            emptyText="暂无可用优惠券"
          />

          {/* 不可用优惠券列表 */}
          <CouponList
            title={`不可用优惠券 (${unusableCoupons.length})`}
            coupons={filterUnusableCoupons(availableCoupons, getGoodsTotal())}
            layout="vertical"
            showEmpty={false}
          />
        </>
      )}
    </View>
  </View>
</Popup>

🎯 优惠券类型支持

支持的优惠券类型

类型 说明 计算逻辑
满减券 10 满X减Y 直接减免固定金额
折扣券 20 满X享Y折 按折扣率计算
免费券 30 免费使用 全额减免

状态管理

状态 说明 显示效果
可用 0 未使用且未过期 正常显示,可选择
已使用 1 已经使用过 灰色显示,不可选
已过期 2 超过有效期 灰色显示,不可选

🔧 关键特性

1. 智能排序算法

  • 可用优惠券优先显示
  • 按优惠金额从大到小排序
  • 相同优惠金额按过期时间排序

2. 实时状态更新

  • 商品数量变化时自动重新计算
  • 自动检查选中优惠券的可用性
  • 不满足条件时自动取消选择

3. 完善的错误处理

  • 网络请求失败提示
  • 优惠券不可用原因说明
  • 加载状态显示

4. 用户体验优化

  • 加载状态提示
  • 操作成功/失败反馈
  • 清晰的分类显示

🚀 使用方式

1. 页面初始化

// 页面加载时自动获取优惠券
useDidShow(() => {
  loadAllData() // 包含优惠券加载
})

2. 选择优惠券

// 点击优惠券行打开弹窗
onClick={() => setCouponVisible(true)}

// 在弹窗中选择具体优惠券
onCouponClick={handleCouponSelect}

3. 支付时传递

// 构建订单时传递优惠券ID
const orderData = buildSingleGoodsOrder(
  goods.goodsId!,
  quantity,
  address.id,
  {
    couponId: selectedCoupon ? selectedCoupon.id : undefined
  }
)

📈 预期效果

用户体验

  • 加载流畅:优惠券数据异步加载,不阻塞页面
  • 选择便捷:智能排序,最优优惠券在前
  • 反馈及时:实时计算优惠金额和可用性
  • 操作简单:一键选择/取消,操作直观

业务价值

  • 数据准确:使用后端计算的状态,确保准确性
  • 逻辑完整:支持所有优惠券类型和状态
  • 扩展性强:易于添加新的优惠券类型
  • 维护简单:业务逻辑集中,便于维护

🔍 测试建议

功能测试

  1. 数据加载:测试优惠券列表加载
  2. 类型支持:测试不同类型优惠券的显示和计算
  3. 状态管理:测试不同状态优惠券的行为
  4. 动态计算:测试数量变化时的重新计算
  5. 错误处理:测试网络异常时的处理

边界测试

  1. 空数据:无优惠券时的显示
  2. 网络异常:请求失败时的处理
  3. 数据异常:后端返回异常数据的处理
  4. 并发操作:快速切换选择的处理

现在订单确认页面的优惠券功能已经完全集成后端数据,提供了完整的业务逻辑和良好的用户体验! 🎉