鸿蒙原生应用实战(三):历史记录与日历热力图
2026/6/11 16:13:04 网站建设 项目流程

📊 鸿蒙原生应用实战(三):历史记录与日历热力图

系列目录

  • 第1篇:项目初始化与首页开发
  • 第2篇:训练详情页与计时器功能
  • 第3篇:历史记录与日历热力图 ←当前
  • 第4篇:身体数据记录与趋势分析
  • 第5篇:设置页面与项目总结

一、前言

上一篇我们完成了训练详情页和计时器功能,用户已经可以完成一次完整的训练了。但一个合格的健身 App 还需要让用户看到自己的训练历史——数据可视化是养成运动习惯的关键驱动力

本篇我们开发历史记录页面,包含三个核心模块:

  1. 本月统计概览(训练次数、总时长、连续天数)
  2. 月历热力图(日期网格 + 训练标记)
  3. 周训练时长柱状图

二、页面整体设计

┌────────────────────────────────────┐ │ ← 返回 📊 历史记录 │ ← 顶部导航 ├────────────────────────────────────┤ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │📅 本月 │ │⏱️ 总时长│ │🔥 连续 │ │ ← 统计卡片 │ │ 15次 │ │ 480分钟 │ │ 5天 │ │ │ └────────┘ └────────┘ └────────┘ │ ├────────────────────────────────────┤ │ ← 2026年6月 → │ ← 月份切换 │ 日 一 二 三 四 五 六 │ │ 1🏋️ 2 3🏋️ 4 5🏋️ 6 │ │ 7🏋️ 8 9🏋️ 10 11🏋️ 12 13🏋️ │ ← 日历网格 │ 14 15🏋️ 16 17🏋️ 18 ... │ │ ... │ ├────────────────────────────────────┤ │ 📈 本周训练时长(分钟) │ │ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │ ← 柱状图 │ │45│ │30│ │50│ │40│ │60│ │35│ │ │ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ │ │ 一 二 三 四 五 六 日 │ └────────────────────────────────────┘

三、状态管理

页面需要管理以下状态:

@StatecurrentYear:number=2026;@StatecurrentMonth:number=6;@StatemonthData:MonthDay[]=[];@StateworkoutRecords:Record<string,boolean>={};@StateweeklyStats:number[]=[0,0,0,0,0,0,0];@StatetotalWorkouts:number=0;@StatetotalMinutes:number=0;@StatestreakDays:number=0;

MonthDay接口定义:

interfaceMonthDay{day:number;// 日期数字(0 表示空白占位)hasWorkout:boolean;// 当天是否有训练}

四、月历组件实现

月历是历史记录页面的核心组件,需要处理:

  1. 计算某月有多少天
  2. 计算当月1号是星期几(用于填充空白)
  3. 渲染日期网格
  4. 月份切换

4.1 生成月历数据

loadMonthData():void{constdaysInMonth=newDate(this.currentYear,this.currentMonth,0).getDate();constfirstDayOfWeek=newDate(this.currentYear,this.currentMonth-1,1).getDay();constdata:MonthDay[]=[];// 模拟训练记录(这里实际应该从本地数据库读取)for(leti=1;i<=daysInMonth;i++){this.workoutRecords[`${this.currentYear}-${this.currentMonth}-${i}`]=i%2===0;}// 填充月初空白for(leti=0;i<firstDayOfWeek;i++){data.push({day:0,hasWorkout:false});}// 填充日期for(leti=1;i<=daysInMonth;i++){data.push({day:i,hasWorkout:this.workoutRecords[`${this.currentYear}-${this.currentMonth}-${i}`]||false});}this.monthData=data;}

关键点new Date(year, month, 0).getDate()可以获取某月的总天数。比如new Date(2026, 6, 0).getDate()返回 30(6月有30天)。

4.2 月份切换

prevMonth():void{if(this.currentMonth===1){this.currentYear--;this.currentMonth=12;}else{this.currentMonth--;}this.loadMonthData();}nextMonth():void{if(this.currentMonth===12){this.currentYear++;this.currentMonth=1;}else{this.currentMonth++;}this.loadMonthData();}

4.3 星期头

Row(){ForEach(['日','一','二','三','四','五','六'],(day:string)=>{Text(day).fontSize(13).fontColor('#999').width('14.28%')// 100% / 7.textAlign(TextAlign.Center)})}.width('100%')

每个星期占14.28%(100% ÷ 7),用ForEach循环渲染。

4.4 日期网格

使用Grid组件渲染 7 列网格:

Grid(){ForEach(this.monthData,(item:MonthDay)=>{GridItem(){if(item.day>0){Column(){Text(`${item.day}`).fontSize(14).fontColor(item.hasWorkout?'#FFFFFF':'#333')if(item.hasWorkout){Text('🏋️').fontSize(10).margin({top:2})}}.width('100%').aspectRatio(1)// 保持正方形.backgroundColor(item.hasWorkout?'#FF6B35':'transparent').borderRadius(8)}}})}.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')// 7等分.rowsGap(4).columnsGap(4)

⚠️踩坑columnsTemplate使用1fr单位表示等分比例,7 个1fr就是 7 等分。不能用百分比,否则布局会异常。

热力图效果:有训练记录的日期显示橙色背景 + 白色文字 + 🏋️ 图标,没有训练的日期透明显示。这就是"热力图"的视觉概念——颜色越深表示有活动。


五、统计卡片

页面顶部的三个统计卡片,用@Builder封装:

@BuilderstatCard(icon:string,label:string,value:string,unit:string){Column(){Text(`${icon}${value}`).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FF6B35')Text(unit).fontSize(12).fontColor('#999')Text(label).fontSize(12).fontColor('#666')}.width('30%').padding(12).backgroundColor('#FFFFFF').borderRadius(12).shadow({radius:2,color:'rgba(0,0,0,0.06)'}).alignItems(HorizontalAlign.Center)}

三张卡片并排布局:

Row(){this.statCard('📅','本月训练',`${this.totalWorkouts}`,'次')this.statCard('⏱️','总时长',`${this.totalMinutes}`,'分钟')this.statCard('🔥','连续天数',`${this.streakDays}`,'天')}.width('100%').justifyContent(FlexAlign.SpaceEvenly).padding(12)

六、周柱状图

柱状图是 ArkTS 中比较有技巧性的实现——鸿蒙原生 SDK 没有内置图表组件,我们需要用 Column 组件模拟柱状图

6.1 实现思路

每个柱子是一个Column容器,内部嵌一个子Column作为柱体,柱体的高度通过layoutWeight控制:

ForEach(['一','二','三','四','五','六','日'],(day:string,index:number)=>{Column(){Column(){Text('')// 占位,柱体由 layoutWeight 撑开.width('100%').layoutWeight(this.weeklyStats[index])}.width('100%').height(100)// 固定容器高度.backgroundColor('#FFF0E8').borderRadius(6).justifyContent(FlexAlign.End)Text(day).fontSize(11).fontColor('#999').margin({top:4})}.width('12%').alignItems(HorizontalAlign.Center)})

原理:外部Column固定height: 100,内部子 Column 的layoutWeight等于训练分钟数,数值越大柱体越高。

6.2 数据模拟

this.weeklyStats=[45,30,50,0,40,60,35];// 分别对应 周一 到 周日 的训练分钟数

七、颜色与视觉设计

7.1 配色方案

元素颜色用途
#FF6B35🟠 橙色主题色、训练标记、选中态
#FFFFFF⬜ 白色卡片背景、有训练日期文字
#F5F5F5⬜ 浅灰页面背景
#333333⬛ 深灰主要文字
#999999🔘 中灰次要文字、辅助信息
#FFF0E8🟠 浅橙柱状图背景

7.2 阴影与圆角

HarmonyOS 的卡片设计强调圆角 + 阴影的质感:

.backgroundColor('#FFFFFF').borderRadius(16).shadow({radius:4,color:'rgba(0,0,0,0.08)'})

borderRadius控制圆角大小,shadow控制阴影扩散半径和颜色。


八、踩坑总结

8.1clip方法已弃用

我一开始用.clip(new Rect(...))给柱状图做裁剪,但 SDK 提示该方法已弃用。实际上设置borderRadius后,子组件会自动裁剪到圆角范围内,不需要额外裁剪。

8.2 Grid 的 columnsTemplate

columnsTemplate一定要和实际列数匹配。7 列就用 7 个1fr,不要用repeat(7, 1fr)之类的 CSS 语法——ArkTS 不支持。

8.3 模拟数据 vs 真实数据

目前历史记录的数据是写死的模拟数据:

// 模拟:逢双数日有训练this.workoutRecords[`${year}-${month}-${i}`]=i%2===0;

真实项目中应该从本地数据库(如@ohos.data.preferences或 SQLite)读取。在后续的优化篇中,我们会接入数据持久化。


九、下篇预告

下一篇我们开发身体数据记录页面,实现:

  • 体重、体脂率、BMI 实时展示
  • 新增记录弹窗
  • 历史记录列表
  • 趋势自动判断(下降/上升/持平)

下一篇:身体数据记录与趋势分析 →

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询