请添加图片描述

写在前面

用户中心是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

BoxDecorationimage 属性显示头像,比用 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的个人空间,承载了很多功能。如何在一个页面里清晰地展示这么多信息和功能,是个不小的挑战。

我的设计原则:

  1. 信息分组要清晰。用户信息、订单、服务,每个模块都用白色卡片包裹,一眼就能区分。

  2. 层次要分明。用户信息最重要,放在最上面;订单和服务次之,放在中间;退出登录最不重要,放在最下面。

  3. 交互要简单。点击统计项跳转到对应页面,点击服务项执行对应操作,不需要复杂的交互。

  4. 细节要统一。颜色、间距、圆角都保持一致,让整个页面看起来协调。

做完用户中心后,我拿给几个朋友试用。有人说"功能很全,想要的都能找到",有人说"布局很清晰,不会觉得乱"。这些反馈让我觉得,那些细节的打磨是值得的。

一些经验:

  • 功能入口要分组。如果把所有功能都堆在一起,用户会找不到想要的。分成"我的订单"和"其他服务"两个模块,清晰明了。

  • 资产统计要实时。用户在其他页面进行了操作,回到用户中心时,数据应该是最新的。

  • 退出登录要确认。这是一个重要操作,必须让用户再次确认,避免误操作。

下一步计划:

用户中心做完了,接下来要做个人资料页。那个页面会让用户编辑自己的信息,比如头像、昵称、性别等。不过有了前面的基础,应该会更容易。

如果你也在做类似的项目,希望这篇文章能给你一些启发。用户中心看起来简单,但要做到清晰易用,还是需要在信息组织和交互设计上下功夫的。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐