进度页面是运动健康App的数据汇总页。用户从首页点击"运动记录"或底部导航"进度"跳转到这里,查看本周的运动数据和统计概览。这个页面的任务是:把散落在各处的运动数据(步数、卡路里、时长、次数)汇总成直观的图表和数字,让用户一眼看到"我这周运动了多少"。

页面结构分为两部分:上方的本周活动柱状图和下方的统计概览卡片。柱状图展示每天的趋势变化,统计卡片展示累计总量。从"趋势"到"总量",信息从细节到宏观。
完整效果
在这里插入图片描述
在这里插入图片描述

柱状图的实现

数据源和渲染

Text('本周活动').fontSize(17).fontWeight(FontWeight.Bold).fontColor(T1)
  .width('100%').padding({ left: 16 }).margin({ bottom: 12 })

Row() {
  ForEach(WEEKLY_DATA, (day: ActivityRecord) => {
    Column() {
      Column() {}.width(28).height(day.calories / 15).borderRadius(14)
        .linearGradient({ angle: 180, colors: [[A, 0], ['#FF9F43', 1]] })
      Text(day.calories.toString()).fontSize(10).fontColor(T2).margin({ top: 6 })
      Text(day.date).fontSize(10).fontColor(T3)
    }.alignItems(HorizontalAlign.Center).layoutWeight(1)
  })
}.width('100%').height(120).padding({ left: 16, right: 16 })
.alignItems(VerticalAlign.Bottom).margin({ bottom: 20 })

在这里插入图片描述

柱状图用ForEach渲染七天的数据,每天是一个Column(柱子+卡路里数值+日期)。

柱子高度的缩放系数

.width(28).height(day.calories / 15).borderRadius(14)

柱子宽度28px,高度由day.calories / 15计算。和首页的柱状图对比:

属性 首页 进度页
柱子宽度 22px 28px
高度计算 calories / 20 calories / 15
圆角 11px(宽度一半) 14px(宽度一半)
区域高度 100px 120px

进度页的柱子更宽(28 vs 22)、更高(除以15 vs 除以20)、区域更大(120 vs 100)。这是因为进度页是专门的数据展示页,有更多空间给柱状图。首页的柱状图是"预览",进度页的柱状图是"详情"——同一个图表在不同页面的尺寸不同,符合信息层级。

除以15意味着最大卡路里值不能超过1800(120×15),否则柱子会溢出区域。首页除以20的最大上限是2000(100×20)。两个页面的上限不同,但都没有做动态计算。

底部对齐

.alignItems(VerticalAlign.Bottom)

Row的alignItems(VerticalAlign.Bottom)让所有柱子底部对齐——矮柱子在上面,高的柱子在下面。这是柱状图的标准对齐方式,底部代表"零"。

柱子上方的数值

Text(day.calories.toString()).fontSize(10).fontColor(T2).margin({ top: 6 })

每个柱子上方显示卡路里数值,10px灰色。margin({ top: 6 })在柱子和数值之间留6px间距。数值是固定的——直接显示WEEKLY_DATA中的calories值。

统计概览卡片

2×2 网格布局

Text('统计概览').fontSize(17).fontWeight(FontWeight.Bold).fontColor(T1)
  .width('100%').padding({ left: 16 }).margin({ bottom: 12 })
Row({ space: 10 }) {
  this.SCard('总步数', '76,532', '本周', A)
  this.SCard('总消耗', '3,660', '千卡', '#FF9F43')
}.width('100%').padding({ left: 16, right: 16 }).margin({ bottom: 10 })
Row({ space: 10 }) {
  this.SCard('运动时长', '295', '分钟', '#5DADE2')
  this.SCard('训练次数', '16', '次', '#00B894')
}.width('100%').padding({ left: 16, right: 16 })

在这里插入图片描述

四个统计卡片分成两行,每行两个。用两个Row实现2×2网格,而不是用Grid或Flex-wrap。原因是:ArkUI的Grid组件需要指定列数和行列间距,对于简单的2×2布局来说太重了。两个Row更直观——每个Row放两个卡片,space: 10控制卡片间距。

SCard Builder

@Builder SCard(label: string, value: string, unit: string, color: string) {
  Column() {
    Text(value).fontSize(28).fontWeight(FontWeight.Bold).fontColor(color)
    Text(unit).fontSize(11).fontColor(T3).margin({ top: 4 })
    Text(label).fontSize(12).fontColor(T2).margin({ top: 4 })
  }.layoutWeight(1).padding(16).backgroundColor('#FFFFFF').borderRadius(16)
  .shadow({ radius: 4, color: 'rgba(0,0,0,0.03)', offsetY: 1 })
}

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

layoutWeight(1)让两个卡片均分Row的宽度。padding(16)给卡片内部留白,避免文字紧贴边框。backgroundColor('#FFFFFF')白色背景+borderRadius(16)圆角+shadow轻微阴影,和首页的训练卡片风格一致。

四个统计指标的含义

指标 数值 单位 颜色 含义
总步数 76,532 本周 红色(A) 一周累计步数
总消耗 3,660 千卡 橙色(#FF9F43) 一周累计卡路里消耗
运动时长 295 分钟 蓝色(#5DADE2) 一周累计运动时间
训练次数 16 绿色(#00B894) 一周训练次数

四个颜色和首页的数据环一致:红色=步数,橙色=卡路里,蓝色=运动时间,绿色=训练次数。颜色体系在不同页面之间保持统一,用户看到红色就联想到"步数",不管在哪个页面。

数据的硬编码问题

四个统计值(76532、3660、295、16)是硬编码的字符串,不是从WEEKLY_DATA动态计算的。如果把WEEKLY_DATA七天的卡路里加起来,和3660对不上。这意味着柱状图和统计卡片展示的是"假数据"——柱状图来自真实数据源,统计卡片是静态写死的。

实际项目中应该动态计算:

const totalCalories = WEEKLY_DATA.reduce((sum, day) => sum + day.calories, 0)
const totalSteps = WEEKLY_DATA.reduce((sum, day) => sum + day.steps, 0)

但原型阶段硬编码够用——页面布局和交互逻辑已经验证了,数据对接是后续的工作。

页面的数据流

ProgressPage没有@State变量——它不需要响应式渲染。数据来自WEEKLY_DATA(导入的常量)和硬编码的统计值。用户在这个页面只能"看",不能"操作"(没有筛选、排序、编辑功能)。

这种"纯展示"页面的实现最简单——不需要状态管理,不需要事件处理,只需要把数据渲染成UI。但缺点是数据不实时——如果用户刚跑完步,步数不会自动更新。实际项目中应该在onPageShow中刷新数据。

和首页柱状图的复用

首页和进度页都有柱状图,代码几乎一样——都是ForEach渲染WEEKLY_DATA,每天一个Column。区别在于尺寸和样式:首页柱子更窄(22px vs 28px),区域更小(100px vs 120px)。

这两个柱状图没有抽成公共组件——每个页面各自实现了一遍。如果以后要修改柱状图的样式(比如加动画、改颜色),需要改两个地方。更好的做法是抽成一个@Builder WeeklyChart(height: number, barWidth: number),两个页面调用时传入不同参数。

但原型阶段的重复是可以接受的——先验证布局,再优化代码结构。过早抽象反而会增加理解成本。

踩坑记录

柱状图高度溢出。 day.calories / 15的系数15是硬编码的。如果某天卡路里超过1800(120×15),柱子高度会超过区域高度,和其他柱子重叠。首页用20作为系数,上限是2000。两个页面的上限不同,但都没有做动态计算或上限截断。

2×2网格的间距不均。 两个Row各自有space: 10,但Row之间没有设置间距——第二行紧贴第一行。实际上有margin({ bottom: 10 })在第一个Row下方,但第二个Row没有margin。如果卡片高度不一致(比如数值长度不同导致文字换行),两行之间可能出现不对齐。

SCard的layoutWeight。 两个SCard都用layoutWeight(1)均分宽度。如果以后要改成三个卡片一行,需要把layoutWeight和Row的space配合调整。三个卡片时space需要减小,否则间距太大。

统计值的数字格式。 76532显示为"76,532"——有千位分隔符。但这个分隔符是硬编码在字符串里的,不是动态格式化的。实际项目中应该用数字格式化:totalSteps.toLocaleString()。但鸿蒙的toLocaleString支持可能不完整,需要测试。

柱状图的触摸反馈。 柱子没有绑定onClick——用户不能点击某个柱子查看当天的详细数据。如果要加交互,需要在ForEach的Column上绑定onClick,传入day参数,跳转到当天的详情页。


进度页面的核心价值是"数据可视化"——把一周的运动数据变成柱状图和统计卡片。柱状图展示趋势(哪天运动多、哪天运动少),统计卡片展示总量(一共运动了多少)。两个维度互补——趋势告诉用户"哪天偷懒了",总量告诉用户"整体表现如何"。颜色体系和首页保持一致,用户不需要重新学习"红色代表什么"。

Logo

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

更多推荐