news 2026/7/12 2:35:31

HarmonyOS运动健康——数据服务层与数据架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS运动健康——数据服务层与数据架构

每个页面展示的步数、训练计划、动作列表、饮食记录,背后都指向同一个文件:WorkoutData.ets。这个文件是整个App的数据层,所有页面的数据都从这里导出。

拆解一个App不能只看UI,还要看数据是怎么组织的。数据层的设计决定了页面能不能高效渲染、后续能不能方便地接入真实后端。这一篇把WorkoutData.ets从头到尾讲一遍。
完整效果

三个数据模型

Exercise——单个训练动作

export interface Exercise { id: number; name: string; icon: string; category: string; difficulty: string duration: number; calories: number; muscles: string; steps: string[] }

九个字段,描述一个完整的训练动作:

字段类型含义示例
idnumber唯一标识1
namestring动作名称‘俯卧撑’
iconstringemoji图标‘💪’
categorystring分类‘力量’
difficultystring难度‘初级’
durationnumber时长(分钟)10
caloriesnumber消耗卡路里80
musclesstring目标肌群‘胸肌·肱三头肌·核心’
stepsstring[]动作步骤[‘双手撑地与肩同宽’, …]

steps是字符串数组——每个步骤是一条文字说明。ExerciseDetail页面用ForEach渲染这个数组,用户可以一步步跟着做。这种"步骤数组"的设计比把所有步骤拼成一个长字符串更好:每个步骤独立,可以逐条高亮、逐条展示。

WorkoutPlan——训练计划

export interface WorkoutPlan { id: number; name: string; icon: string; desc: string; duration: number exercises: number[]; level: string; color: string }

训练计划的核心是exercises: number[]——一个动作ID数组。不是直接引用Exercise对象,而是存ID。比如"7分钟晨练"计划的exercises是[7,4,3,1,6],对应开合跳、波比跳、平板支撑、俯卧撑、仰卧起坐。

这种"ID引用"模式在数据层设计中很常见——计划只存动作的ID,不存动作的完整数据。好处是:修改一个动作的名称或步骤,所有引用它的计划自动生效。如果直接存对象,改一个动作需要同步修改多个计划。

ActivityRecord——每日活动记录

export interface ActivityRecord { date: string; steps: number; calories: number; minutes: number; workouts: number }

五个字段记录一天的运动数据:日期、步数、卡路里消耗、运动分钟数、训练次数。这个模型被首页的三个数据环(步数/卡路里/运动时间)和本周活动柱状图共用。

三个数据数组

EXERCISES——10个训练动作

export const EXERCISES: Exercise[] = [ { id: 1, name: '俯卧撑', icon: '💪', category: '力量', difficulty: '初级', ... }, { id: 2, name: '深蹲', icon: '🦵', category: '力量', difficulty: '初级', ... }, { id: 3, name: '平板支撑', icon: '🧘', category: '核心', difficulty: '中级', ... }, { id: 4, name: '波比跳', icon: '🔥', category: '有氧', difficulty: '高级', ... }, { id: 5, name: '哑铃弯举', icon: '🏋️', category: '力量', difficulty: '初级', ... }, { id: 6, name: '仰卧起坐', icon: '🔄', category: '核心', difficulty: '初级', ... }, { id: 7, name: '开合跳', icon: '⭐', category: '有氧', difficulty: '初级', ... }, { id: 8, name: '箭步蹲', icon: '🦿', category: '力量', difficulty: '中级', ... }, { id: 9, name: '俄罗斯转体', icon: '💫', category: '核心', difficulty: '中级', ... }, { id: 10, name: '登山者', icon: '⛰️', category: '有氧', difficulty: '中级', ... }, ]

10个动作覆盖三个分类:力量(俯卧撑、深蹲、哑铃弯举、箭步蹲)、核心(平板支撑、仰卧起坐、俄罗斯转体)、有氧(波比跳、开合跳、登山者)。难度从初级到高级都有。

每个动作的steps数组有3-6个步骤,写得像"教练在旁边指导"——“双手撑地与肩同宽”“身体保持直线”“屈肘下降至胸部接近地面”。这些文字不是从数据库导入的,是手动写的。真实项目中这些内容应该从后端API获取,或者由运动科学团队审核后发布。

WORKOUT_PLANS——5个训练计划

export const WORKOUT_PLANS: WorkoutPlan[] = [ { id: 1, name: '7分钟晨练', icon: '🌅', desc: '高效燃脂·唤醒身体', duration: 7, exercises: [7,4,3,1,6], level: '初级', color: '#FF6B6B' }, { id: 2, name: '核心强化', icon: '🎯', desc: '腹肌撕裂·核心稳定', duration: 15, exercises: [3,6,9,10,1], level: '中级', color: '#8A5DFA' }, { id: 3, name: '力量突破', icon: '⚡', desc: '增肌塑形·力量提升', duration: 20, exercises: [1,2,5,8,4], level: '高级', color: '#FECA57' }, { id: 4, name: '燃脂HIIT', icon: '🔥', desc: '高强度间歇·高效燃脂', duration: 12, exercises: [4,7,10,9,5], level: '高级', color: '#E8734A' }, { id: 5, name: '全身拉伸', icon: '🧘', desc: '放松身心·提升柔韧性', duration: 10, exercises: [3,2,8,7,1], level: '初级', color: '#00B894' }, ]

5个计划,每个包含5个动作的ID。计划之间有重叠——俯卧撑(id:1)出现在4个计划中,平板支撑(id:3)出现在3个计划中。这就是"ID引用"的好处:同一个动作可以被多个计划复用,不需要重复定义。

每个计划有自己的颜色(color字段),首页的推荐训练卡片用这个颜色做标签背景。不同计划用不同颜色区分——红色=晨练、紫色=核心、黄色=力量、橙色=HIIT、绿色=拉伸。

WEEKLY_DATA——7天活动记录

export const WEEKLY_DATA: ActivityRecord[] = [ { date: '周一', steps: 8432, calories: 420, minutes: 35, workouts: 2 }, { date: '周二', steps: 12500, calories: 580, minutes: 50, workouts: 3 }, { date: '周三', steps: 6200, calories: 310, minutes: 20, workouts: 1 }, { date: '周四', steps: 15600, calories: 720, minutes: 60, workouts: 3 }, { date: '周五', steps: 9800, calories: 490, minutes: 40, workouts: 2 }, { date: '周六', steps: 18900, calories: 890, minutes: 75, workouts: 4 }, { date: '周日', steps: 5100, calories: 250, minutes: 15, workouts: 1 }, ]

7天的数据,周六最高(18900步、890千卡、75分钟、4次训练),周日最低(5100步、250千卡、15分钟、1次训练)。这种分布符合真实场景——工作日运动量中等,周末有时间多运动,周日休息。

首页用WEEKLY_DATA[3](周四)作为"今天"的数据。这是一个硬编码的问题——真实项目应该根据当前日期动态获取,而不是固定取第四条。

两个工具函数

getExerciseById——按ID查找动作

export function getExerciseById(id: number): Exercise | undefined { for (let i: number = 0; i < EXERCISES.length; i++) { if (EXERCISES[i].id === id) return EXERCISES[i] } return undefined }

遍历EXERCISES数组,找到id匹配的动作返回。找不到返回undefined。ExerciseDetail页面用这个函数——路由参数传入动作ID,页面根据ID获取完整动作数据。

getExercisesForPlan——获取计划的所有动作

export function getExercisesForPlan(plan: WorkoutPlan): Exercise[] { const result: Exercise[] = [] for (let i: number = 0; i < plan.exercises.length; i++) { const ex: Exercise | undefined = getExerciseById(plan.exercises[i]) if (ex) result.push(ex) } return result }

输入一个训练计划,返回该计划包含的所有动作对象。内部调用getExerciseById逐个查找,找到的push到结果数组。WorkoutPage用这个函数——用户选择一个计划后,页面调用getExercisesForPlan获取该计划的动作列表。

这两个函数实现了"ID引用→实际对象"的转换。数据层存ID(轻量),页面层用对象(完整)。中间由工具函数桥接。

数据流向

整个App的数据流是单向的:

WorkoutData.ets(数据源) ↓ export Index.ets → 读取WEEKLY_DATA、WORKOUT_PLANS WorkoutPage.ets → 读取WORKOUT_PLANS、调用getExercisesForPlan ExerciseDetail.ets → 调用getExerciseById ProgressPage.ets → 读取WEEKLY_DATA CommunityPage.ets → 独立数据(不依赖WorkoutData) MealPage.ets → 独立数据(不依赖WorkoutData) ProfilePage.ets → 独立数据(不依赖WorkoutData)

首页、训练页、进度页共用WorkoutData的数据。社区页、饮食页、个人中心页各自有独立的硬编码数据,不走WorkoutData。这意味着WorkoutData不是一个"全局数据服务",而是"运动相关数据服务"——饮食和社区的数据应该在各自的service中定义。

和其他App的数据层对比

属性运动健康WorkoutData购物商城ProductData3D拆解
文件数1个1个3个(AiAnalysisService/AuthService/NetworkService)
接口数3个1个(Product)3个
数据数组3个2个(getFlashSaleProducts/getHotProducts)mock占位
工具函数2个0个0个
ID引用模式有(exercises: number[])
数据持久化

运动健康的数据层是三个App中最完整的——有接口定义、有工具函数、有ID引用模式。购物商城更简单(只有Product接口和两个数据函数)。3D拆解最弱(三个service都是mock占位,没有真实数据)。

踩坑记录

首页硬编码WEEKLY_DATA[3]。Index.ets用@State today: ActivityRecord = WEEKLY_DATA[3]固定取周四的数据。不管用户什么时候打开App,看到的都是周四的数据。真实项目应该用new Date().getDay()获取当前星期几,然后映射到WEEKLY_DATA的索引。但getDay()返回0-6(周日=0),而WEEKLY_DATA的索引是0-6(周一=0),需要做转换:(date.getDay() + 6) % 7

训练计划切换后动作列表不更新。WorkoutPage用@State selectedPlan存储当前选中的计划,但ForEach渲染动作列表时直接用了全局EXERCISES而不是调用getExercisesForPlan。切换计划后,动作列表不会更新——因为ForEach的数据源没变。应该在切换计划时调用getExercisesForPlan并更新@State变量。

没有数据持久化。所有数据都是const数组,运行时不会变化。用户完成一个训练后,WEEKLY_DATA不会自动更新——步数不会增加、卡路里不会累加。真实项目需要AppStorage或数据库来持久化用户数据。

社区和饮食的数据不在WorkoutData中。社区页面的帖子列表、饮食页面的餐食列表各自硬编码在自己的页面文件中。如果以后要做"运动+饮食+社区"的数据联动(比如发帖时自动带上今天的运动数据),需要把所有数据统一到一个数据服务中。

工具函数的线性查找。getExerciseById用for循环遍历数组查找。10个动作没问题,但如果动作数量增长到1000+,线性查找会影响性能。可以用Map建立id→Exercise的索引:const exerciseMap = new Map(EXERCISES.map(e => [e.id, e])),查找变成O(1)。


数据层是App的"后台"——用户看不到它,但所有页面都依赖它。WorkoutData.ets的设计思路是:接口定义数据结构,常量数组存储数据,工具函数提供查询能力。这种"接口+常量+函数"的三层结构是HarmonyOS轻量级应用的典型数据架构。它不需要数据库、不需要网络请求、不需要状态管理框架——用最简单的方式让页面有数据可渲染。当App需要接入真实后端时,只需要把常量数组替换成API调用,接口定义和工具函数可以复用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 2:35:08

AutoMoT:统一VLA架构的自动驾驶新范式

1. AutoMoT不是又一个“端到端黑箱”&#xff0c;而是把自动驾驶的“感知-理解-决策-执行”链条第一次真正拧成一股绳你有没有试过在高速上打开辅助驾驶&#xff0c;突然前方一辆车毫无征兆地压线变道&#xff0c;系统却迟滞半秒才开始减速&#xff1f;或者在无保护左转时&…

作者头像 李华
网站建设 2026/7/12 2:34:45

MinIO Client (mc) 2024 数据迁移实战:3种场景与5个关键参数详解

MinIO Client (mc) 2024 数据迁移实战&#xff1a;3种场景与5个关键参数详解在企业级数据管理领域&#xff0c;MinIO作为高性能对象存储解决方案&#xff0c;其客户端工具mc已成为数据迁移的核心利器。本文将深入剖析mc命令在复杂生产环境中的高阶应用&#xff0c;通过跨版本迁…

作者头像 李华
网站建设 2026/7/12 2:34:45

红日靶场1 内网横向渗透:MS17-010与SMB Beacon 3种攻击路径实测

红日靶场1内网横向渗透实战&#xff1a;MS17-010与SMB Beacon攻击路径深度解析靶场环境与攻击面分析红日靶场1构建了一个典型的企业内网域环境&#xff0c;包含三台核心主机&#xff1a;Win7边界服务器&#xff08;双网卡&#xff09;、Win2003域成员和Win2008域控服务器。网络…

作者头像 李华
网站建设 2026/7/12 2:33:12

MFC中基于WinInet封装CHttpClient类:零依赖HTTP客户端实现指南

1. 项目概述&#xff1a;为什么要在MFC里造一个HTTP轮子&#xff1f;如果你在Windows平台上用C和MFC做客户端开发&#xff0c;迟早会碰到一个需求&#xff1a;从服务器拉点数据&#xff0c;或者往服务器提交点信息。一开始你可能觉得&#xff0c;这还不简单&#xff1f;找个现成…

作者头像 李华