基于React Native鸿蒙跨平台实现删除地址时增加边界校验,确保至少保留一个地址,退出登录采用二次确认的交互模式
个人中心模块是移动端应用的核心功能区,承担着用户信息管理、地址管理、系统设置等关键交互场景的承载作用。这份 React Native 个人资料完善应用代码,以精细化状态管理为核心,结合内联编辑交互模式、组件化卡片布局、响应式设计的思路,完整实现了现代移动端个人中心的全流程体验,同时具备清晰的跨端适配架构。本文将从 React Native 原生技术实现、个人中心核心交互设计、鸿蒙(HarmonyOS)跨端适配全方案 三个维度,深度拆解这款个人中心应用的技术内核,并提供可落地的鸿蒙迁移实践指南。
1. 分层状态
个人中心的核心是用户数据的精准管理,应用基于 TypeScript 定义双层强类型数据模型:
UserProfile接口规范用户核心信息字段(昵称、头像、性别、生日等),覆盖个人资料的全维度信息;Address接口定义收货地址数据结构,包含默认地址标记,满足地址管理的核心需求。
这种结构化设计不仅在编码阶段规避字段名错误,更重要的是为跨端适配提供了统一的数据标准,确保两端数据流转的一致性。
状态管理层面,应用采用 useState 实现分层的状态控制:
profile:管理用户核心信息,是个人中心的基础数据层;addresses:管理收货地址列表,支持多地址存储和默认地址标记;editingField:标记当前正在编辑的字段名称,是内联编辑模式的核心控制状态;editValue:存储编辑过程中的临时值,避免直接修改原数据导致的状态污染。
状态更新严格遵循不可变更新原则,例如更新用户信息时通过扩展运算符(...prev)复制原对象,仅修改目标字段:
setProfile(prev => ({
...prev,
[editingField]: editValue
}));
这种设计保证了状态变更的可追溯性,避免直接修改引用类型导致的渲染异常,是 React 状态管理的核心最佳实践。
2. 内联编辑
个人中心的核心交互是用户信息的编辑,应用采用内联编辑(Inline Edit) 模式,区别于传统的跳转式编辑,大幅提升操作效率:
(1)编辑状态
通过 editingField 状态标记当前编辑字段(如 nickname/gender/birthday),实现“查看-编辑”模式的无缝切换:
- 非编辑状态:展示字段值 + 编辑按钮,保持界面简洁;
- 编辑状态:替换为输入框 + 保存/取消按钮,聚焦当前编辑操作;
- 不同字段采用差异化的编辑控件布局:昵称编辑使用横向展开的输入框,性别/生日等短字段使用紧凑型内联输入框,兼顾不同字段的输入体验。
(2)编辑流程
编辑交互遵循“开始-编辑-保存/取消”的完整闭环:
startEditing:初始化编辑状态,记录当前字段值到editValue,避免编辑过程中污染原数据;saveEdit:提交编辑内容,更新profile状态,清空编辑标记,给出成功提示;cancelEdit:取消编辑,清空编辑状态,恢复原数据,无任何数据变更。
这种设计保证了编辑操作的原子性,即使编辑过程中误操作,也不会导致原数据损坏,提升了用户操作的安全感。
3. 地址管理
收货地址管理是个人中心的高频场景,应用实现了完整的地址操作逻辑:
(1)默认地址管理
setDefaultAddress 函数通过遍历地址列表,仅将目标地址标记为默认,其余地址取消默认标记:
const updatedAddresses = addresses.map(addr => ({
...addr,
isDefault: addr.id === id
}));
这种实现方式保证了唯一默认地址的业务规则,避免出现多个默认地址的异常场景。
(2)地址操作
删除地址时增加边界校验,确保至少保留一个地址:
if (addresses.length <= 1) {
Alert.alert('提示', '至少需要保留一个地址');
return;
}
这种防御性编程思路,避免了业务逻辑层面的异常,提升了应用的健壮性。
(3)地址列表
默认地址通过 defaultTag 标签进行视觉标记,操作按钮根据默认状态动态调整文本(“默认地址”/“设为默认”)和样式,让用户清晰感知当前地址状态。
4. 响应式布局
个人中心作为高频使用的模块,视觉体验和适配性直接影响用户留存,应用在布局设计上体现了以下技术亮点:
(1)卡片式组件
整个页面采用卡片式设计,将不同功能模块(用户信息、个人资料、收货地址、系统设置)拆分为独立的卡片组件:
- 每个卡片使用统一的样式(白色背景 + 圆角 + 阴影),
elevation/shadow双配置兼顾安卓/iOS 平台的阴影效果; - 卡片间保持统一的间距,视觉层次清晰,符合现代移动端 UI 设计趋势;
- 卡片内部通过
divider分隔不同字段,保持布局的规整性。
(2)自适应尺寸
通过 Dimensions.get('window') 获取屏幕尺寸,结合 Flex 弹性布局实现全设备适配:
- 用户头像采用固定宽高比(80x80)+ 圆形裁剪(
borderRadius: 40),保证在不同屏幕上的视觉一致性; - 信息行采用
flex: 1/flex: 2的比例分配标签和值的宽度,避免内容挤压; - 地址操作按钮采用固定内边距 + 自适应宽度,在小屏设备上自动压缩,保证功能完整性;
- 服务条款区域使用
flexWrap: 'wrap',避免文本超出屏幕宽度导致的截断。
(3)交互
- 按钮状态差异化:默认地址按钮使用主色调(
#3b82f6),删除按钮使用警示色(#fecaca),普通操作按钮使用中性色,视觉权重区分明显; - 编辑状态反馈:编辑模式下输入框增加边框,保存/取消按钮使用绿/红对比色,操作意图清晰;
- 点击反馈:所有可点击元素(编辑按钮、设置项、地址操作)均有明确的视觉标识,如箭头、下划线、背景色变化;
- 提示反馈:关键操作(保存信息、设置默认地址、退出登录)均通过
Alert.alert给出明确的弹窗提示,保证用户感知。
(1)退出登录
退出登录采用二次确认的交互模式,通过 Alert.alert 的按钮配置实现:
Alert.alert(
'确认退出',
'您确定要退出登录吗?',
[
{ text: '取消', style: 'cancel' },
{ text: '确定', onPress: () => Alert.alert('退出', '已退出登录') }
]
);
这种设计避免了误触导致的退出操作,符合用户操作习惯。
(2)系统设置
系统设置模块采用“文本 + 箭头”的通用布局,每个设置项都是独立的可点击组件,为后续跳转到具体设置页面预留了扩展接口,保持了代码的可扩展性。
将 React Native 个人中心应用迁移至鸿蒙平台,核心是基于 ArkTS + ArkUI 实现数据模型、状态管理、内联编辑交互、UI 组件的对等还原,以下是关键适配技术点:
1. 数据模型
RN 的 TypeScript 类型定义可无缝迁移至鸿蒙 ArkTS,仅需将 type 替换为 interface,核心字段完全复用:
// RN 类型定义
type UserProfile = {
nickname: string;
avatar: string;
gender: string;
birthday: string;
phone: string;
email: string;
address: string;
bio: string;
};
type Address = {
id: string;
name: string;
phone: string;
address: string;
isDefault: boolean;
};
// 鸿蒙 ArkTS 适配
interface UserProfile {
nickname: string;
avatar: string;
gender: string;
birthday: string;
phone: string;
email: string;
address: string;
bio: string;
}
interface Address {
id: string;
name: string;
phone: string;
address: string;
isDefault: boolean;
}
状态管理层面,RN 的 useState 对应鸿蒙的 @State 装饰器,核心状态迁移方案:
// RN 状态定义
const [profile, setProfile] = useState<UserProfile>({/* 初始值 */});
const [addresses, setAddresses] = useState<Address[]>([/* 初始地址 */]);
const [editingField, setEditingField] = useState<string | null>(null);
const [editValue, setEditValue] = useState<string>('');
// 鸿蒙 ArkTS 适配
@Entry
@Component
struct ProfileApp {
// 用户信息核心状态
@State profile: UserProfile = {
nickname: '小明',
avatar: 'https://via.placeholder.com/100',
gender: '男',
birthday: '1990-01-01',
phone: '138****8888',
email: 'xiaoming@example.com',
address: '北京市朝阳区某某街道123号',
bio: '热爱生活,积极向上'
};
// 地址列表状态
@State addresses: Address[] = [
{ id: '1', name: '小明', phone: '138****8888', address: '北京市朝阳区某某街道123号', isDefault: true },
{ id: '2', name: '小明', phone: '138****8888', address: '上海市浦东新区某某路456号', isDefault: false },
];
// 编辑状态控制
@State editingField: string | null = null;
@State editValue: string = '';
// 编辑交互函数(逻辑完全复用)
startEditing(field: string, currentValue: string) {
this.editingField = field;
this.editValue = currentValue;
}
saveEdit() {
if (this.editingField) {
// 鸿蒙直接修改对象属性,无需扩展运算符
this.profile[this.editingField] = this.editValue;
this.editingField = null;
this.editValue = '';
// 弹窗适配
AlertDialog.show({
title: '成功',
message: '信息已更新',
confirm: { text: '确定' }
});
}
}
// 其他交互函数(cancelEdit/setDefaultAddress/deleteAddress)完全复用...
}
适配亮点:
- 状态更新逻辑简化:ArkTS 支持直接修改
@State装饰的对象属性,无需像 RN 那样使用扩展运算符复制整个对象,代码更简洁; - 弹窗反馈适配:将 RN 的
Alert.alert替换为鸿蒙AlertDialog.show,参数结构基本一致,仅需调整配置格式; - 数组操作兼容:地址列表的
map/filter等操作在 ArkTS 中完全生效,默认地址设置、地址删除等逻辑零修改复用。
ArkUI 组件体系与 RN 组件高度对齐,核心组件映射关系及适配方案:
| RN 组件/特性 | 鸿蒙 ArkUI 对应实现 | 适配关键说明 |
|---|---|---|
SafeAreaView |
Column().safeArea(true) |
安全区域适配,避免状态栏/导航栏遮挡 |
ScrollView |
Scroll() |
滚动容器完全等效,支持纵向滚动 |
TouchableOpacity |
Button().stateEffect(true) |
点击透明度反馈通过 stateEffect 实现 |
View |
Column/Row/Stack |
根据布局方向选择,Flex 布局属性完全兼容 |
TextInput |
TextInput |
输入框核心属性等效,autoFocus → focus(true) |
Image |
Image |
图片加载、圆角裁剪完全兼容 |
| 条件渲染 | if/else 语句 |
替代 RN 的三元表达式,实现编辑/查看模式切换 |
| 列表渲染 | ForEach |
替代 RN 的 map,实现地址列表的动态生成 |
(1)昵称内联编辑
// RN 昵称编辑
<View style={styles.nicknameContainer}>
{editingField === 'nickname' ? (
<View style={styles.editInputContainer}>
<TextInput
style={styles.editInput}
value={editValue}
onChangeText={setEditValue}
autoFocus
/>
<View style={styles.editButtons}>
<TouchableOpacity style={styles.saveButton} onPress={saveEdit}>
<Text style={styles.saveButtonText}>保存</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.cancelButton} onPress={cancelEdit}>
<Text style={styles.cancelButtonText}>取消</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={styles.displayNameContainer}>
<Text style={styles.displayName}>{profile.nickname}</Text>
<TouchableOpacity
style={styles.editIcon}
onPress={() => startEditing('nickname', profile.nickname)}
>
<Text style={styles.editText}>编辑</Text>
</TouchableOpacity>
</View>
)}
</View>
// 鸿蒙昵称编辑适配
Row({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
if (this.editingField === 'nickname') {
// 编辑模式
Column() {
TextInput({
text: this.editValue,
placeholder: '请输入昵称'
})
.borderWidth(1)
.borderColor('#cbd5e1')
.borderRadius(6)
.padding(8)
.fontSize(16)
.onChange((value) => {
this.editValue = value;
})
.focus(true); // 自动聚焦适配
Row({ space: 8 }) {
Button('保存')
.backgroundColor('#10b981')
.paddingHorizontal(10)
.paddingVertical(6)
.borderRadius(4)
.fontSize(12)
.fontColor('#ffffff')
.onClick(() => {
this.saveEdit();
});
Button('取消')
.backgroundColor('#ef4444')
.paddingHorizontal(10)
.paddingVertical(6)
.borderRadius(4)
.fontSize(12)
.fontColor('#ffffff')
.onClick(() => {
this.cancelEdit();
});
}
}
.flex(1);
} else {
// 查看模式
Row({ alignItems: ItemAlign.Center }) {
Text(this.profile.nickname)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Button('编辑')
.backgroundColor(Color.Transparent)
.fontSize(14)
.fontColor('#3b82f6')
.padding(4)
.marginLeft(12)
.onClick(() => {
this.startEditing('nickname', this.profile.nickname);
});
}
}
}
(2)地址列表
// RN 地址列表渲染
{addresses.map(address => (
<View key={address.id} style={styles.addressItem}>
{/* 地址内容... */}
</View>
))}
// 鸿蒙地址列表渲染适配
ForEach(this.addresses, (address: Address) => {
Column()
.width('100%')
.marginBottom(16)
.paddingBottom(16)
.borderBottomWidth(1)
.borderBottomColor('#e2e8f0') {
// 地址头部(姓名 + 默认标签)
Row({ alignItems: ItemAlign.Center, space: 8 }) {
Text(address.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
if (address.isDefault) {
Text('默认')
.fontSize(10)
.fontColor('#3b82f6')
.fontWeight(FontWeight.Bold)
.backgroundColor('#dbeafe')
.paddingHorizontal(6)
.paddingVertical(2)
.borderRadius(4);
}
}
.marginBottom(4);
// 手机号和地址
Text(address.phone)
.fontSize(14)
.fontColor('#64748b')
.marginBottom(4);
Text(address.address)
.fontSize(14)
.fontColor('#475569')
.lineHeight(20)
.marginBottom(12);
// 地址操作按钮
Row({ space: 8 }) {
Button(address.isDefault ? '默认地址' : '设为默认')
.backgroundColor(address.isDefault ? '#3b82f6' : '#e2e8f0')
.fontColor(address.isDefault ? '#ffffff' : '#475569')
.fontSize(12)
.paddingHorizontal(12)
.paddingVertical(6)
.borderRadius(6)
.onClick(() => {
this.setDefaultAddress(address.id);
});
Button('编辑')
.backgroundColor('#e2e8f0')
.fontColor('#475569')
.fontSize(12)
.paddingHorizontal(12)
.paddingVertical(6)
.borderRadius(6)
.onClick(() => {
this.editAddress(address.id);
});
Button('删除')
.backgroundColor('#fecaca')
.fontColor('#dc2626')
.fontSize(12)
.paddingHorizontal(12)
.paddingVertical(6)
.borderRadius(6)
.onClick(() => {
this.deleteAddress(address.id);
});
}
}
})
(3)退出登录
// RN 退出登录
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>退出登录</Text>
</TouchableOpacity>
// 鸿蒙退出登录适配
Button('退出登录')
.backgroundColor('#ef4444')
.paddingVertical(16)
.borderRadius(10)
.width('100%')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
.marginTop(10)
.marginBottom(20)
.onClick(() => {
AlertDialog.show({
title: '确认退出',
message: '您确定要退出登录吗?',
cancel: {
text: '取消',
action: () => {}
},
confirm: {
text: '确定',
action: () => {
AlertDialog.show({
title: '退出',
message: '已退出登录',
confirm: { text: '确定' }
});
}
}
});
});
3. 样式
RN 的 StyleSheet 样式在鸿蒙中通过链式样式 + @Styles 装饰器实现复用,核心适配规则:
// RN 样式定义
const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
},
});
// 鸿蒙样式适配
@Entry
@Component
struct ProfileApp {
// 通用卡片样式封装
@Styles cardStyle() {
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(16)
.marginBottom(16)
.shadow({ // 统一阴影配置
color: '#000',
offsetX: 0,
offsetY: 1,
opacity: 0.1,
radius: 2
});
}
// 通用信息行样式
@Styles infoRowStyle() {
.flexDirection(FlexDirection.Row)
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(ItemAlign.Center)
.paddingVertical(12);
}
build() {
Column() {
// 个人资料卡片使用通用样式
Column()
.cardStyle() {
// 信息行使用通用样式
Row()
.infoRowStyle() {
// 信息内容...
}
}
}
}
}
适配亮点:
- 基础样式完全复用:颜色、间距、圆角、字体大小等属性直接迁移,仅调整语法格式(如
fontWeight: 'bold'→fontWeight(FontWeight.Bold)); - 布局属性对齐:
flexDirection: 'row'→flexDirection(FlexDirection.Row),语义完全一致; - 阴影效果统一:鸿蒙的
shadow配置替代 RN 的elevation/shadow双配置,一次配置兼顾所有平台; - 条件样式适配:通过三元表达式实现动态样式,如默认地址按钮的背景色切换,逻辑与 RN 完全一致。
1. 核心
- 数据模型优先复用:用户信息、收货地址等核心数据模型完全复用,仅调整类型定义语法,保证两端数据逻辑一致;
- 交互逻辑零修改:内联编辑、地址管理、退出登录等核心交互逻辑,仅需调整状态更新语法和弹窗 API,业务逻辑完全复用;
- 视觉体验统一:复用相同的色值、间距、圆角、字体,保证两端个人中心的视觉体验一致;
- 原生能力增强:鸿蒙端可利用原生 API 增强个人中心功能,如通过
ImagePicker实现头像选择,通过Preferences持久化存储用户信息。
2. 扩展
- 头像上传功能:RN 端使用
ImagePicker库,鸿蒙端接入系统ImagePickerAPI,实现真实的头像更换功能; - 数据持久化:RN 端使用
AsyncStorage,鸿蒙端使用Preferences,实现用户信息和地址的本地存储; - 表单校验增强:鸿蒙端利用正则表达式 API,实现手机号、邮箱、生日等字段的格式校验;
- 地址选择优化:接入鸿蒙的地图 API,实现省市区三级联动选择,替代手动输入地址;
- 生物识别:接入鸿蒙的指纹/人脸认证 API,增强退出登录的安全性;
- 多语言适配:基于鸿蒙的国际化能力,实现个人中心的多语言支持。
- 状态驱动是核心:个人中心的核心是用户信息的状态管理,RN 的
useState与鸿蒙的@State均可实现精准的状态控制,内联编辑的核心逻辑完全复用; - 组件映射高度对齐:ArkUI 的
TextInput/Button/Row/Column/Image与 RN 组件一一对应,布局与样式可低成本迁移; - 交互逻辑零重构:内联编辑、地址管理、退出登录等核心交互逻辑,仅需调整 API 调用方式,业务逻辑无需重构;
- 体验一致性优先:保持相同的视觉设计和交互反馈,是个人中心跨端适配的关键用户体验保障。
这款 React Native 个人中心应用的跨端适配实践,验证了 ArkTS 与 React 技术体系的高度兼容性。对于个人中心这类数据密集型、交互精细化的场景,核心的状态管理、交互逻辑、UI 渲染均可实现 90% 以上的代码复用,仅需适配 UI 组件和原生 API 调用,是跨端开发的高效路径。
在移动应用开发中,个人资料管理是用户高频使用的功能模块,其设计质量直接影响用户体验和应用留存率。本文将深入分析一个基于 React Native 实现的个人资料管理系统,探讨其架构设计、技术实现以及鸿蒙跨端适配策略。
状态管理
该系统采用了 React Hooks 中的 useState 进行状态管理,构建了多层次的状态模型:
const [profile, setProfile] = useState<UserProfile>({
nickname: '小明',
avatar: ' `https://via.placeholder.com/100` ',
gender: '男',
birthday: '1990-01-01',
phone: '138****8888',
email: 'xiaoming@example.com',
address: '北京市朝阳区某某街道123号',
bio: '热爱生活,积极向上'
});
const [addresses, setAddresses] = useState<Address[]>([
{ id: '1', name: '小明', phone: '138****8888', address: '北京市朝阳区某某街道123号', isDefault: true },
{ id: '2', name: '小明', phone: '138****8888', address: '上海市浦东新区某某路456号', isDefault: false },
]);
const [editingField, setEditingField] = useState<string | null>(null);
const [editValue, setEditValue] = useState<string>('');
这种状态管理方式具有以下优势:
- 类型安全:通过 TypeScript 接口
UserProfile和Address确保状态结构一致性 - 分层管理:将用户基本信息、地址信息和编辑状态分离管理
- 响应式:状态变更自动触发组件重渲染
- 跨端兼容:React Hooks 在鸿蒙系统的 React Native 实现中通常都有良好支持
实时编辑
系统实现了就地编辑功能,允许用户直接在界面上修改信息:
const startEditing = (field: string, currentValue: string) => {
setEditingField(field);
setEditValue(currentValue);
};
const saveEdit = () => {
if (editingField) {
setProfile(prev => ({
...prev,
[editingField]: editValue
}));
setEditingField(null);
setEditValue('');
Alert.alert('成功', '信息已更新');
}
};
这种编辑方式提供了流畅的用户体验:
- 点击编辑后直接在原位置显示输入框
- 支持保存和取消操作
- 编辑完成后自动恢复显示模式
地址管理
系统实现了完整的地址管理功能:
const setDefaultAddress = (id: string) => {
const updatedAddresses = addresses.map(addr => ({
...addr,
isDefault: addr.id === id
}));
setAddresses(updatedAddresses);
Alert.alert('成功', '默认地址已设置');
};
地址管理支持:
- 设置默认地址
- 添加新地址
- 编辑现有地址
- 删除地址(保留至少一个地址)
基础架构
该实现采用了 React Native 核心组件库,确保了在鸿蒙系统上的基本兼容性:
SafeAreaView:适配刘海屏等异形屏ScrollView:处理内容滚动TouchableOpacity:提供触摸反馈TextInput:提供文本输入功能Image:显示用户头像Alert:系统级弹窗提示
Base64 图标
系统使用 Base64 编码的图标库:
const ICONS_BASE64 = {
profile: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
// 其他图标...
};
这种处理方式在跨端开发中尤为重要:
- 避免了不同平台对资源文件格式的兼容性问题
- 减少了网络请求,提高了加载速度
- 简化了构建流程,无需处理多平台资源文件
屏幕尺寸
系统通过 Dimensions API 获取屏幕尺寸:
const { width, height } = Dimensions.get('window');
这种方式确保了在不同屏幕尺寸的设备上都能获得一致的布局体验,为后续的响应式布局调整做准备。
组件采用了模块化的样式定义:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
// 其他样式...
});
这种方式确保了样式的一致性和可维护性,同时为后续的主题定制和深色模式适配预留了扩展空间。
API 兼容性
在鸿蒙系统上使用 React Native 时,应注意以下 API 兼容性问题:
- Image API:鸿蒙系统的 Image 实现可能与 React Native 有所差异,建议测试确认图片加载行为
- Alert API:鸿蒙系统的 Alert 实现可能与 React Native 有所差异,建议测试确认弹窗行为
- TextInput API:鸿蒙系统的 TextInput 实现可能与 React Native 有所差异,建议测试确认输入行为
-
类型定义:
// 更详细的类型定义 interface UserProfile { nickname: string; avatar: string; gender: '男' | '女' | '其他'; birthday: string; phone: string; email: string; address: string; bio: string; } interface Address { id: string; name: string; phone: string; address: string; isDefault: boolean; } -
状态管理:
// 使用 useReducer 管理复杂状态 const [state, dispatch] = useReducer(profileReducer, { profile: initialProfile, addresses: initialAddresses, editingField: null, editValue: '' }); -
表单验证:
// 添加表单验证 const validateField = (field: string, value: string): boolean => { switch (field) { case 'email': return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); case 'phone': return /^1[3-9]\d{9}$/.test(value); default: return true; } }; -
错误处理:
// 更健壮的错误处理 const saveEdit = () => { if (editingField) { if (!validateField(editingField, editValue)) { Alert.alert('错误', '输入格式不正确'); return; } // 保存逻辑 } }; -
可访问性:
// 添加可访问性标签 <TouchableOpacity accessible={true} accessibilityLabel="编辑昵称" // 其他属性 />
本个人资料管理系统实现了一个功能完整、用户友好的个人信息管理界面,通过合理的架构设计和代码组织,为用户提供了良好的使用体验。在跨端开发场景下,该实现充分考虑了 React Native 和鸿蒙系统的兼容性需求,为后续的功能扩展和性能优化预留了空间。
真实演示案例代码:
// App.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, TextInput, Image } from 'react-native';
// Base64 图标库
const ICONS_BASE64 = {
profile: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
edit: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
address: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
settings: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
logout: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
save: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
more: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};
const { width, height } = Dimensions.get('window');
// 用户信息类型
type UserProfile = {
nickname: string;
avatar: string;
gender: string;
birthday: string;
phone: string;
email: string;
address: string;
bio: string;
};
// 收货地址类型
type Address = {
id: string;
name: string;
phone: string;
address: string;
isDefault: boolean;
};
// 个人资料完善应用组件
const ProfileApp: React.FC = () => {
const [profile, setProfile] = useState<UserProfile>({
nickname: '小明',
avatar: 'https://via.placeholder.com/100',
gender: '男',
birthday: '1990-01-01',
phone: '138****8888',
email: 'xiaoming@example.com',
address: '北京市朝阳区某某街道123号',
bio: '热爱生活,积极向上'
});
const [addresses, setAddresses] = useState<Address[]>([
{ id: '1', name: '小明', phone: '138****8888', address: '北京市朝阳区某某街道123号', isDefault: true },
{ id: '2', name: '小明', phone: '138****8888', address: '上海市浦东新区某某路456号', isDefault: false },
]);
const [editingField, setEditingField] = useState<string | null>(null);
const [editValue, setEditValue] = useState<string>('');
// 开始编辑字段
const startEditing = (field: string, currentValue: string) => {
setEditingField(field);
setEditValue(currentValue);
};
// 保存编辑
const saveEdit = () => {
if (editingField) {
setProfile(prev => ({
...prev,
[editingField]: editValue
}));
setEditingField(null);
setEditValue('');
Alert.alert('成功', '信息已更新');
}
};
// 取消编辑
const cancelEdit = () => {
setEditingField(null);
setEditValue('');
};
// 设置默认地址
const setDefaultAddress = (id: string) => {
const updatedAddresses = addresses.map(addr => ({
...addr,
isDefault: addr.id === id
}));
setAddresses(updatedAddresses);
Alert.alert('成功', '默认地址已设置');
};
// 添加新地址
const addNewAddress = () => {
Alert.alert('添加地址', '请填写新的收货地址信息');
};
// 编辑地址
const editAddress = (addressId: string) => {
Alert.alert('编辑地址', `编辑地址 ID: ${addressId}`);
};
// 删除地址
const deleteAddress = (addressId: string) => {
if (addresses.length <= 1) {
Alert.alert('提示', '至少需要保留一个地址');
return;
}
const updatedAddresses = addresses.filter(addr => addr.id !== addressId);
setAddresses(updatedAddresses);
Alert.alert('成功', '地址已删除');
};
// 更改头像
const changeAvatar = () => {
Alert.alert('更改头像', '选择从相册或拍照');
};
// 退出登录
const handleLogout = () => {
Alert.alert(
'确认退出',
'您确定要退出登录吗?',
[
{ text: '取消', style: 'cancel' },
{ text: '确定', onPress: () => Alert.alert('退出', '已退出登录') }
]
);
};
return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.content}>
{/* 头部 - 用户信息 */}
<View style={styles.header}>
<TouchableOpacity onPress={changeAvatar}>
<Image source={{ uri: profile.avatar }} style={styles.avatar} />
</TouchableOpacity>
<View style={styles.userInfo}>
<View style={styles.nicknameContainer}>
{editingField === 'nickname' ? (
<View style={styles.editInputContainer}>
<TextInput
style={styles.editInput}
value={editValue}
onChangeText={setEditValue}
autoFocus
/>
<View style={styles.editButtons}>
<TouchableOpacity style={styles.saveButton} onPress={saveEdit}>
<Text style={styles.saveButtonText}>保存</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.cancelButton} onPress={cancelEdit}>
<Text style={styles.cancelButtonText}>取消</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={styles.displayNameContainer}>
<Text style={styles.displayName}>{profile.nickname}</Text>
<TouchableOpacity
style={styles.editIcon}
onPress={() => startEditing('nickname', profile.nickname)}
>
<Text style={styles.editText}>编辑</Text>
</TouchableOpacity>
</View>
)}
</View>
<Text style={styles.bio}>{profile.bio}</Text>
</View>
</View>
{/* 个人信息卡片 */}
<View style={styles.card}>
<Text style={styles.cardTitle}>个人信息</Text>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>性别</Text>
{editingField === 'gender' ? (
<View style={styles.editRow}>
<TextInput
style={styles.editInputSmall}
value={editValue}
onChangeText={setEditValue}
autoFocus
/>
<View style={styles.inlineEditButtons}>
<TouchableOpacity style={styles.inlineSaveButton} onPress={saveEdit}>
<Text style={styles.inlineButtonText}>✓</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.inlineCancelButton} onPress={cancelEdit}>
<Text style={styles.inlineButtonText}>×</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={styles.displayRow}>
<Text style={styles.infoValue}>{profile.gender}</Text>
<TouchableOpacity
style={styles.editButton}
onPress={() => startEditing('gender', profile.gender)}
>
<Text style={styles.editButtonText}>编辑</Text>
</TouchableOpacity>
</View>
)}
</View>
<View style={styles.divider} />
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>生日</Text>
{editingField === 'birthday' ? (
<View style={styles.editRow}>
<TextInput
style={styles.editInputSmall}
value={editValue}
onChangeText={setEditValue}
autoFocus
/>
<View style={styles.inlineEditButtons}>
<TouchableOpacity style={styles.inlineSaveButton} onPress={saveEdit}>
<Text style={styles.inlineButtonText}>✓</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.inlineCancelButton} onPress={cancelEdit}>
<Text style={styles.inlineButtonText}>×</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={styles.displayRow}>
<Text style={styles.infoValue}>{profile.birthday}</Text>
<TouchableOpacity
style={styles.editButton}
onPress={() => startEditing('birthday', profile.birthday)}
>
<Text style={styles.editButtonText}>编辑</Text>
</TouchableOpacity>
</View>
)}
</View>
<View style={styles.divider} />
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>手机号</Text>
{editingField === 'phone' ? (
<View style={styles.editRow}>
<TextInput
style={styles.editInputSmall}
value={editValue}
onChangeText={setEditValue}
autoFocus
/>
<View style={styles.inlineEditButtons}>
<TouchableOpacity style={styles.inlineSaveButton} onPress={saveEdit}>
<Text style={styles.inlineButtonText}>✓</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.inlineCancelButton} onPress={cancelEdit}>
<Text style={styles.inlineButtonText}>×</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={styles.displayRow}>
<Text style={styles.infoValue}>{profile.phone}</Text>
<TouchableOpacity
style={styles.editButton}
onPress={() => startEditing('phone', profile.phone)}
>
<Text style={styles.editButtonText}>编辑</Text>
</TouchableOpacity>
</View>
)}
</View>
<View style={styles.divider} />
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>邮箱</Text>
{editingField === 'email' ? (
<View style={styles.editRow}>
<TextInput
style={styles.editInputSmall}
value={editValue}
onChangeText={setEditValue}
autoFocus
/>
<View style={styles.inlineEditButtons}>
<TouchableOpacity style={styles.inlineSaveButton} onPress={saveEdit}>
<Text style={styles.inlineButtonText}>✓</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.inlineCancelButton} onPress={cancelEdit}>
<Text style={styles.inlineButtonText}>×</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={styles.displayRow}>
<Text style={styles.infoValue}>{profile.email}</Text>
<TouchableOpacity
style={styles.editButton}
onPress={() => startEditing('email', profile.email)}
>
<Text style={styles.editButtonText}>编辑</Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
{/* 收货地址卡片 */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}>收货地址</Text>
<TouchableOpacity style={styles.addButton} onPress={addNewAddress}>
<Text style={styles.addButtonText}>+ 添加</Text>
</TouchableOpacity>
</View>
{addresses.map(address => (
<View key={address.id} style={styles.addressItem}>
<View style={styles.addressInfo}>
<View style={styles.addressHeader}>
<Text style={styles.addressName}>{address.name}</Text>
{address.isDefault && (
<View style={styles.defaultTag}>
<Text style={styles.defaultTagText}>默认</Text>
</View>
)}
</View>
<Text style={styles.addressPhone}>{address.phone}</Text>
<Text style={styles.fullAddress}>{address.address}</Text>
</View>
<View style={styles.addressActions}>
<TouchableOpacity
style={[styles.actionButton, address.isDefault ? styles.defaultAction : {}]}
onPress={() => setDefaultAddress(address.id)}
>
<Text style={styles.actionButtonText}>
{address.isDefault ? '默认地址' : '设为默认'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => editAddress(address.id)}
>
<Text style={styles.actionButtonText}>编辑</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.deleteButton]}
onPress={() => deleteAddress(address.id)}
>
<Text style={styles.deleteButtonText}>删除</Text>
</TouchableOpacity>
</View>
</View>
))}
</View>
{/* 其他设置卡片 */}
<View style={styles.card}>
<Text style={styles.cardTitle}>其他设置</Text>
<TouchableOpacity style={styles.settingItem}>
<Text style={styles.settingText}>账户安全</Text>
<Text style={styles.arrow}>›</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.settingItem}>
<Text style={styles.settingText}>隐私设置</Text>
<Text style={styles.arrow}>›</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.settingItem}>
<Text style={styles.settingText}>通知设置</Text>
<Text style={styles.arrow}>›</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.settingItem}>
<Text style={styles.settingText}>关于应用</Text>
<Text style={styles.arrow}>›</Text>
</TouchableOpacity>
</View>
{/* 退出登录按钮 */}
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>退出登录</Text>
</TouchableOpacity>
{/* 服务条款和隐私政策 */}
<View style={styles.termsContainer}>
<Text style={styles.termsText}>继续使用即表示您同意我们的</Text>
<TouchableOpacity>
<Text style={styles.linkText}>服务条款</Text>
</TouchableOpacity>
<Text style={styles.termsText}>和</Text>
<TouchableOpacity>
<Text style={styles.linkText}>隐私政策</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f7fa',
},
content: {
flex: 1,
padding: 16,
},
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 20,
backgroundColor: '#ffffff',
borderRadius: 12,
marginBottom: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
marginRight: 16,
},
userInfo: {
flex: 1,
},
nicknameContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 8,
},
displayNameContainer: {
flexDirection: 'row',
alignItems: 'center',
},
displayName: {
fontSize: 22,
fontWeight: 'bold',
color: '#1e293b',
},
editIcon: {
marginLeft: 12,
padding: 4,
},
editText: {
fontSize: 14,
color: '#3b82f6',
},
bio: {
fontSize: 14,
color: '#64748b',
lineHeight: 20,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
cardTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
},
addButton: {
backgroundColor: '#3b82f6',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
},
addButtonText: {
color: '#ffffff',
fontSize: 12,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
},
infoLabel: {
fontSize: 16,
color: '#475569',
flex: 1,
},
displayRow: {
flexDirection: 'row',
alignItems: 'center',
flex: 2,
},
infoValue: {
fontSize: 16,
color: '#1e293b',
flex: 1,
},
editButton: {
backgroundColor: '#e2e8f0',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 4,
marginLeft: 10,
},
editButtonText: {
fontSize: 12,
color: '#475569',
},
editRow: {
flexDirection: 'row',
alignItems: 'center',
flex: 2,
},
editInputContainer: {
flex: 1,
},
editInput: {
borderWidth: 1,
borderColor: '#cbd5e1',
borderRadius: 6,
padding: 8,
fontSize: 16,
},
editInputSmall: {
borderWidth: 1,
borderColor: '#cbd5e1',
borderRadius: 6,
padding: 6,
fontSize: 14,
flex: 1,
marginRight: 8,
},
editButtons: {
flexDirection: 'row',
},
inlineEditButtons: {
flexDirection: 'row',
width: 60,
},
saveButton: {
backgroundColor: '#10b981',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 4,
marginLeft: 8,
},
saveButtonText: {
color: '#ffffff',
fontSize: 12,
},
cancelButton: {
backgroundColor: '#ef4444',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 4,
marginLeft: 8,
},
cancelButtonText: {
color: '#ffffff',
fontSize: 12,
},
inlineSaveButton: {
backgroundColor: '#10b981',
width: 25,
height: 25,
borderRadius: 12.5,
alignItems: 'center',
justifyContent: 'center',
marginRight: 4,
},
inlineCancelButton: {
backgroundColor: '#ef4444',
width: 25,
height: 25,
borderRadius: 12.5,
alignItems: 'center',
justifyContent: 'center',
},
inlineButtonText: {
color: '#ffffff',
fontWeight: 'bold',
fontSize: 12,
},
divider: {
height: 1,
backgroundColor: '#e2e8f0',
marginHorizontal: -16,
},
addressItem: {
marginBottom: 16,
paddingBottom: 16,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
addressInfo: {
marginBottom: 12,
},
addressHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
addressName: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginRight: 8,
},
defaultTag: {
backgroundColor: '#dbeafe',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
defaultTagText: {
fontSize: 10,
color: '#3b82f6',
fontWeight: 'bold',
},
addressPhone: {
fontSize: 14,
color: '#64748b',
marginBottom: 4,
},
fullAddress: {
fontSize: 14,
color: '#475569',
lineHeight: 20,
},
addressActions: {
flexDirection: 'row',
},
actionButton: {
backgroundColor: '#e2e8f0',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
marginRight: 8,
},
defaultAction: {
backgroundColor: '#3b82f6',
},
actionButtonText: {
fontSize: 12,
color: '#475569',
},
deleteButton: {
backgroundColor: '#fecaca',
},
deleteButtonText: {
color: '#dc2626',
},
settingItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 14,
},
settingText: {
fontSize: 16,
color: '#1e293b',
},
arrow: {
fontSize: 18,
color: '#94a3b8',
},
logoutButton: {
backgroundColor: '#ef4444',
paddingVertical: 16,
borderRadius: 10,
alignItems: 'center',
marginTop: 10,
marginBottom: 20,
},
logoutButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: 'bold',
},
termsContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
flexWrap: 'wrap',
},
termsText: {
fontSize: 12,
color: '#94a3b8',
},
linkText: {
fontSize: 12,
color: '#3b82f6',
textDecorationLine: 'underline',
},
});
export default ProfileApp;

打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐



所有评论(0)