目录

  1. 核心知识点:地址管理(新增+编辑) 完整核心用法
    1.1 核心内置 API/Hook/组件 介绍(TS类型标注+鸿蒙适配)
    1.2 鸿蒙端地址管理 核心实现原则(电商级规范)
    1.3 鸿蒙端地址管理 官方设计&交互规范(硬性要求)
    1.4 核心优势(TS严格类型+零崩溃+复用表单+鸿蒙适配)
  2. 实战开发:双版本完整实现(TS全类型+单文件+复制即用+零白屏)
    2.1 版本一:基础极简版 - 地址列表+新增+编辑+删除(满足80%基础场景)
    2.2 版本二:企业增强版 - 默认地址单选+表单强校验+防抖提交+空数据适配(生产级可用)
  3. OpenHarmony6.0+ 专属避坑指南(高频踩坑TOP10,鸿蒙真机实测+最优解)
  4. 扩展用法:地址管理高频进阶技巧(纯RN原生+TS适配+鸿蒙兼容)

一、核心知识点:地址管理(新增+编辑) 完整核心用法

✅ 1. 核心内置 API/Hook/组件 介绍

本次地址管理功能全程基于 React Native原生能力 开发,无第三方依赖,所有API均为鸿蒙生态深度适配版本,TS全量显式类型定义,无隐式any,完美适配鸿蒙手机/平板/折叠屏,零白屏闪退风险:

核心 API/Hook/组件 作用说明 TS环境要求 鸿蒙端核心特性
FlatList 地址列表高性能渲染载体,替代ScrollView 必传data泛型+renderItem参数类型 按需渲染+组件复用,千条地址无卡顿,内存占用低
useState<T>() 管理地址列表、表单数据、错误信息、加载状态,全量泛型约束 显式指定类型,如useState<AddressInfo[]>([]) 状态更新无延迟,鸿蒙端响应实时
TextInput 收货人/手机号/地址等表单输入,单行输入(规避鸿蒙崩溃BUG) 指定onChangeText入参类型为string 鸿蒙原生键盘适配,无光标错位,输入流畅
TouchableOpacity 列表项/按钮/单选框点击容器,鸿蒙原生交互反馈 无额外类型要求,TS自动推导 水波纹效果与系统一致,无手势冲突
Alert 删除确认/操作成功提示,鸿蒙原生弹窗样式 无类型要求 弹窗样式符合鸿蒙规范,交互友好
Keyboard.dismiss() 表单提交/取消时收起键盘,提升体验 无类型要求 鸿蒙端原生键盘控制,无残留

二、实战开发:完整实现

地址管理基础功能

import React, { useState } from 'react';
import {
  View, Text, TextInput, TouchableOpacity, StyleSheet,
  SafeAreaView, Alert, FlatList, Keyboard, TouchableWithoutFeedback
} from 'react-native';

export interface AddressInfo {
  id: string;          // 地址唯一标识
  name: string;        // 收货人 必选
  phone: string;       // 手机号 必选
  province: string;    // 省份 必选
  city: string;        // 城市 必选
  area: string;        // 区县 必选
  detail: string;      // 详细地址 必选
  isDefault: boolean;  // 是否默认地址
}

export interface AddressFormError {
  name: string;
  phone: string;
  province: string;
  city: string;
  area: string;
  detail: string;
}

const PRIMARY_COLOR = '#007DFF';
const TEXT_COLOR = '#333333';
const SUB_TEXT_COLOR = '#666666';
const PLACEHOLDER_COLOR = '#999999';
const ERROR_COLOR = '#FF3B30';
const BORDER_COLOR = '#e5e5e5';
const BG_COLOR = '#f7f8fa';
const DISABLE_COLOR = '#CCCCCC';
const DEFAULT_TAG_BG = '#007DFF10';

const PHONE_REG = /^1[3-9]\d{9}$/; // 手机号正则
const INIT_EMPTY_FORM: AddressInfo = {
  id: '',
  name: '',
  phone: '',
  province: '',
  city: '',
  area: '',
  detail: '',
  isDefault: false
};
// 初始化表单错误信息
const INIT_FORM_ERROR: AddressFormError = {
  name: '', phone: '', province: '', city: '', area: '', detail: ''
};
const INIT_ADDRESS_LIST: AddressInfo[] = [
  {
    id: '1',
    name: '张三',
    phone: '13800138000',
    province: '北京市',
    city: '北京市',
    area: '朝阳区',
    detail: '建国门外大街88号',
    isDefault: true
  },
  {
    id: '2',
    name: '李四',
    phone: '13900139000',
    province: '上海市',
    city: '上海市',
    area: '浦东新区',
    detail: '陆家嘴环路1000号',
    isDefault: false
  }
];

const AddressManagerBasic = () => {
  const [addressList, setAddressList] = useState<AddressInfo[]>(INIT_ADDRESS_LIST);
  const [form, setForm] = useState<AddressInfo>(INIT_EMPTY_FORM);
  const [formError, setFormError] = useState<AddressFormError>(INIT_FORM_ERROR);
  const [loading, setLoading] = useState<boolean>(false);
  const [isEditing, setIsEditing] = useState<boolean>(false);
  const [showForm, setShowForm] = useState<boolean>(false);

  // 表单输入变更
  const handleInputChange = (key: keyof AddressInfo, value: string) => {
    setForm(prev => ({ ...prev, [key]: value }));
    setFormError(prev => ({ ...prev, [key]: '' }));
  };

  // 切换默认地址
  const toggleDefault = () => {
    setForm(prev => ({ ...prev, isDefault: !prev.isDefault }));
  };

  // 表单校验
  const validateForm = (): boolean => {
    const errors: AddressFormError = { ...INIT_FORM_ERROR };
    if (!form.name.trim()) errors.name = '收货人不能为空';
    if (!form.phone.trim()) errors.phone = '手机号不能为空';
    else if (!PHONE_REG.test(form.phone)) errors.phone = '手机号格式错误';
    if (!form.province.trim()) errors.province = '请选择省份';
    if (!form.city.trim()) errors.city = '请选择城市';
    if (!form.area.trim()) errors.area = '请选择区县';
    if (!form.detail.trim()) errors.detail = '详细地址不能为空';
    setFormError(errors);
    return Object.values(errors).every(item => item === '');
  };

  const handleSubmit = async () => {
    Keyboard.dismiss();
    const isValid = validateForm();
    if (!isValid) return;

    setLoading(true);
    try {
      await new Promise(resolve => setTimeout(resolve, 1000));
      if (isEditing) {
        const newAddressList = addressList.map(item => {
          if (item.id === form.id) return form;
          return form.isDefault ? { ...item, isDefault: false } : item;
        });
        setAddressList(newAddressList);
        Alert.alert('成功', '地址修改成功!');
      } else {
        const newAddress = { ...form, id: Date.now().toString() };
        let newAddressList = [];
        if (newAddress.isDefault) {
          newAddressList = addressList.map(item => ({ ...item, isDefault: false }));
        } else {
          newAddressList = [...addressList];
        }
        newAddressList.push(newAddress);
        setAddressList(newAddressList);
        Alert.alert('成功', '地址新增成功!');
      }
      resetForm();
    } catch (error) {
      Alert.alert('提示', '操作失败,请重试');
    } finally {
      setLoading(false);
    }
  };

  // 编辑地址(数据完美回显)
  const handleEdit = (item: AddressInfo) => {
    setForm({ ...item });
    setIsEditing(true);
    setFormError(INIT_FORM_ERROR);
    setShowForm(true); // ✅ 修复BUG1 编辑时强制显示表单
  };

  // 删除地址
  const handleDelete = (id: string) => {
    Alert.alert('确认删除', '是否删除该地址?', [
      { text: '取消', style: 'cancel' },
      { text: '确认', onPress: () => {
        setAddressList(prev => prev.filter(item => item.id !== id));
        // 删除后如果是当前编辑的地址,重置表单
        if (form.id === id) resetForm();
      }}
    ]);
  };

  const resetForm = () => {
    setForm(INIT_EMPTY_FORM);
    setIsEditing(false);
    setFormError(INIT_FORM_ERROR);
    setShowForm(false);
    Keyboard.dismiss();
  };

  // 渲染地址列表项
  const renderAddressItem = ({ item }: { item: AddressInfo }) => {
    return (
      <View style={styles.addressItem}>
        <View style={styles.addressLeft}>
          <View style={styles.namePhoneRow}>
            <Text style={styles.nameText}>{item.name}</Text>
            <Text style={styles.phoneText}>{item.phone}</Text>
            {item.isDefault && (
              <View style={styles.defaultTag}>
                <Text style={styles.defaultTagText}>默认</Text>
              </View>
            )}
          </View>
          <Text style={styles.addressText}>
            {item.province}{item.city}{item.area}{item.detail}
          </Text>
        </View>
        <View style={styles.operateBox}>
          <TouchableOpacity onPress={() => handleEdit(item)}>
            <Text style={styles.operateText}>编辑</Text>
          </TouchableOpacity>
          <TouchableOpacity onPress={() => handleDelete(item.id)}>
            <Text style={[styles.operateText, { color: ERROR_COLOR }]}>删除</Text>
          </TouchableOpacity>
      </View>
      </View>
    );
  };

  return (
    <TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
      <SafeAreaView style={styles.container}>
        <Text style={styles.pageTitle}>地址管理</Text>

        <FlatList
          data={addressList}
          renderItem={renderAddressItem}
          keyExtractor={item => item.id}
          showsVerticalScrollIndicator={false}
          contentContainerStyle={styles.listContent}
          bounces={false}
          extraData={addressList}
          ListEmptyComponent={() => (
            <View style={styles.emptyBox}>
              <Text style={styles.emptyText}>暂无保存的地址</Text>
            </View>
          )}
        />

        <TouchableOpacity
          style={styles.addBtn}
          onPress={() => {
            setShowForm(true);
            setIsEditing(false);
            setFormError(INIT_FORM_ERROR);
          }}
          disabled={loading}
        >
          <Text style={styles.addBtnText}>+ 新增地址</Text>
        </TouchableOpacity>

        {showForm && (
          <View style={styles.formContainer}>
            <Text style={styles.formTitle}>{isEditing ? '编辑地址' : '新增地址'}</Text>

            {/* 收货人 */}
            <View style={styles.formItem}>
              <Text style={styles.formLabel}>收货人</Text>
              <TextInput
                style={styles.formInput}
                value={form.name}
                placeholder="请输入收货人姓名"
                placeholderTextColor={PLACEHOLDER_COLOR}
                maxLength={10}
                editable={!loading}
                onChangeText={(v) => handleInputChange('name', v)}
              />
            </View>
            {formError.name && <Text style={styles.errorText}>{formError.name}</Text>}

            {/* 手机号 */}
            <View style={styles.formItem}>
              <Text style={styles.formLabel}>手机号</Text>
              <TextInput
                style={styles.formInput}
                value={form.phone}
                placeholder="请输入手机号"
                placeholderTextColor={PLACEHOLDER_COLOR}
                keyboardType="phone-pad"
                maxLength={11}
                editable={!loading}
                onChangeText={(v) => handleInputChange('phone', v)}
              />
            </View>
            {formError.phone && <Text style={styles.errorText}>{formError.phone}</Text>}

            {/* 省份 */}
            <View style={styles.formItem}>
              <Text style={styles.formLabel}>省份</Text>
              <TextInput
                style={styles.formInput}
                value={form.province}
                placeholder="请输入省份"
                placeholderTextColor={PLACEHOLDER_COLOR}
                editable={!loading}
                onChangeText={(v) => handleInputChange('province', v)}
              />
            </View>
            {formError.province && <Text style={styles.errorText}>{formError.province}</Text>}

            {/* 城市 */}
            <View style={styles.formItem}>
              <Text style={styles.formLabel}>城市</Text>
              <TextInput
                style={styles.formInput}
                value={form.city}
                placeholder="请输入城市"
                placeholderTextColor={PLACEHOLDER_COLOR}
                editable={!loading}
                onChangeText={(v) => handleInputChange('city', v)}
              />
            </View>
            {formError.city && <Text style={styles.errorText}>{formError.city}</Text>}

            {/* 区县 */}
            <View style={styles.formItem}>
              <Text style={styles.formLabel}>区县</Text>
              <TextInput
                style={styles.formInput}
                value={form.area}
                placeholder="请输入区县"
                placeholderTextColor={PLACEHOLDER_COLOR}
                editable={!loading}
                onChangeText={(v) => handleInputChange('area', v)}
              />
            </View>
            {formError.area && <Text style={styles.errorText}>{formError.area}</Text>}

            {/* 详细地址 */}
            <View style={styles.formItem}>
              <Text style={styles.formLabel}>详细地址</Text>
              <TextInput
                style={styles.formInput}
                value={form.detail}
                placeholder="请输入详细地址"
                placeholderTextColor={PLACEHOLDER_COLOR}
                maxLength={50}
                editable={!loading}
                onChangeText={(v) => handleInputChange('detail', v)}
              />
            </View>
            {formError.detail && <Text style={styles.errorText}>{formError.detail}</Text>}

            {/* 默认地址单选 */}
            <View style={styles.defaultRow}>
              <TouchableOpacity
                style={styles.radioBox}
                onPress={toggleDefault}
                disabled={loading}
              >
                {form.isDefault && <View style={styles.radioActive} />}
              </TouchableOpacity>
              <Text style={styles.defaultText}>设为默认地址</Text>
            </View>

            {/* 表单按钮区 */}
            <View style={styles.formBtnBox}>
              <TouchableOpacity
                style={[styles.submitBtn, loading && styles.submitBtnDisabled]}
                onPress={handleSubmit}
                disabled={loading}
              >
                <Text style={styles.submitBtnText}>{loading ? '提交中...' : '保存地址'}</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={styles.cancelBtn}
                onPress={resetForm}
                disabled={loading}
              >
                <Text style={styles.cancelBtnText}>取消</Text>
              </TouchableOpacity>
            </View>
          </View>
        )}
      </SafeAreaView>
    </TouchableWithoutFeedback>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: BG_COLOR,
    paddingHorizontal: 16,
  },
  pageTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: TEXT_COLOR,
    paddingVertical: 16,
    borderBottomWidth: 1,
    borderBottomColor: BORDER_COLOR,
  },
  listContent: {
    paddingVertical: 8,
  },
  addressItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'flex-start',
    padding: 12,
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    marginBottom: 8,
  },
  addressLeft: {
    flex: 1,
  },
  namePhoneRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 4,
  },
  nameText: {
    fontSize: 16,
    color: TEXT_COLOR,
    fontWeight: '500',
    marginRight: 12,
  },
  phoneText: {
    fontSize: 14,
    color: SUB_TEXT_COLOR,
  },
  defaultTag: {
    marginLeft: 8,
    paddingHorizontal: 6,
    paddingVertical: 2,
    borderRadius: 4,
    backgroundColor: DEFAULT_TAG_BG,
  },
  defaultTagText: {
    fontSize: 12,
    color: PRIMARY_COLOR,
  },
  addressText: {
    fontSize: 14,
    color: SUB_TEXT_COLOR,
    lineHeight: 20,
  },
  operateBox: {
    flexDirection: 'column',
    alignItems: 'flex-end',
    gap: 8,
  },
  operateText: {
    fontSize: 14,
    color: SUB_TEXT_COLOR,
  },
  emptyBox: {
    justifyContent: 'center',
    alignItems: 'center',
    paddingVertical: 60,
  },
  emptyText: {
    fontSize: 16,
    color: SUB_TEXT_COLOR,
  },
  addBtn: {
    backgroundColor: '#FFFFFF',
    borderWidth: 1,
    borderColor: PRIMARY_COLOR,
    borderRadius: 25,
    paddingVertical: 14,
    alignItems: 'center',
    marginVertical: 16,
  },
  addBtnText: {
    fontSize: 16,
    color: PRIMARY_COLOR,
    fontWeight: '500',
  },
  formContainer: {
    marginTop: 16,
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    padding: 16,
  },
  formTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: TEXT_COLOR,
    marginBottom: 16,
  },
  formItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  formLabel: {
    fontSize: 16,
    color: TEXT_COLOR,
    width: 80,
  },
  formInput: {
    flex: 1,
    fontSize: 16,
    color: TEXT_COLOR,
    textAlign: 'right',
  },
  errorText: {
    fontSize: 12,
    color: ERROR_COLOR,
    marginLeft: 80,
    marginTop: 4,
  },
  defaultRow: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 12,
  },
  radioBox: {
    width: 20,
    height: 20,
    borderRadius: 10,
    borderWidth: 1,
    borderColor: BORDER_COLOR,
    justifyContent: 'center',
    alignItems: 'center',
    marginRight: 8,
  },
  radioActive: {
    width: 12,
    height: 12,
    borderRadius: 6,
    backgroundColor: PRIMARY_COLOR,
  },
  defaultText: {
    fontSize: 16,
    color: TEXT_COLOR,
  },
  formBtnBox: {
    flexDirection: 'row',
    gap: 12,
    marginTop: 20,
  },
  submitBtn: {
    flex: 1,
    backgroundColor: PRIMARY_COLOR,
    borderRadius: 25,
    paddingVertical: 14,
    alignItems: 'center',
  },
  submitBtnDisabled: {
    backgroundColor: DISABLE_COLOR,
  },
  submitBtnText: {
    fontSize: 16,
    color: '#FFFFFF',
    fontWeight: '500',
  },
  cancelBtn: {
    flex: 1,
    borderWidth: 1,
    borderColor: BORDER_COLOR,
    borderRadius: 25,
    paddingVertical: 14,
    alignItems: 'center',
  },
  cancelBtnText: {
    fontSize: 16,
    color: TEXT_COLOR,
  },
});

export default AddressManagerBasic;

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、OpenHarmony6.0+ 专属避坑指南

问题现象 核心问题原因 鸿蒙端最优解决方案 (复制即用)
进入页面白屏+自动退出 表单包含multiline多行输入框,鸿蒙RN致命BUG 删除multiline属性,所有输入框用单行,详细地址通过maxLength限制长度
编辑地址时,默认地址切换后其他地址未取消 未处理编辑时的默认地址逻辑,仅新增时处理 编辑提交时,判断form.isDefault为true,遍历列表取消其他地址默认状态
表单提交后,地址列表不刷新 FlatList未监听数据源变化,组件复用导致 给FlatList添加extraData={[addressList, selectedIds]},强制监听数据更新
TS报「隐式any」错误,编译失败 未定义地址/表单错误接口,变量无类型 定义AddressInfo/AddressFormError接口,所有状态显式指定类型,如useState<AddressInfo[]>([])
批量删除时,选中状态不更新 选中ID数组未作为FlatList的extraData依赖项 extraData中添加selectedIds,确保选中状态变化时列表重新渲染
输入框失焦后,校验错误提示不显示 未绑定onBlur事件,仅提交时校验 给每个TextInput添加onBlur={validateForm},失焦时触发校验,提升体验
鸿蒙平板上,地址列表项布局错位 硬编码宽度,平板屏幕适配性差 flex布局替代硬编码宽度,地址列表项宽度用Dimensions.get('window').width - 32自适应
提交按钮快速点击,导致重复添加地址 无loading状态锁,重复触发提交 提交时setLoading(true),禁用按钮,请求结束后setLoading(false)
删除默认地址后,表单默认状态未重置 未判断删除的是否为默认地址,表单状态残留 删除时判断if (id === form.id),同步重置表单为初始状态
深色模式下,默认地址标签看不清 标签颜色跟随主题切换,违反鸿蒙规范 标签背景色固定#007DFF10,文字色固定#007DFF,深浅模式不修改

四、扩展用法:地址管理高频进阶技巧

✅ 扩展1:地址选择器(省市区三级联动)

集成react-native-picker(鸿蒙兼容版),替换手动输入省市区,实现三级联动选择,TS指定选择器返回值类型为{ province: string; city: string; area: string },无类型错误。

✅ 扩展2:地址缓存持久化(重启不丢失)

使用@react-native-async-storage/async-storage,组件挂载时读取本地缓存的地址列表,新增/编辑/删除时同步更新缓存,TS指定存储类型为AddressInfo[]

✅ 扩展3:地址分享功能

添加「分享地址」按钮,点击后将地址信息拼接为字符串(如“收货人:张三 手机号:13800138000 地址:北京市朝阳区建国门外大街88号”),调用RN原生分享API,鸿蒙端适配原生分享面板。


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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐