rn_for_openharmony商城项目app实战-个人中心实现
本文介绍了使用React Native实现OpenHarmony商城项目中个人中心页面的开发过程。文章重点讲解了页面布局设计、数据获取与处理、交互实现等核心内容。个人中心作为高频访问页面,采用顶部用户信息卡片、中间数据统计和底部功能列表的标准布局。作者详细说明了如何从全局状态管理数据,包括用户信息、订单状态统计、收藏和浏览记录等,并分享了实现卡片叠加效果、订单快捷入口等UI细节的技巧。文中还提供了
rn_for_openharmony商城项目app实战-个人中心实现
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_buy
写在前面

个人中心这个页面,说白了就是一堆入口的集合。但别小看它,这是用户使用频率很高的页面。用户想看订单、改地址、领优惠券、找客服,都得从这里进去。
设计个人中心的时候,我参考了好几个主流 App。发现大家的布局都差不多:顶部是用户信息,中间是一些数据统计,下面是功能列表。这种布局用户已经很熟悉了,没必要搞创新,老老实实照着来就行。
不过实现起来还是有些细节要注意的,比如数据怎么从全局状态里拿、角标怎么显示、卡片怎么做出层叠效果。这篇文章就来聊聊这些。
先看看要用到哪些数据
个人中心要展示的信息挺多的:用户头像和昵称、收藏数量、浏览历史数量、优惠券数量、各状态订单数量、未读消息数量。这些数据散落在全局状态的各个角落,得一个个拿出来。
import React from 'react';
import {View, Text, StyleSheet, ScrollView, TouchableOpacity, Image} from 'react-native';
import {useApp} from '../store/AppContext';
import {ListItem} from '../components/ListItem';
import {TabBar} from '../components/TabBar';
引入的东西不多。ListItem 是我封装的列表项组件,后面功能列表会用到。
export const ProfileScreen = () => {
const {user, navigate, orders, favorites, coupons, unreadCount, browseHistory} = useApp();
一口气从 useApp 里解构出一堆东西。这就是全局状态的好处,不用一层层传 props,直接拿就行。
数据都从哪来的?
user是用户信息,包括头像、昵称、手机号。orders是订单列表,用来统计各状态订单数量。favorites和browseHistory分别是收藏和浏览历史。coupons是优惠券列表。unreadCount是未读消息数,这个在 Context 里已经算好了。
订单状态配置
订单区域要显示四个状态:待付款、待发货、待收货、待评价。我用数组来配置,方便后面遍历渲染:
const orderStats = [
{label: '待付款', icon: '💳', status: 'pending'},
{label: '待发货', icon: '📦', status: 'paid'},
{label: '待收货', icon: '🚚', status: 'shipped'},
{label: '待评价', icon: '⭐', status: 'delivered'},
];
每个配置项包含显示文字、图标和对应的订单状态值。status 字段用来从订单列表里筛选对应状态的订单。
再写个辅助函数来统计数量:
const getOrderCount = (status: string) => orders.filter(o => o.status === status).length;
传入状态值,返回该状态的订单数量。简单粗暴。
为什么不在 Context 里算好?
也可以,但我觉得这种页面级别的统计放在页面里算更合适。Context 里已经有
unreadCount了,再加一堆订单统计会显得臃肿。而且这个统计只有个人中心用,没必要全局共享。
用户信息卡片
页面顶部是用户信息区域,蓝色背景,比较醒目:
return (
<View style={styles.container}>
<ScrollView style={styles.content}>
<TouchableOpacity style={styles.userCard} onPress={() => navigate('profileEdit')}>
<Image source={{uri: user?.avatar}} style={styles.avatar} />
整个卡片用 TouchableOpacity 包裹,点击跳转到个人资料编辑页。头像用 Image 组件,数据源是用户信息里的 avatar 字段。
<View style={styles.userInfo}>
<Text style={styles.username}>{user?.username || '未登录'}</Text>
<Text style={styles.phone}>{user?.phone || '点击登录'}</Text>
</View>
<Text style={styles.arrow}>›</Text>
</TouchableOpacity>
用户名和手机号,如果没登录就显示引导文案。右边一个箭头,暗示用户这里可以点击进入下一级页面。
可选链操作符
?.
user?.avatar这种写法是 ES2020 的可选链语法。如果user是null或undefined,不会报错,而是返回undefined。这样就不用写user && user.avatar这种啰嗦的判断了。
数据统计卡片
用户卡片下面是数据统计区,展示收藏、足迹、优惠券三个数字:
<View style={styles.statsCard}>
<TouchableOpacity style={styles.statItem} onPress={() => navigate('favorites')}>
<Text style={styles.statValue}>{favorites.length}</Text>
<Text style={styles.statLabel}>收藏</Text>
</TouchableOpacity>
每个统计项都可以点击,跳转到对应的详情页。数字在上,文字在下,这是常见的数据展示方式。
<View style={styles.statDivider} />
<TouchableOpacity style={styles.statItem} onPress={() => navigate('browseHistory')}>
<Text style={styles.statValue}>{browseHistory.length}</Text>
<Text style={styles.statLabel}>足迹</Text>
</TouchableOpacity>
中间用分割线隔开。statDivider 就是一条竖线,宽度 1 像素,颜色浅灰。
<View style={styles.statDivider} />
<TouchableOpacity style={styles.statItem} onPress={() => navigate('couponList')}>
<Text style={styles.statValue}>{coupons.filter(c => !c.used).length}</Text>
<Text style={styles.statLabel}>优惠券</Text>
</TouchableOpacity>
</View>
优惠券这里有个小细节:显示的是可用优惠券数量,所以要过滤掉已使用的。filter(c => !c.used) 筛选出 used 为 false 的优惠券。
为什么要做成卡片叠加效果?
你可能注意到了,这个白色卡片会"压"在上面蓝色区域上。这是通过负 margin 实现的,后面样式部分会讲。这种设计能增加页面的层次感,不会显得太平。
订单快捷入口
这是个人中心的重头戏,用户查订单的频率很高:
<View style={styles.orderCard}>
<TouchableOpacity style={styles.orderHeader} onPress={() => navigate('orderList')}>
<Text style={styles.orderTitle}>我的订单</Text>
<View style={styles.orderAllBtn}>
<Text style={styles.orderAll}>全部订单</Text>
<Text style={styles.orderArrow}>›</Text>
</View>
</TouchableOpacity>
头部左边是标题,右边是"全部订单"入口。点击跳转到订单列表页。
<View style={styles.orderStats}>
{orderStats.map(item => {
const count = getOrderCount(item.status);
return (
<TouchableOpacity
key={item.label}
style={styles.orderItem}
onPress={() => navigate('orderList')}
>
遍历前面定义的 orderStats 数组,渲染四个订单状态入口。每个入口点击都跳转到订单列表页。
能不能跳转时带上状态参数?
当然可以,比如
navigate('orderList', {status: item.status}),然后订单列表页根据参数筛选显示。我这里偷懒了,都跳到全部订单,用户自己切换状态。正式项目建议加上参数。
<View style={styles.orderIconWrap}>
<Text style={styles.orderIcon}>{item.icon}</Text>
{count > 0 && (
<View style={styles.orderBadge}>
<Text style={styles.orderBadgeText}>{count}</Text>
</View>
)}
</View>
<Text style={styles.orderLabel}>{item.label}</Text>
</TouchableOpacity>
);
})}
</View>
</View>
图标右上角的角标只在数量大于 0 时显示。这个细节很重要,如果没有待付款订单,就不应该显示角标,不然用户会困惑。
角标用绝对定位放在图标右上角,红色背景白色文字,很醒目。
功能列表
下面是两组功能入口,用 ListItem 组件来渲染:
<View style={styles.section}>
<ListItem
icon="❤️"
title="我的收藏"
rightText={`${favorites.length}件`}
onPress={() => navigate('favorites')}
/>
<ListItem
icon="🕐"
title="浏览历史"
rightText={`${browseHistory.length}条`}
onPress={() => navigate('browseHistory')}
/>
<ListItem
icon="🎫"
title="优惠券"
rightText={`${coupons.filter(c => !c.used).length}张可用`}
onPress={() => navigate('couponList')}
/>
<ListItem
icon="📍"
title="收货地址"
onPress={() => navigate('addressList')}
/>
</View>
第一组是和购物相关的功能:收藏、浏览历史、优惠券、收货地址。rightText 属性用来显示右侧的附加信息,比如数量。
ListItem 组件的好处
把列表项抽成组件后,所有列表的样式都是统一的。图标在左边,标题在中间,右边可以显示文字或箭头。如果哪天要改样式,改一个地方就够了。
<View style={styles.section}>
<ListItem
icon="🔔"
title="消息通知"
rightText={unreadCount > 0 ? `${unreadCount}条未读` : ''}
onPress={() => navigate('messageList')}
/>
<ListItem
icon="❓"
title="帮助中心"
onPress={() => navigate('helpCenter')}
/>
<ListItem
icon="💬"
title="意见反馈"
onPress={() => navigate('feedback')}
/>
<ListItem
icon="⚙️"
title="设置"
onPress={() => navigate('settings')}
/>
</View>
第二组是服务类功能:消息、帮助、反馈、设置。消息那里会显示未读数量,没有未读就不显示。
两组之间有个间距,视觉上区分开不同类型的功能。
</ScrollView>
<TabBar />
</View>
);
};
最后别忘了 TabBar,个人中心是底部导航的四个页面之一。
ListItem 组件长什么样
顺便说一下 ListItem 组件的实现,很简单:
interface Props {
icon: string;
title: string;
rightText?: string;
onPress?: () => void;
}
export const ListItem = ({icon, title, rightText, onPress}: Props) => (
<TouchableOpacity style={styles.container} onPress={onPress}>
<Text style={styles.icon}>{icon}</Text>
<Text style={styles.title}>{title}</Text>
<Text style={styles.rightText}>{rightText}</Text>
<Text style={styles.arrow}>›</Text>
</TouchableOpacity>
);
接收图标、标题、右侧文字和点击回调。布局是一行四列:图标、标题、右侧文字、箭头。标题用 flex: 1 占据剩余空间。
为什么箭头要单独写?
因为几乎所有列表项都需要箭头,表示可以点击进入下一级。如果某些场景不需要箭头,可以加个
showArrow属性来控制。
样式详解
先看用户卡片的样式:
const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#f5f5f5'},
content: {flex: 1},
userCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#3498db',
paddingTop: 60,
paddingBottom: 24,
paddingHorizontal: 20,
},
蓝色背景,paddingTop: 60 给状态栏留空间。paddingBottom: 24 比较大,是为了给下面的统计卡片留出叠加的空间。
avatar: {
width: 70,
height: 70,
borderRadius: 35,
backgroundColor: '#fff',
borderWidth: 3,
borderColor: 'rgba(255,255,255,0.3)',
},
头像是圆形,borderRadius 设为宽高的一半。加了个半透明的白色边框,看起来更精致。
username: {fontSize: 22, fontWeight: 'bold', color: '#fff'},
phone: {fontSize: 14, color: 'rgba(255,255,255,0.8)', marginTop: 4},
用户名大一点、加粗,手机号小一点、颜色淡一点,形成主次关系。
统计卡片的关键样式:
statsCard: {
flexDirection: 'row',
backgroundColor: '#fff',
marginHorizontal: 16,
marginTop: -16,
borderRadius: 12,
paddingVertical: 16,
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
},
重点是 marginTop: -16,负值让卡片向上偏移,和上面的蓝色区域产生叠加效果。阴影让卡片有悬浮感,elevation 是 Android 的阴影属性。
statItem: {flex: 1, alignItems: 'center'},
statValue: {fontSize: 22, fontWeight: 'bold', color: '#333'},
statLabel: {fontSize: 13, color: '#999', marginTop: 4},
statDivider: {width: 1, backgroundColor: '#f0f0f0'},
三个统计项用 flex: 1 平分空间,中间用 1 像素的分割线隔开。
订单区域的角标样式:
orderIconWrap: {
position: 'relative',
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#f5f5f5',
justifyContent: 'center',
alignItems: 'center',
},
orderBadge: {
position: 'absolute',
top: -4,
right: -4,
backgroundColor: '#e74c3c',
borderRadius: 10,
minWidth: 18,
height: 18,
justifyContent: 'center',
alignItems: 'center',
},
orderBadgeText: {color: '#fff', fontSize: 11, fontWeight: 'bold'},
图标外面套一个圆形容器,角标用绝对定位放在右上角。minWidth: 18 保证角标至少有一定宽度,数字多的时候会自动撑开。
section: {backgroundColor: '#fff', marginTop: 12},
});
功能列表的分组,白色背景,上面留 12 像素的间距和上一个区块分开。
一些细节优化
当前实现已经能用了,但还有一些可以打磨的地方:
1. 登录状态判断
现在不管有没有登录都显示同样的内容。正式项目应该判断登录状态,未登录时显示登录引导,点击跳转登录页而不是编辑资料页。
2. 下拉刷新
用户可能想刷新数据,比如看看有没有新消息、订单状态有没有变化。可以给 ScrollView 加上 RefreshControl 组件实现下拉刷新。
3. 骨架屏
如果数据是从接口拿的,加载过程中可以显示骨架屏,比直接显示空白或 loading 图标体验更好。
什么时候做这些优化?
看项目阶段。如果是快速验证想法的 MVP,先把主流程跑通。如果是要上线的产品,这些细节都要考虑。用户体验是由无数个细节堆出来的。
写在最后
个人中心看起来就是一堆入口的堆砌,但要做好也不容易。信息的层次要清晰,重要的放上面,次要的放下面。数据要实时,用户一眼就能看到自己有多少收藏、多少未读消息。交互要顺畅,点哪里跳哪里,不能让用户迷路。
这个页面涉及到的全局状态比较多,也是对 Context 设计的一个检验。如果发现某个数据拿起来很别扭,可能是 Context 的结构需要调整。
下一篇写商品详情页,那个页面交互会更复杂一些,涉及到加购物车、收藏、查看评价等功能。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)