案例开源地址:https://atomgit.com/nutpi/rn_openharmony_buy

写在前面

上一篇写了优惠券列表,那是用户查看自己所有优惠券的地方。这篇要写的优惠券选择页,是在结算时选择要使用哪张优惠券。

这两个页面看起来差不多,但逻辑不同。优惠券列表是纯展示,优惠券选择要根据订单金额判断哪些优惠券可用、哪些不可用,用户选择后要把结果传回结算页。
请添加图片描述

和优惠券列表的区别

优惠券列表页:

  • 展示所有优惠券(可用、已用、过期)
  • 点击"去使用"跳转首页
  • 纯展示,不涉及选择

优惠券选择页:

  • 只展示未使用的优惠券
  • 根据订单金额判断是否可用
  • 点击选中优惠券,返回结算页

引入依赖

import React from 'react';
import {View, Text, FlatList, StyleSheet, TouchableOpacity} from 'react-native';
import {useApp} from '../store/AppContext';
import {Header} from '../components/Header';
import {Empty} from '../components/Empty';
import {Coupon} from '../types';

标准的列表页依赖,没什么特别的。

组件主体

export const CouponSelectScreen = () => {
  const {coupons, screenParams, goBack} = useApp();
  const minAmount = screenParams?.minAmount || 0;
  const onSelect = screenParams?.onSelect;

从页面参数里拿两个东西:

  • minAmount:订单金额,用来判断优惠券是否可用
  • onSelect:选择回调,选中优惠券后调用这个函数把结果传回去

这两个参数是结算页跳转时传过来的。

为什么用回调传递结果?

也可以用全局状态,但回调更灵活。结算页传一个函数过来,选择页调用这个函数把结果传回去,两个页面的耦合度很低。

优惠券分组

const availableCoupons = coupons.filter(c => !c.used && minAmount >= c.minAmount);
const unavailableCoupons = coupons.filter(c => !c.used && minAmount < c.minAmount);

把优惠券分成两组:

  • 可用:未使用且订单金额满足最低消费(minAmount >= c.minAmount
  • 不可用:未使用但订单金额不满足最低消费

注意这里只筛选未使用的优惠券,已使用的不显示。已过期的也应该过滤掉,这里简化了没加过期判断。

为什么要显示不可用的优惠券?

让用户知道自己还有哪些优惠券,只是当前订单金额不够用。这样用户可能会多买点东西凑单,提高客单价。如果不显示,用户可能不知道自己有这些优惠券。

选择处理

const handleSelect = (coupon: Coupon | null) => {
  if (onSelect) {
    onSelect(coupon);
  }
  goBack();
};

选择优惠券后,调用回调函数把选中的优惠券传回去,然后返回上一页。

参数类型是 Coupon | null,因为用户也可以选择"不使用优惠券",这时候传 null

优惠券卡片渲染

const renderCoupon = ({item, disabled}: {item: Coupon; disabled?: boolean}) => (
  <TouchableOpacity
    style={[styles.couponCard, disabled && styles.disabledCard]}
    onPress={() => !disabled && handleSelect(item)}
    disabled={disabled}
  >

卡片接收 disabled 参数,不可用的优惠券点击无效。disabled 属性会阻止 onPress 触发,但为了保险,onPress 里也加了判断。

左边金额区域

    <View style={[styles.leftPart, disabled && styles.disabledLeft]}>
      <Text style={[styles.discount, disabled && styles.disabledText]}>${item.discount}</Text>
      <Text style={[styles.condition, disabled && styles.disabledText]}>满${item.minAmount}可用</Text>
    </View>

和优惠券列表一样,左边显示优惠金额和使用条件。不可用时变灰色。

右边信息区域

    <View style={styles.rightPart}>
      <Text style={[styles.title, disabled && styles.disabledText]}>{item.title}</Text>
      <Text style={[styles.expire, disabled && styles.disabledText]}>有效期至 {item.expireDate}</Text>
      {disabled && <Text style={styles.disabledReason}>订单金额不满足</Text>}
    </View>

显示优惠券名称和有效期。不可用的优惠券下面显示原因:“订单金额不满足”。

为什么要显示不可用原因?

让用户知道为什么不能用这张优惠券。如果只是变灰不说原因,用户会困惑。告诉用户"订单金额不满足",用户就知道多买点东西就能用了。

选择指示器

    {!disabled && <View style={styles.selectCircle}><Text style={styles.selectIcon}>○</Text></View>}
  </TouchableOpacity>
);

可用的优惠券右边有个圆圈,表示可以选择。不可用的不显示这个圆圈。

不使用优惠券选项

return (
  <View style={styles.container}>
    <Header title="选择优惠券" />

    <TouchableOpacity style={styles.noCouponBtn} onPress={() => handleSelect(null)}>
      <Text style={styles.noCouponText}>不使用优惠券</Text>
    </TouchableOpacity>

列表最上面有个"不使用优惠券"的选项。用户可能有优惠券但不想用(比如想留着下次用),点击这个选项传 null 回去。

列表渲染

    {availableCoupons.length === 0 && unavailableCoupons.length === 0 ? (
      <Empty icon="🎫" title="暂无优惠券" subtitle="去领取优惠券吧" />
    ) : (
      <FlatList
        data={[...availableCoupons, ...unavailableCoupons]}
        keyExtractor={item => item.id.toString()}
        contentContainerStyle={styles.list}
        renderItem={({item}) => renderCoupon({item, disabled: minAmount < item.minAmount})}
        ListHeaderComponent={
          availableCoupons.length > 0 ? (
            <Text style={styles.listHeader}>可用优惠券 ({availableCoupons.length})</Text>
          ) : null
        }
      />
    )}
  </View>
);
};

把可用和不可用的优惠券合并成一个数组,可用的在前面。renderItem 里根据订单金额判断是否 disabled。

ListHeaderComponent 显示"可用优惠券 (X)"的标题,只在有可用优惠券时显示。

为什么不用 SectionList?

也可以用 SectionList 把可用和不可用分成两个 section。但这里不可用的优惠券不需要单独的标题,用 FlatList 更简单。

样式

const styles = StyleSheet.create({
  container: {flex: 1, backgroundColor: '#f5f5f5'},
  noCouponBtn: {
    backgroundColor: '#fff',
    padding: 16,
    marginBottom: 12,
    alignItems: 'center',
  },
  noCouponText: {fontSize: 16, color: '#3498db'},

"不使用优惠券"按钮用蓝色文字,和普通优惠券卡片区分开。

  listHeader: {fontSize: 14, color: '#666', marginBottom: 12},
  couponCard: {
    flexDirection: 'row',
    backgroundColor: '#fff',
    borderRadius: 12,
    marginBottom: 12,
    overflow: 'hidden',
    alignItems: 'center',
  },
  disabledCard: {opacity: 0.6},

不可用的卡片透明度降低,视觉上变暗。

  disabledReason: {fontSize: 12, color: '#e74c3c', marginTop: 4},
  selectCircle: {paddingRight: 16},
  selectIcon: {fontSize: 24, color: '#3498db'},
});

不可用原因用红色小字,选择圆圈用蓝色。

结算页如何调用

看看结算页是怎么跳转到优惠券选择页的:

// CheckoutScreen.tsx
const [selectedCoupon, setSelectedCoupon] = useState<Coupon | null>(null);

const handleSelectCoupon = () => {
  navigate('couponSelect', {
    minAmount: totalPrice,
    onSelect: (coupon: Coupon | null) => {
      setSelectedCoupon(coupon);
    },
  });
};

跳转时传入订单金额和选择回调。用户选择后,回调被调用,selectedCoupon 被更新,结算页显示选中的优惠券。

选中状态显示

当前实现点击就选中并返回,没有显示当前选中的是哪张。可以加个选中状态:

const currentCouponId = screenParams?.currentCouponId;

// 在卡片里
{!disabled && (
  <View style={styles.selectCircle}>
    <Text style={styles.selectIcon}>
      {item.id === currentCouponId ? '●' : '○'}
    </Text>
  </View>
)}

结算页跳转时把当前选中的优惠券 ID 传过来,选择页显示哪张是选中的。选中的显示实心圆,未选中的显示空心圆。

最优优惠券推荐

可以自动推荐最优惠的优惠券:

const bestCoupon = availableCoupons.reduce((best, current) => {
  if (!best) return current;
  return current.discount > best.discount ? current : best;
}, null as Coupon | null);

// 在最优优惠券旁边显示标签
{item.id === bestCoupon?.id && (
  <View style={styles.bestTag}>
    <Text style={styles.bestTagText}>最优</Text>
  </View>
)}

找出优惠金额最大的优惠券,在旁边显示"最优"标签,引导用户选择。

写在最后

优惠券选择页和优惠券列表页看起来相似,但职责不同。选择页要根据订单金额判断可用性,要支持选择和取消选择,要把结果传回结算页。

这种"选择并返回"的交互模式在电商 App 里很常见,地址选择也是类似的逻辑。核心是通过页面参数传递回调函数,选择完成后调用回调把结果传回去。

下一篇写消息列表,展示系统通知、订单消息等。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐