案例开源地址:https://atomgit.com/nutpi/rn_openharmony_buy
请添加图片描述

写在前面

收货地址是电商 App 的基础功能。用户下单时需要选择收货地址,所以要有个地方管理地址:查看、添加、编辑、删除、设置默认地址。

地址列表页有两种使用场景:一种是从个人中心进入,纯粹管理地址;另一种是从结算页进入,选择收货地址。这两种场景的交互略有不同,需要在代码里区分处理。

地址数据结构

先看看地址数据长什么样:

interface Address {
  id: number;
  name: string;        // 收货人姓名
  phone: string;       // 手机号
  province: string;    // 省
  city: string;        // 市
  district: string;    // 区
  detail: string;      // 详细地址
  isDefault: boolean;  // 是否默认地址
}

地址拆成省、市、区、详细地址四个字段,方便后续做地址选择器。isDefault 标记是否为默认地址,下单时会自动选中默认地址。

引入依赖

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

用到了 Alert 做删除确认,FlatList 渲染地址列表。

组件主体

export const AddressListScreen = () => {
  const {addresses, deleteAddress, setDefaultAddress, navigate, goBack, screenParams} = useApp();
  const isSelecting = screenParams?.selecting;

从全局状态拿地址列表和操作方法。screenParams?.selecting 判断当前是"选择模式"还是"管理模式"。

如果是从结算页跳过来选地址,selecting 会是 true;如果是从个人中心进来管理地址,selectingundefinedfalse

两种模式有什么区别?

管理模式:点击地址卡片进入编辑页面。
选择模式:点击地址卡片选中这个地址,然后返回上一页。

选择地址处理

const handleSelect = (address: Address) => {
  if (isSelecting && screenParams?.onSelect) {
    screenParams.onSelect(address);
    goBack();
  }
};

选择模式下,点击地址会调用 screenParams.onSelect 回调,把选中的地址传回去,然后返回上一页。

这个回调是结算页跳转时传过来的,结算页会用这个地址更新收货地址。

为什么用回调而不是全局状态?

也可以用全局状态,比如设置一个 selectedAddress。但用回调更灵活,不同页面可以有不同的处理逻辑。而且不会污染全局状态。

删除地址

const handleDelete = (id: number) => {
  Alert.alert('删除地址', '确定要删除这个地址吗?', [
    {text: '取消', style: 'cancel'},
    {text: '删除', style: 'destructive', onPress: () => deleteAddress(id)},
  ]);
};

删除是危险操作,需要二次确认。style: 'destructive' 让删除按钮显示成红色。

设置默认地址

const handleSetDefault = (id: number) => {
  setDefaultAddress(id);
};

设置默认地址不需要确认,点击直接生效。Context 里的实现会把其他地址的 isDefault 设为 false,保证只有一个默认地址。

地址卡片渲染

const renderAddress = ({item}: {item: Address}) => (
  <TouchableOpacity
    style={styles.addressCard}
    onPress={() => isSelecting ? handleSelect(item) : navigate('addressEdit', {address: item})}
  >

整个卡片可点击。选择模式下点击选中地址,管理模式下点击进入编辑页面。

地址信息

    <View style={styles.addressContent}>
      <View style={styles.nameRow}>
        <Text style={styles.name}>{item.name}</Text>
        <Text style={styles.phone}>{item.phone}</Text>
        {item.isDefault && (
          <View style={styles.defaultTag}>
            <Text style={styles.defaultText}>默认</Text>
          </View>
        )}
      </View>
      <Text style={styles.detail} numberOfLines={2}>
        {item.province} {item.city} {item.district} {item.detail}
      </Text>
    </View>

第一行显示姓名、电话,如果是默认地址还有个红色的"默认"标签。第二行显示完整地址,限制两行,超出显示省略号。

为什么姓名和电话放一行?

这是常见的设计,用户一眼就能看到这个地址是谁的、怎么联系。如果分两行会显得信息很散。

操作按钮

    <View style={styles.actions}>
      {!item.isDefault && (
        <TouchableOpacity style={styles.actionBtn} onPress={() => handleSetDefault(item.id)}>
          <Text style={styles.actionText}>设为默认</Text>
        </TouchableOpacity>
      )}
      <TouchableOpacity style={styles.actionBtn} onPress={() => navigate('addressEdit', {address: item})}>
        <Text style={styles.actionText}>编辑</Text>
      </TouchableOpacity>
      <TouchableOpacity style={styles.actionBtn} onPress={() => handleDelete(item.id)}>
        <Text style={[styles.actionText, styles.deleteText]}>删除</Text>
      </TouchableOpacity>
    </View>
  </TouchableOpacity>
);

底部是操作按钮:设为默认、编辑、删除。

"设为默认"按钮只在非默认地址上显示,已经是默认的就不需要这个按钮了。

删除按钮用红色,提醒用户这是危险操作。

页面主体

return (
  <View style={styles.container}>
    <Header title={isSelecting ? '选择收货地址' : '收货地址'} />

标题根据模式不同而不同。选择模式显示"选择收货地址",管理模式显示"收货地址"。

    {addresses.length === 0 ? (
      <Empty 
        icon="📍" 
        title="暂无收货地址" 
        subtitle="添加一个收货地址吧" 
        buttonText="添加地址" 
        onPress={() => navigate('addressEdit')} 
      />
    ) : (
      <FlatList
        data={addresses}
        keyExtractor={item => item.id.toString()}
        contentContainerStyle={styles.list}
        renderItem={renderAddress}
      />
    )}

没有地址时显示空状态,引导用户添加地址。有地址时用 FlatList 渲染列表。

contentContainerStyle 里的 paddingBottom: 100 给底部的添加按钮留空间,不然最后一个地址会被按钮挡住。

添加地址按钮

    <TouchableOpacity style={styles.addBtn} onPress={() => navigate('addressEdit')}>
      <Text style={styles.addBtnIcon}>➕</Text>
      <Text style={styles.addBtnText}>添加新地址</Text>
    </TouchableOpacity>
  </View>
);
};

底部固定一个添加按钮,蓝色背景很醒目。点击跳转到地址编辑页,不传 address 参数表示新增。

Context 里的地址操作

看看全局状态里地址相关的方法:

const [addresses, setAddresses] = useState<Address[]>(mockAddresses);

初始化了一些模拟数据,方便测试。

删除地址:

const deleteAddress = (id: number) => {
  setAddresses(prev => prev.filter(a => a.id !== id));
};

filter 过滤掉指定 ID 的地址。

设置默认地址:

const setDefaultAddress = (id: number) => {
  setAddresses(prev => prev.map(a => ({...a, isDefault: a.id === id})));
};

遍历所有地址,把指定 ID 的地址设为默认,其他的设为非默认。这样保证只有一个默认地址。

为什么不用两步操作?

也可以先把所有地址的 isDefault 设为 false,再把指定地址设为 true。但那样要调用两次 setAddresses,触发两次渲染。用 map 一次搞定更高效。

获取默认地址:

const defaultAddress = addresses.find(a => a.isDefault) || addresses[0] || null;

找到默认地址。如果没有设置默认,就用第一个地址。如果一个地址都没有,返回 null

样式细节

地址卡片的样式:

addressCard: {
  backgroundColor: '#fff',
  borderRadius: 12,
  padding: 16,
  marginBottom: 12,
},
addressContent: {
  borderBottomWidth: 1,
  borderBottomColor: '#f0f0f0',
  paddingBottom: 12,
},

卡片用白色背景和圆角,地址信息和操作按钮之间有分割线。

默认标签的样式:

defaultTag: {
  backgroundColor: '#e74c3c',
  paddingHorizontal: 8,
  paddingVertical: 3,
  borderRadius: 4,
  marginLeft: 12,
},
defaultText: {fontSize: 11, color: '#fff', fontWeight: '600'},

红色背景白色文字,小巧醒目。

添加按钮的样式:

addBtn: {
  position: 'absolute',
  bottom: 32,
  left: 16,
  right: 16,
  flexDirection: 'row',
  justifyContent: 'center',
  alignItems: 'center',
  backgroundColor: '#3498db',
  paddingVertical: 16,
  borderRadius: 25,
},

绝对定位固定在底部,左右留 16 的边距,圆角做成胶囊形状。

滑动删除

当前删除要点击按钮,可以加个滑动删除的手势,更符合移动端习惯:

import {Swipeable} from 'react-native-gesture-handler';

const renderAddress = ({item}: {item: Address}) => (
  <Swipeable
    renderRightActions={() => (
      <TouchableOpacity style={styles.swipeDelete} onPress={() => handleDelete(item.id)}>
        <Text style={styles.swipeDeleteText}>删除</Text>
      </TouchableOpacity>
    )}
  >
    {/* 地址卡片内容 */}
  </Swipeable>
);

左滑显示删除按钮,点击删除。需要安装 react-native-gesture-handler 库。

地址数量限制

用户可能添加很多地址,可以设置一个上限:

const MAX_ADDRESSES = 20;

// 添加按钮
{addresses.length < MAX_ADDRESSES ? (
  <TouchableOpacity style={styles.addBtn} onPress={() => navigate('addressEdit')}>
    <Text style={styles.addBtnText}>添加新地址</Text>
  </TouchableOpacity>
) : (
  <View style={styles.limitTip}>
    <Text style={styles.limitText}>最多保存 {MAX_ADDRESSES} 个地址</Text>
  </View>
)}

达到上限后隐藏添加按钮,显示提示文字。

写在最后

地址列表页的核心是两种模式的处理:管理模式和选择模式。通过页面参数区分,点击卡片时执行不同的逻辑。

地址的增删改查都在全局状态里实现,页面只负责展示和触发操作。设置默认地址要保证只有一个默认,用 map 一次遍历搞定。

下一篇写地址编辑页,实现地址的新增和修改。


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

Logo

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

更多推荐