Flutter for Openharmony盲盒抽奖App应用实战+用户中心实现
用户中心是App的个人空间,承载了用户信息、资产统计、订单入口、功能服务等多个模块。这个页面看起来简单,但实际上是整个App功能的汇总入口。如何在一个页面里清晰地展示这么多信息和功能,是个不小的挑战。我在做这个页面时,特别注意了信息的分组和层次。用户信息放在最上面,最醒目;订单和服务分成两个模块,清晰明了;底部是退出登录等操作,不会太抢眼。@override这是标准的 StatefulWidget

写在前面
用户中心是App的个人空间,承载了用户信息、资产统计、订单入口、功能服务等多个模块。这个页面看起来简单,但实际上是整个App功能的汇总入口。如何在一个页面里清晰地展示这么多信息和功能,是个不小的挑战。
我在做这个页面时,特别注意了信息的分组和层次。用户信息放在最上面,最醒目;订单和服务分成两个模块,清晰明了;底部是退出登录等操作,不会太抢眼。
用户中心的功能规划
在动手之前,我先梳理了一下用户中心需要做什么。作为用户的个人空间,这个页面的核心是:展示用户信息和资产,提供各种功能入口。
主要功能:
- 用户信息展示(头像、手机号、登录状态)
- 资产统计(商品数、潮盒数、潮豆数)
- 我的订单(待发货、待收货、全部订单)
- 其他服务(收货地址、常见问题、联系客服等)
- 申请成为分销商
- 退出登录
功能很多,但要做到清晰有序,不能让用户觉得混乱。
页面结构搭建
页面类的定义
class MinePage extends StatefulWidget {
const MinePage({super.key});
State<MinePage> createState() => _MinePageState();
}
这是标准的 StatefulWidget,因为页面需要监听用户数据变化。
状态变量的初始化
class _MinePageState extends State<MinePage> {
final UserService _userService = UserService();
void initState() {
super.initState();
// 监听数据变化,定期刷新UI
}
用
UserService获取用户数据。实际项目中,应该用 Provider 或其他状态管理方案,让数据变化时自动刷新UI。
页面布局结构
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.primary,
body: SafeArea(
child: Column(
children: [
// 标题
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(
'个人中心',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.black,
),
),
),
// 内容区域
Expanded(
child: Container(
decoration: const BoxDecoration(
color: Color(0xFFF5F5F5),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
布局思路:
- 顶部用金色背景,显示"个人中心"标题
- 内容区域用浅灰色背景,圆角20,形成视觉层次
- 用
Expanded让内容区域占满剩余空间这种设计能让页面看起来更有层次感,不会太单调。
用户信息卡片
获取用户数据
Widget _buildUserCard() {
final userService = UserService();
final phone = userService.currentPhone ?? '未登录';
final avatar = userService.currentAvatar;
从
UserService获取用户手机号和头像。如果没有登录,显示"未登录"。
卡片容器
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(16),
),
用户信息卡片用金色背景,跟顶部的颜色一致。圆角16,看起来更精致。
用户头像和信息
child: Column(
children: [
// 用户信息
Row(
children: [
// 头像
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
image: avatar != null
? DecorationImage(
image: NetworkImage(avatar),
fit: BoxFit.cover,
)
: null,
),
child: avatar == null
? const Icon(
Icons.smart_toy,
color: Colors.white,
size: 36,
)
: null,
),
头像的处理:
- 如果有头像,显示网络图片
- 如果没有头像,显示一个机器人图标作为默认头像
- 圆形头像,直径60
用
BoxDecoration的image属性显示头像,比用Image组件更灵活。
用户名和状态
const SizedBox(width: 16),
// 用户名和状态
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
phone,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.black,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'已登录',
style: TextStyle(fontSize: 12, color: AppColors.black),
),
),
],
),
用户信息的展示:
- 手机号用大字号加粗,最醒目
- 登录状态用小标签显示,半透明白色背景
为什么用半透明背景?因为卡片本身是金色的,用纯白色会太突兀,半透明能融入背景。
资产统计
const SizedBox(height: 20),
// 统计数据 - 从 UserService 获取实时数据
Row(
children: [
_buildStatItem('${userService.productCount}', '商品'),
_buildDivider(),
_buildStatItem('${userService.boxCount}', '潮盒'),
_buildDivider(),
_buildStatItem('${userService.chaodou}', '潮豆'),
],
),
资产统计的设计:
- 三个统计项:商品数、潮盒数、潮豆数
- 用分割线隔开,清晰明了
- 数据从
UserService实时获取为什么要实时获取?因为用户可能在其他页面进行了操作(比如开箱、购买),资产数据会变化。
统计项的实现
Widget _buildStatItem(String value, String label) {
return Expanded(
child: GestureDetector(
onTap: () => _handleStatItemTap(label),
child: Column(
children: [
Text(
value,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.black,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(fontSize: 12, color: AppColors.black),
),
],
),
),
);
}
统计项的设计:
- 数值用大字号加粗,醒目
- 标签用小字号,不抢眼
- 整个统计项可点击,点击后跳转到对应页面
用
Expanded让三个统计项平分空间,看起来更整齐。
统计项的点击处理
void _handleStatItemTap(String label) {
if (label == '商品') {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WarehousePage()),
).then((_) {
// 返回时刷新UI
setState(() {});
});
} else if (label == '潮盒') {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WarehousePage(initialTab: 1)),
).then((_) {
// 返回时刷新UI
setState(() {});
});
}
}
点击处理的逻辑:
- 点击"商品",跳转到仓库页的商品tab
- 点击"潮盒",跳转到仓库页的潮盒tab
- 点击"潮豆",暂时不处理(可以跳转到潮豆明细页)
用
then监听页面返回,返回时调用setState刷新UI,确保数据是最新的。
分割线
Widget _buildDivider() {
return Container(
width: 1,
height: 30,
color: Colors.black.withOpacity(0.1),
);
}
分割线用很浅的黑色(
opacity: 0.1),只是为了分隔统计项,不会太抢眼。
我的订单模块
订单模块的容器
Widget _buildOrderSection(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
订单模块用白色卡片,跟用户信息卡片区分开。
订单模块的标题
child: Column(
children: [
// 标题栏
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'我的订单',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.black,
),
),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const MyOrdersPage()),
);
},
child: const Row(
children: [
Text(
'全部订单',
style: TextStyle(fontSize: 13, color: AppColors.grey),
),
Icon(Icons.chevron_right, size: 18, color: AppColors.grey),
],
),
),
],
),
标题栏的设计:
- 左边是"我的订单"标题,加粗
- 右边是"全部订单"链接,配一个右箭头
- 点击"全部订单"跳转到订单管理页
用
mainAxisAlignment: MainAxisAlignment.spaceBetween让标题和链接分别靠左右两边。
订单状态入口
const SizedBox(height: 16),
// 订单状态
Row(
children: [
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const MyOrdersPage(initialTab: 1),
),
);
},
child: _buildOrderItem(Icons.local_shipping_outlined, '待发货'),
),
const SizedBox(width: 40),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const MyOrdersPage(initialTab: 2),
),
);
},
child: _buildOrderItem(Icons.inventory_2_outlined, '待收货'),
),
],
),
订单状态入口的设计:
- 提供两个快捷入口:待发货、待收货
- 点击后跳转到订单管理页的对应tab
- 两个入口之间用
SizedBox(width: 40)隔开为什么只提供两个入口?因为这两个是用户最关心的状态,其他状态可以在"全部订单"里查看。
订单项的实现
Widget _buildOrderItem(IconData icon, String label) {
return Column(
children: [
Icon(icon, size: 28, color: AppColors.primaryDark),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(fontSize: 13, color: AppColors.black),
),
],
);
}
订单项的设计:
- 上面是图标,size: 28,用深金色
- 下面是文字,小字号
- 图标和文字之间用
SizedBox(height: 8)隔开这种上图标下文字的设计很常见,简洁明了。
其他服务模块
服务模块的容器
Widget _buildServiceSection(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
服务模块的容器样式跟订单模块一样,保持一致性。
服务模块的标题
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'其他服务',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.black,
),
),
const SizedBox(height: 16),
标题用加粗文字,跟订单模块的标题样式一致。
服务项的布局
// 第一行服务
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildServiceItem(Icons.location_on_outlined, '收货地址', onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AddressListPage()),
);
}),
_buildServiceItem(Icons.help_outline, '常见问题'),
_buildServiceItem(Icons.headset_mic_outlined, '联系客服'),
_buildServiceItem(Icons.security_outlined, '隐私政策'),
],
),
const SizedBox(height: 20),
// 第二行服务
Row(
children: [
_buildServiceItem(Icons.description_outlined, '用户协议'),
const SizedBox(width: 24),
_buildServiceItem(Icons.monetization_on_outlined, '赚取佣金'),
const SizedBox(width: 24),
_buildServiceItem(Icons.person_off_outlined, '注销账户'),
],
),
服务项的布局:
- 第一行4个服务项,用
spaceAround平分空间- 第二行3个服务项,用
SizedBox隔开- 两行之间用
SizedBox(height: 20)隔开为什么分两行?因为服务项太多,放在一行会太挤。分两行看起来更舒服。
服务项的实现
Widget _buildServiceItem(IconData icon, String label, {VoidCallback? onTap}) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
width: 70,
child: Column(
children: [
Icon(icon, size: 26, color: AppColors.primaryDark),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(fontSize: 12, color: AppColors.black),
textAlign: TextAlign.center,
),
],
),
),
);
}
服务项的设计:
- 固定宽度70,让所有服务项大小一致
- 上面是图标,size: 26,用深金色
- 下面是文字,小字号,居中对齐
- 整个服务项可点击,点击后执行
onTap回调文字用
textAlign: TextAlign.center居中,避免文字太长时左对齐不好看。
底部按钮
按钮区域的容器
Widget _buildBottomButtons(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
按钮区域用
Padding包裹,左右各留16的间距。
申请成为分销商按钮
// 申请成为分销商
Container(
width: double.infinity,
height: 50,
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(25),
),
child: const Center(
child: Text(
'申请成为分销商',
style: TextStyle(
fontSize: 16,
color: AppColors.black,
fontWeight: FontWeight.bold,
),
),
),
),
申请按钮的设计:
- 金色背景,跟App主题一致
- 圆角25,做成胶囊形状
- 黑色加粗文字,醒目
- 宽度占满,高度50
这个按钮是为了推广分销功能,所以用了比较醒目的样式。
退出登录按钮
const SizedBox(height: 12),
// 退出登录
GestureDetector(
onTap: () => _showLogoutDialog(context),
child: Container(
width: double.infinity,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF333333),
borderRadius: BorderRadius.circular(25),
),
child: const Center(
child: Text(
'退出登录',
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),
退出按钮的设计:
- 深灰色背景(
0xFF333333),表示这是一个"危险"操作- 白色加粗文字,跟背景形成对比
- 样式跟申请按钮类似,但颜色不同
为什么用深灰色?因为退出登录是一个不可逆的操作,用深色能提醒用户谨慎点击。
退出登录确认弹窗
void _showLogoutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: const Center(
child: Text(
'提示',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
content: const Text(
'你确定要退出登录吗',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: AppColors.grey),
),
actionsAlignment: MainAxisAlignment.spaceEvenly,
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'取消',
style: TextStyle(fontSize: 16, color: AppColors.grey),
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
// 执行退出登录逻辑
_userService.logout();
// 返回登录页面
Navigator.of(context).pushReplacementNamed('/login');
},
child: const Text(
'确定',
style: TextStyle(fontSize: 16, color: AppColors.black),
),
),
],
),
);
}
退出确认弹窗的设计:
- 标题"提示",居中显示
- 内容"你确定要退出登录吗",居中显示
- 两个按钮:取消和确定,平分空间
- 点击确定后,调用
logout方法,然后跳转到登录页为什么要弹窗确认?因为退出登录是一个重要操作,必须让用户再次确认,避免误操作。
一些细节优化
页面刷新机制
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WarehousePage()),
).then((_) {
// 返回时刷新UI
setState(() {});
});
跳转到其他页面后,用
then监听返回事件。返回时调用setState刷新UI,确保数据是最新的。这个机制很重要,因为用户可能在其他页面进行了操作,资产数据会变化。
颜色的统一
AppColors.primary // 金色
AppColors.primaryDark // 深金色
AppColors.black // 黑色
AppColors.grey // 灰色
所有颜色都用
AppColors常量,保持全局一致。不要在代码里直接写颜色值,不好维护。
间距的统一
margin: const EdgeInsets.all(16), // 卡片外边距
padding: const EdgeInsets.all(16), // 卡片内边距
const SizedBox(height: 16), // 模块之间的间距
const SizedBox(height: 12), // 按钮之间的间距
const SizedBox(height: 8), // 小元素之间的间距
所有间距都是4的倍数(8、12、16),看起来更整齐。
圆角的统一
borderRadius: BorderRadius.circular(16), // 卡片圆角
borderRadius: BorderRadius.circular(12), // 弹窗圆角
borderRadius: BorderRadius.circular(25), // 按钮圆角
不同元素用不同的圆角,但都是4的倍数。大元素用大圆角,小元素用小圆角。
踩过的坑
数据不刷新
一开始我没有在页面返回时刷新UI,结果用户在其他页面开箱后,回到用户中心,资产数据还是旧的。
解决方案:
在跳转时用
then监听返回事件:Navigator.push(...).then((_) { setState(() {}); });返回时调用
setState刷新UI,确保数据是最新的。
退出登录没有确认
一开始我直接执行退出登录,结果用户反馈说容易误操作。
解决方案:
加一个确认弹窗:
void _showLogoutDialog(BuildContext context) { showDialog(...); }让用户再次确认,避免误操作。
服务项点击区域小
一开始我只给图标加了点击事件,结果用户点击文字没反应。
解决方案:
用
GestureDetector包裹整个服务项:GestureDetector( onTap: onTap, child: SizedBox( width: 70, child: Column(...), ), )这样整个服务项都可以点击,用户体验更好。
头像加载失败
用户反馈说有时候头像显示不出来,显示一片空白。
解决方案:
加一个默认头像:
child: avatar == null ? const Icon( Icons.smart_toy, color: Colors.white, size: 36, ) : null,如果没有头像或加载失败,显示一个机器人图标。
写在最后
用户中心是App的个人空间,承载了很多功能。如何在一个页面里清晰地展示这么多信息和功能,是个不小的挑战。
我的设计原则:
-
信息分组要清晰。用户信息、订单、服务,每个模块都用白色卡片包裹,一眼就能区分。
-
层次要分明。用户信息最重要,放在最上面;订单和服务次之,放在中间;退出登录最不重要,放在最下面。
-
交互要简单。点击统计项跳转到对应页面,点击服务项执行对应操作,不需要复杂的交互。
-
细节要统一。颜色、间距、圆角都保持一致,让整个页面看起来协调。
做完用户中心后,我拿给几个朋友试用。有人说"功能很全,想要的都能找到",有人说"布局很清晰,不会觉得乱"。这些反馈让我觉得,那些细节的打磨是值得的。
一些经验:
-
功能入口要分组。如果把所有功能都堆在一起,用户会找不到想要的。分成"我的订单"和"其他服务"两个模块,清晰明了。
-
资产统计要实时。用户在其他页面进行了操作,回到用户中心时,数据应该是最新的。
-
退出登录要确认。这是一个重要操作,必须让用户再次确认,避免误操作。
下一步计划:
用户中心做完了,接下来要做个人资料页。那个页面会让用户编辑自己的信息,比如头像、昵称、性别等。不过有了前面的基础,应该会更容易。
如果你也在做类似的项目,希望这篇文章能给你一些启发。用户中心看起来简单,但要做到清晰易用,还是需要在信息组织和交互设计上下功夫的。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)