个人中心是运动健康App的"我的"页面。用户从首页底部导航"我的"跳转到这里,查看个人资料、运动统计、快捷入口。这个页面的任务是:让用户看到"我运动了多少"和"我能去哪里"——上方是个人身份和数据,下方是功能导航。

页面结构分为三部分:顶部导航栏、用户信息卡片、统计数据行、功能菜单列表。从上到下,信息从"我是谁"到"我做了什么"再到"我能做什么"。
完整效果
在这里插入图片描述
在这里插入图片描述

导航栏

Row() {
  Row() {
    SymbolGlyph($r('sys.symbol.chevron_left')).fontSize(20).fontColor([T1])
  }.width(34).height(34).borderRadius(17).backgroundColor('rgba(0,0,0,0.03)')
  .justifyContent(FlexAlign.Center).onClick(() => { router.back() })
  Text('我的').fontSize(20).fontWeight(FontWeight.Bold).fontColor(T1)
    .margin({ left: 10 }).layoutWeight(1)
  Text('⚙️').fontSize(20)
}.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 8 })

在这里插入图片描述

导航栏和其他页面保持一致——左边返回按钮,中间标题。但右边多了一个⚙️设置图标。和社区页面的"发布"按钮、用户主页的"关注"按钮一样,导航栏右侧是全局操作区——每个页面可以放不同的按钮。

⚙️设置图标目前没有绑定onClick——点击不会跳转到设置页面。菜单列表里有一个"设置"入口,和导航栏的⚙️功能重复。实际项目中应该二选一:要么导航栏放设置入口,要么菜单列表放,不需要两个地方都有。

用户信息卡片

Row() {
  Stack() {
    Column() {}.width(64).height(64).borderRadius(32)
      .linearGradient({ angle: 135, colors: [[A, 0], ['#FF9F43', 1]] })
    Text('🏃').fontSize(28)
  }
  Column() {
    Text('运动达人').fontSize(20).fontWeight(FontWeight.Bold).fontColor(T1)
    Text('连续运动 15 天 · 超越 89% 的用户').fontSize(12).fontColor(T3).margin({ top: 2 })
  }.alignItems(HorizontalAlign.Start).margin({ left: 14 }).layoutWeight(1)
  Text('编辑 ›').fontSize(12).fontColor(A)
}.width('100%').padding(20).backgroundColor('#FFFFFF').borderRadius(18)
.margin({ left: 16, right: 16, bottom: 16 })

在这里插入图片描述

用户卡片是一个白色圆角Row,左边头像、中间名字和标签、右边编辑按钮。

头像的Stack实现

Stack() {
  Column() {}.width(64).height(64).borderRadius(32)
    .linearGradient({ angle: 135, colors: [[A, 0], ['#FF9F43', 1]] })
  Text('🏃').fontSize(28)
}

头像用Stack实现——底层是一个64×64的圆形,填充红到橙的渐变色;上层是emoji文字🏃。Stack的默认对齐是居中,所以emoji自动居中在渐变圆上。

这种"渐变背景+emoji前景"的头像设计和首页右上角的头像一致——整个App用同一套头像风格。64×64是个人中心的尺寸,首页是42×42,用户主页是100×100。同一个元素在不同页面有不同尺寸,符合信息层级。

编辑按钮

Text('编辑 ›').fontSize(12).fontColor(A)

红色文字+右箭头,暗示"可点击"。但没有绑定onClick——点击不会跳转到编辑页面。这是原型阶段的典型做法:UI元素存在,但交互留到后续实现。

统计数据行

Row({ space: 8 }) {
  this.Stat('累计运动', '156', '天', A)
  this.Stat('累计消耗', '48,920', '千卡', '#FF9F43')
  this.Stat('完成训练', '342', '次', '#5DADE2')
}.width('100%').padding({ left: 16, right: 16 }).margin({ bottom: 16 })

在这里插入图片描述

三个统计卡片并排,用space: 8控制间距。每个卡片展示一个核心指标。

Stat Builder

@Builder Stat(label: string, value: string, unit: string, color: string) {
  Column() {
    Text(value).fontSize(24).fontWeight(FontWeight.Bold).fontColor(color)
    Text(unit).fontSize(10).fontColor(T3)
    Text(label).fontSize(11).fontColor(T2).margin({ top: 2 })
  }.layoutWeight(1).padding(14).backgroundColor('#FFFFFF').borderRadius(14)
}

每个统计卡片由三行文字组成:数值(24px加粗彩色)、单位(10px灰色)、标签(11px灰色)。数值最醒目,单位和标签视觉层级较低。

和ProgressPage的SCard Builder对比:

属性 ProfilePage Stat ProgressPage SCard
数值字号 24px 28px
单位位置 第二行 第二行
标签位置 第三行 第三行
padding 14px 16px
圆角 14px 16px

个人中心的Stat比进度页的SCard略小——因为个人中心是"概览",进度页是"详情"。同一个数据在不同页面有不同的展示尺寸,符合信息层级。

三个统计指标

指标 数值 单位 颜色 含义
累计运动 156 A(红色) 总运动天数
累计消耗 48,920 千卡 #FF9F43(橙色) 总卡路里消耗
完成训练 342 #5DADE2(蓝色) 总训练次数

颜色和首页、进度页保持一致:红色=运动/步数,橙色=卡路里,蓝色=训练。用户看到红色就联想到"运动",不管在哪个页面。

48920千卡的千位分隔符是硬编码的——直接在字符串里写了逗号。和ProgressPage的76532步一样,没有动态格式化。如果数字变化,需要手动改字符串。

功能菜单列表

Column({ space: 0 }) {
  this.Menu('运动记录', '📊', () => { router.pushUrl({ url: 'pages/ProgressPage' }) })
  this.Div()
  this.Menu('饮食记录', '🍽️', () => { router.pushUrl({ url: 'pages/MealPage' }) })
  this.Div()
  this.Menu('我的成就', '🏆', () => { })
  this.Div()
  this.Menu('身体数据', '📏', () => { })
  this.Div()
  this.Menu('目标设置', '🎯', () => { })
  this.Div()
  this.Menu('设置', '⚙️', () => { })
}.width('100%').backgroundColor('#FFFFFF').borderRadius(18)
.margin({ left: 16, right: 16 })

六个菜单项,每项之间用Divider分隔。整个菜单区域是一个白色圆角卡片。

Menu Builder

@Builder Menu(label: string, icon: string, action: () => void) {
  Row() {
    Text(icon).fontSize(18).margin({ right: 14 })
    Text(label).fontSize(14).fontColor(T1).layoutWeight(1)
    SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(12).fontColor(['#DDD'])
  }.width('100%').padding({ left: 18, right: 18, top: 14, bottom: 14 })
  .onClick(action)
}

每个菜单项是一个Row:左边emoji图标、中间文字、右边右箭头(SymbolGlyph)。layoutWeight(1)让文字区域撑满中间空间,右箭头被推到最右边。

右箭头用SymbolGlyph($r('sys.symbol.chevron_right'))——和返回按钮的sys.symbol.chevron_left是同一套图标。左箭头=返回,右箭头=前进,这是通用的导航语义。

菜单项的功能映射

菜单项 图标 跳转目标 状态
运动记录 📊 ProgressPage 已实现
饮食记录 🍽️ MealPage 已实现
我的成就 🏆 未实现
身体数据 📏 未实现
目标设置 🎯 未实现
设置 ⚙️ 未实现

六个菜单中只有两个有实际跳转——运动记录去ProgressPage,饮食记录去MealPage。其余四个的action是空函数() => { },点击没有反应。这是原型阶段的正常状态:先搭好UI框架,再逐步填充功能。

Div Builder

@Builder Div() {
  Divider().strokeWidth(0.5).color('#F0EEF4').margin({ left: 52 })
}

分割线用Divider组件,0.5px宽,浅灰色。margin({ left: 52 })让分割线从图标右侧开始——不延伸到左边的emoji图标下方。52px的计算:18px(padding-left)+ 18px(icon fontSize)+ 14px(margin-right)+ 约2px余量。

这种"缩进分割线"是列表设计的常见手法——分割线只在文字区域下方,不在图标下方。视觉上更干净,图标区域保持完整。

页面整体结构

Column() {
  // 导航栏(固定)
  Row() { ... }
  // 内容区(可滚动)
  Scroll() {
    Column() {
      // 用户卡片
      // 统计数据
      // 功能菜单
    }
  }.width('100%').layoutWeight(1)
}.width('100%').height('100%')

和其他页面一样的结构:导航栏在Scroll外面(固定),内容在Scroll里面(可滚动)。layoutWeight(1)让Scroll占据导航栏以外的所有空间。

和社区页面对比:社区页面的Scroll里是ForEach渲染的帖子列表,个人中心的Scroll里是固定结构(卡片+统计+菜单)。前者是动态内容,后者是静态结构。静态结构不需要ForEach,直接写死就行。

Builder的复用

个人中心用了三个Builder:Stat、Menu、Div。它们的职责清晰:

  • Stat:统计数据卡片(数值+单位+标签)
  • Menu:菜单项(图标+文字+箭头+点击事件)
  • Div:分割线(0.5px+缩进)

这三个Builder只在ProfilePage内部使用——没有导出到其他页面。但Stat的结构和ProgressPage的SCard几乎一样(数值+单位+标签),Menu的结构和社区页面的关注按钮有相似之处(文字+背景色+圆角)。

如果要跨页面复用,应该把这些Builder抽到components目录下。但原型阶段每个页面自己定义Builder更简单——不需要考虑接口设计、参数兼容性等问题。过早抽象反而会增加理解成本。

和其他页面的导航关系

个人中心从首页底部导航的"我的"按钮跳转过来。底部导航有5个按钮:首页、训练、进度、社区、我的。"我的"是第5个。

个人中心内部的菜单又跳转到其他页面:运动记录→ProgressPage,饮食记录→MealPage。这意味着用户可以从"我的"→"运动记录"→ProgressPage,绕了一圈到达进度页。和直接从底部导航点"进度"效果一样,但路径更长。

这种"多入口到达同一页面"的设计是正常的——用户可以从不同路径到达ProgressPage,每个路径的上下文不同。从首页点"运动记录"是"我想看今天的运动数据",从"我的"点"运动记录"是"我想看我的运动历史"。同一个页面,不同的用户意图。

踩坑记录

导航栏右侧的⚙️没有功能。 导航栏右边放了一个⚙️图标,菜单列表底部也有一个"设置"入口。两处功能重复,且都没有实现。实际项目应该只保留一个——要么导航栏放图标,要么菜单列表放文字入口。

统计数字的千位分隔符硬编码。 48,920的逗号是字符串里直接写的,不是动态格式化的。如果数字变化(比如从48920变成100000),需要手动改代码。和ProgressPage的76,532步同样的问题。

Div的margin-left计算。 分割线的margin({ left: 52 })是手动计算的——如果Menu的padding或图标尺寸变化,52px可能不再对齐。更稳健的做法是用Row包裹分割线,让分割线从图标右侧开始,而不是用固定margin。

空菜单项的用户体验。 四个菜单项(我的成就、身体数据、目标设置、设置)点击没有反应。用户点击后没有任何反馈——既没有跳转,也没有提示"功能开发中"。真实项目应该加一个toast提示或禁用状态,避免用户困惑。

头像Stack的层级。 Stack默认居中对齐,emoji自动居中在渐变圆上。但如果emoji尺寸变化(比如换成图片),可能需要调整对齐方式。Stack的默认行为是"后添加的元素在上层",这里先放渐变圆再放emoji,顺序正确。


个人中心是运动健康App中结构最简单的页面——没有复杂布局、没有动态数据、没有网络请求。它的价值在于"信息聚合":把分散在各处的数据(运动天数、卡路里消耗、训练次数)汇总到一个页面,把分散的功能入口(运动记录、饮食记录、设置)排列成一个菜单。从技术角度看,这个页面是Builder模式的最佳练习——Stat、Menu、Div三个Builder各司其职,build方法干净整洁。如果说社区页面是状态管理的综合考试,个人中心就是组件拆分的基础练习。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐