React Native for Harmony:地址管理页面(新增+编辑)完整实现
本次地址管理功能全程基于React Native原生能力核心 API/Hook/组件作用说明TS环境要求鸿蒙端核心特性FlatList地址列表高性能渲染载体,替代ScrollView必传data泛型+renderItem参数类型按需渲染+组件复用,千条地址无卡顿,内存占用低管理地址列表、表单数据、错误信息、加载状态,全量泛型约束显式指定类型,如状态更新无延迟,鸿蒙端响应实时TextInput收货人
·
目录
- 核心知识点:地址管理(新增+编辑) 完整核心用法
1.1 核心内置 API/Hook/组件 介绍(TS类型标注+鸿蒙适配)
1.2 鸿蒙端地址管理 核心实现原则(电商级规范)
1.3 鸿蒙端地址管理 官方设计&交互规范(硬性要求)
1.4 核心优势(TS严格类型+零崩溃+复用表单+鸿蒙适配) - 实战开发:双版本完整实现(TS全类型+单文件+复制即用+零白屏)
2.1 版本一:基础极简版 - 地址列表+新增+编辑+删除(满足80%基础场景)
2.2 版本二:企业增强版 - 默认地址单选+表单强校验+防抖提交+空数据适配(生产级可用) - OpenHarmony6.0+ 专属避坑指南(高频踩坑TOP10,鸿蒙真机实测+最优解)
- 扩展用法:地址管理高频进阶技巧(纯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
更多推荐




所有评论(0)