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唯一标识1namestring动作名称‘俯卧撑’iconstringemoji图标‘’categorystring分类‘力量’difficultystring难度‘初级’durationnumber时长(分钟)10caloriesnumber消耗卡路里80musclesstring目标肌群‘胸肌·肱三头肌·核心’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个Product3个数据数组3个2个getFlashSaleProducts/getHotProductsmock占位工具函数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调用接口定义和工具函数可以复用。