HarmonyOS5 运动健康app(二):健康跑步(附代码)
2025-06-28 20:01:35
108次阅读
0个评论
运动记录应用架构设计文档
一、数据模型设计
跑步记录模型 (RunRecord
)
interface RunRecord {
id: string; // 记录唯一标识
date: Date; // 跑步日期
distance: number; // 距离(公里)
duration: number; // 时长(分钟)
pace: number; // 配速(分钟/公里)
}
设计特点:
- 采用"最小必要数据"原则,仅存储核心运动指标
- 通过
id
和date
建立时间轴索引 - 新记录生成逻辑:
const newRecord: RunRecord = {
id: Date.now().toString(),
date: new Date(),
distance: this.currentDistance,
duration: this.currentDuration,
pace: this.currentPace
};
this.runRecords = [newRecord, ...this.runRecords];
二、状态管理系统
核心状态变量
@State runRecords: RunRecord[] = []; // 跑步记录列表
@State isRunning: boolean = false; // 跑步状态标识
@State currentDistance: number = 0; // 实时距离(公里)
@State currentDuration: number = 0; // 实时时长(分钟)
@State currentPace: number = 0; // 实时配速
@State showHistory: boolean = false; // 历史记录可见性
关键状态更新逻辑
开始跑步:
this.intervalId = setInterval(() => {
this.currentDistance += 0.01; // 模拟每100ms增加10米
this.currentDuration += 0.1; // 模拟每100ms增加0.1秒
this.currentPace = this.currentDistance > 0
? this.currentDuration / this.currentDistance / 60
: 0;
}, 100);
结束跑步:
clearInterval(this.intervalId);
// 记录生成与状态重置逻辑...
三、UI组件体系
1. 统计头部组件 (StatsHeader
)
![三栏式布局]
- 总次数:
runRecords.length
- 总距离:
runRecords.reduce().toFixed(1) + "km"
- 平均配速:
(sumPace / recordsCount).toFixed(2) + "min/km"
2. 跑步数据卡片 (RunDataCard
)
两种显示模式:
-
跑步中:
- 大号字体显示实时距离
- 分栏展示配速与时长
- 红色"结束"按钮
-
非跑步状态:
- 三栏展示今日汇总数据
- 绿色"开始"按钮
3. 历史记录列表 (HistoryList
)
ForEach(this.runRecords, (record: RunRecord) => {
ListItem() {
// 日期 | 时长
// 距离 | 配速
}
})
- 空状态提示:"暂无跑步记录"
- 日期格式化为"月/日"显示
四、核心算法模块
1. 时间格式化
private formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs < 10 ? '0' : ''}${secs}`;
}
2. 今日数据聚合
private getTodayPace(): number {
const totalDistance = this.getTodayDistance();
const totalDuration = this.getTodayDuration();
return totalDistance > 0 ? totalDuration / totalDistance / 60 : 0;
}
3. 日期格式化
private formatDate(date: Date): string {
return `${date.getMonth() + 1}/${date.getDate()}`;
}
五、完整实现代码
点击查看完整代码实现import promptAction from '@ohos.promptAction';
// 跑步记录接口
interface RunRecord {
id: string;
date: Date;
distance: number; // 公里
duration: number; // 分钟
pace: number; // 配速(分钟/公里)
}
@Entry
@Component
struct Index {
@State runRecords: RunRecord[] = []; // 跑步记录列表
@State isRunning: boolean = false; // 是否正在跑步
@State currentDistance: number = 0; // 当前跑步距离
@State currentDuration: number = 0; // 当前跑步时长
@State currentPace: number = 0; // 当前配速
@State showHistory: boolean = false; // 是否显示历史记录
private intervalId: number = -1; // 定时器ID
// 开始跑步
private startRun() {
this.isRunning = true;
this.currentDistance = 0;
this.currentDuration = 0;
this.currentPace = 0;
// 模拟GPS定位更新
this.intervalId = setInterval(() => {
this.currentDistance += 0.01; // 模拟每100ms增加10米
this.currentDuration += 0.1; // 模拟每100ms增加0.1秒
// 更新配速
if (this.currentDistance > 0) {
this.currentPace = this.currentDuration / this.currentDistance / 60;
}
}, 100);
}
// 结束跑步
private endRun() {
clearInterval(this.intervalId);
// 创建新记录
const newRecord: RunRecord = {
id: Date.now().toString(),
date: new Date(),
distance: this.currentDistance,
duration: this.currentDuration,
pace: this.currentPace
};
// 添加到记录列表
this.runRecords = [newRecord, ...this.runRecords];
// 重置状态
this.isRunning = false;
this.currentDistance = 0;
this.currentDuration = 0;
this.currentPace = 0;
promptAction.showToast({ message: '跑步记录已保存' });
}
// 格式化时间为分:秒
private formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs < 10 ? '0' : ''}${secs}`;
}
// 获取今日跑步距离
private getTodayDistance(): number {
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayRuns = this.runRecords.filter(record => {
const recordDate = new Date(record.date);
recordDate.setHours(0, 0, 0, 0);
return recordDate.getTime() === today.getTime();
});
return todayRuns.reduce((sum, record) => sum + record.distance, 0);
}
// 获取今日跑步时长
private getTodayDuration(): number {
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayRuns = this.runRecords.filter(record => {
const recordDate = new Date(record.date);
recordDate.setHours(0, 0, 0, 0);
return recordDate.getTime() === today.getTime();
});
return todayRuns.reduce((sum, record) => sum + record.duration, 0);
}
// 获取今日平均配速
private getTodayPace(): number {
const totalDistance = this.getTodayDistance();
const totalDuration = this.getTodayDuration();
if (totalDistance === 0) return 0;
return totalDuration / totalDistance / 60;
}
// 格式化日期
private formatDate(date: Date): string {
return `${date.getMonth() + 1}/${date.getDate()}`;
}
// 头部统计组件
@Builder
StatsHeader() {
Column() {
Text('跑步统计')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 15 })
Row() {
Column() {
Text(`${this.runRecords.length}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text('总次数')
.fontSize(12)
.fontColor('#888')
.margin({top: 5})
}
.width('33%')
Column() {
Text(`${this.runRecords.reduce((sum, r) => sum + r.distance, 0).toFixed(1)}km`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text('总距离')
.fontSize(12)
.fontColor('#888')
.margin({top: 5})
}
.width('33%')
Column() {
Text(`${(this.runRecords.reduce((sum, r) => sum + r.pace, 0) / this.runRecords.length || 0).toFixed(2)}min/km`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text('平均配速')
.fontSize(12)
.fontColor('#888')
.margin({top: 5})
}
.width('33%')
}
.width('100%')
}
.width('100%')
.padding(15)
.backgroundColor('#F8F9FC')
.borderRadius(12)
}
// 跑步数据卡片
@Builder
RunDataCard() {
Column() {
Text(this.isRunning ? '跑步中' : '今日跑步数据')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 25 })
if (this.isRunning) {
// 跑步中数据显示
Column() {
Text(`${this.currentDistance.toFixed(2)}km`)
.fontSize(42)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 15 })
Text(`${this.formatTime(this.currentDuration)}`)
.fontSize(24)
.margin({ bottom: 25 })
Row() {
Column() {
Text(`${this.currentPace.toFixed(2)}min/km`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('配速')
.fontSize(12)
.fontColor('#888')
.margin({top: 5})
}
.width('50%')
Column() {
Text(`${this.formatTime(this.currentDuration)}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('时长')
.fontSize(12)
.fontColor('#888')
.margin({top: 5})
}
.width('50%')
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.margin({ bottom: 25 })
} else {
// 跑步后数据显示
Row() {
Column() {
Text(`${this.getTodayDistance().toFixed(2)}km`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text('距离')
.fontSize(12)
.fontColor('#888')
.margin({top: 5})
}
.width('33%')
Column() {
Text(`${this.formatTime(this.getTodayDuration())}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text('时长')
.fontSize(12)
.fontColor('#888')
.margin({top: 5})
}
.width('33%')
Column() {
Text(`${this.getTodayPace().toFixed(2)}min/km`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text('配速')
.fontSize(12)
.fontColor('#888')
.margin({top: 5})
}
.width('33%')
}
.width('100%')
.margin({ bottom: 25 })
}
if (this.isRunning) {
Button('结束跑步')
.width('100%')
.height(45)
.backgroundColor('#E53935')
.fontColor(Color.White)
.fontSize(16)
.borderRadius(8)
.onClick(() => this.endRun())
} else {
Button('开始跑步')
.width('100%')
.height(45)
.backgroundColor('#2E7D32')
.fontColor(Color.White)
.fontSize(16)
.borderRadius(8)
.onClick(() => this.startRun())
}
}
.width('100%')
.padding(15)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 3, color: '#0000001A' })
}
// 历史记录列表
@Builder
HistoryList() {
if (this.showHistory) {
Column() {
Text('跑步历史')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 15 })
if (this.runRecords.length === 0) {
Text('暂无跑步记录')
.fontSize(14)
.fontColor('#AAA')
.margin({ top: 40 })
} else {
List() {
ForEach(this.runRecords, (record: RunRecord) => {
ListItem() {
Row() {
Column() {
Text(this.formatDate(record.date))
.fontSize(14)
Text(`${this.formatTime(record.duration)}`)
.fontSize(12)
.fontColor('#888')
.margin({top: 4})
}
.width('40%')
Column() {
Text(`${record.distance}km`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
Text(`${record.pace.toFixed(2)}min/km`)
.fontSize(12)
.fontColor('#888')
.margin({top: 4})
}
.width('60%')
}
.width('100%')
.padding(8)
}
})
}
}
}
.width('100%')
.padding(15)
.backgroundColor('#F8F9FC')
.borderRadius(12)
.layoutWeight(1)
}
}
build() {
Column() {
// 统计头部
this.StatsHeader()
// 跑步数据卡片
this.RunDataCard()
// 历史记录
this.HistoryList()
// 底部按钮
Button(this.showHistory ? '隐藏历史' : '显示历史')
.width('100%')
.margin({ top: 15 })
.height(40)
.fontSize(14)
.borderRadius(8)
.backgroundColor('#E0E0E0')
.fontColor('#333')
.onClick(() => {
this.showHistory = !this.showHistory;
})
}
.width('100%')
.height('100%')
.padding(12)
.backgroundColor('#FCFDFF')
}
}
六、数据流转图
[用户操作]
│
├─ 开始跑步 → 启动定时器 → 更新实时数据
│
└─ 结束跑步 → 生成记录 → 重置状态 → 保存记录
│
└─ 更新UI统计面板
00
- 0回答
- 0粉丝
- 0关注
相关话题
- HarmonyOS5 运动健康app(一):健康饮食(附代码)
- HarmonyOS5 运动健康app(三):健康睡眠(附代码)
- HarmonyOS5 购物商城app(二):购物车与支付(附代码)
- HarmonyOS5 购物商城app(一):商品展示(附代码)
- HarmonyOS5 儿童画板app:手绘写字(附代码)
- HarmonyOS5 音乐播放器app(一):歌曲展示与收藏功能(附代码)
- ArkUI-X跨平台技术落地-华为运动健康(二)
- 纯血HarmonyOS5 打造小游戏实践:扫雷(附源文件)
- 鸿蒙HarmonyOS 5小游戏实践:记忆翻牌(附:源代码)
- 纯血HarmonyOS5 打造小游戏实践:绘画板(附源文件)
- HarmonyOS 5 多端适配原理与BreakpointSystem工具类解析:附代码
- 鸿蒙HarmonyOS 5小游戏实践:数字记忆挑战(附:源代码)
- 鸿蒙HarmonyOS 5小游戏实践:打砖块游戏(附:源代码)
- 鸿蒙HarmonyOS 5 小游戏实践:数字华容道(附:源代码)
- 鸿蒙HarmonyOS 5小游戏实践:动物连连看(附:源代码)