【推荐+1】HarmonyOS官方模板优秀案例(第13期:运动健康 · 运动健身)
在commons/lib_api/src/main/ets/params/response/AuthorResponse.ets文件中将云侧开发者自定义的数据结构转换为端侧数据结构。│├──home/src/main/ets// 首页模块。│├──mine/src/main/ets// 我的模块。
【推荐+1】HarmonyOS官方模板优秀案例
💡鸿蒙生态为开发者提供海量的HarmonyOS模板/组件,助力开发效率原地起飞💡
★ 一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板 ★
★ 一键直达 HarmonyOS 行业解决方案 ,运动健康行业解决方案 ★
👉 覆盖20+行业,点击查看往期案例汇总贴,持续更新,点击收藏!一键三连!常看常新!
【第13期】运动健康 · 运动健身
一、概述
1.行业洞察
1)行业诉求:
运动健康类 APP 的核心及关键在于精准的数据采集与分析、个性化的运动/健康管理方案制定、良好的用户体验设计以及社交激励机制等方面。

-
2)行业常用三方SDK
|
分类 |
三方库名称 |
功能 |
支持情况 |
SDK链接 |
|
媒体 |
阿里云视频播放器SDK |
音视频 |
已支持 |
|
|
登录认证 |
中国移动一键登录SDK/易盾一键登录SDK/创蓝闪验/极光安全认证/阿里云号码认证SDK/中国电信一键登录SDK |
登录 |
已支持 |
|
|
分享 |
友盟/ShareSDK/微信分享/QQ分享/新浪微博SDK/MobTech ShareSDK |
统计/推送/分享 |
已支持 |
|
|
支付 |
支付宝支付/微信支付/银联支付 |
支付 |
已支持 |
|
|
数据分析 |
友盟移动统计SD/神策数据SDK |
数据收集、处理、分析、运用 |
已支持 |
|
|
性能监控 |
腾讯Bugly SDK/听云SDK/岳鹰全景监控SDK |
异常上报和运营统计 |
已支持 |
|
|
地图 |
高德地图SDK/百度地图SDK |
地图 |
已支持 |
|
|
推送 |
个推/华为推送/极光PUSH/阿里推送SDK |
消息推送 |
已支持 |
|
|
社交 |
腾讯云 IM |
即时通讯 |
已支持 |
说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”
2.案例概览(下载模板)
基于以上行业分析,本期将介绍鸿蒙生态市场运动健康类行业模板——运动健身应用模板,为行业提供常用功能的开发案例,模板主要分首页、课程、计划和我的四大模块。
- Stage开发模型 + 声明式UI开发范式。
- 分层架构设计 + 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。
- 本模板为运动健身类应用提供了常用功能的开发样例,已集成华为账号等服务,只需做少量配置和定制即可快速实现华为账号的登录、媒体播放等功能。

本模板主要页面及核心功能如下所示:
|
运动健身模板 ├──引导页 │ ├──目标 │ │ ├── 性别 │ │ ├── 目标 │ │ └── 想锻炼的部位 │ │ │ ├──身体数据 │ │ ├── 年龄 │ │ ├── 身高 │ │ └── 体重和目标体重 │ │ │ └──关于你 │ ├── 是否存在伤病困扰 │ └── 喜欢哪类运动、运动频次、期望练习难度 │ ├──首页 │ ├──轮播页 │ │ ├── 课程封面 │ │ └── 课程详情页 │ │ │ ├──常识普及 │ │ ├── 运动健身常识普及 │ │ └── 常识普及详情页 │ │ │ ├──热门课程 │ │ ├── 课程封面、练习难度、练习时长 │ │ └── 热门课程详情页 │ │ │ └──进阶课程 │ ├── 课程封面、练习难度、练习时长 │ └── 进阶课程详情页 │ ├──课程 │ ├──顶部栏 │ │ ├── 搜索 │ │ └── 课程筛选 │ │ │ ├──热门课程 │ │ ├── 课程封面、练习难度、练习时长 │ │ └── 热门课程详情页 │ │ │ ├──进阶课程 │ │ ├── 课程封面、练习难度、练习时长 │ │ └── 进阶课程详情页 │ │ │ └──课程详情页 │ ├── 课程内容 │ ├── 添加日历 │ ├── 课程视频 │ └── 会员课程 │ ├──计划 │ └──周计划日期 │ │ │ ├──计划课程 │ │ ├── 课程封面、练习难度、练习时长 │ │ └── 计划课程详情页 │ │ │ ├──本周目标 │ │ ├── 预计减重 │ │ ├── 目标体重 │ │ ├── 训练天数 │ │ └── 消耗热量 │ │ │ └──推荐课程 │ ├── 课程封面、练习难度、练习时长 │ ├── 推荐课程详情页 │ ├── 课程内容 │ └── 添加计划 │ └──我的 ├──登录 │ ├── 华为账号一键登录 │ ├── 微信登录 │ ├── 账密登录 │ └── 用户隐私协议同意 │ ├──会员中心 │ ├── 会员套餐 │ ├── 会员权益 │ └── 会员服务协议 │ ├──个人信息 │ ├── 头像、昵称、简介 │ ├── 性别、年龄、身高、体重 │ └── 目标、想锻炼的部位 │ ├──运动记录 │ ├── 日运动时长、总消耗 │ ├── 周运动时长、总消耗、累计训练天数 │ ├── 月运动时长、总消耗、累计训练天数 │ └── 年运动时长、总消耗、累计训练天数 │ └──常用服务 ├── 我的足迹 ├── 我的收藏 ├── 意见反馈 └── 设置 ├── 编辑个人信息 ├── 隐私设置 ├── 通知开关 ├── 视频后台播放 ├── 视频默认清晰度 ├── 清理缓存 ├── 检测版本 ├── 关于我们 └── 退出登录 |
二、应用架构设计
1.分层模块化设计
- 产品定制层:专注于满足不同设备或使用场景的个性化需求,作为应用的入口,是用户直接互动的界面。
- 本实践支持直板机、折叠机,为单HAP包形式,包含路由根节点、底部导航栏等。
- 基础特性层:用于存放相对独立的功能UI和业务逻辑实现。
- 本实践的基础特性层将应用底部导航栏的每个选项拆分成一个独立的业务功能模块。
- 每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。
- 公共能力层:存放公共能力,包括公共UI组件、数据管理、外部交互和工具库等共享功能。
- 本实践的公共能力层分为公共基础能力和可分可合组件,均打包为HAR包被上层业务组件引用。
- 公共基础能力包含日志、文件处理等工具类,公共类型定义,网络库,以及弹窗、加载等公共组件。
- 可分可合组件将包含行业特点、可完全自闭环的能力抽出独立的组件模块,支持开发者在开发中单独集成使用,详见业务组件设计章节。

2.业务组件设计
为支持开发者单独获取特定场景的页面和功能,本模板将功能完全自闭环的部分能力抽离出独立的行业组件模块,不依赖公共基础能力包,开发者可以单独集成,开箱即用,降低使用难度。

三、行业场景技术方案
1.运动记录功能
1)场景说明
- 通过柱状图的形式呈现周、月、年的运动记录数据。

2)技术方案
- 使用开源三方库@ohos/mpchart呈现多类型图表
- 使用开源三方库dayjs实现日期数据格式化。
2.横屏视频播放功能
1)场景说明
- 用户可播放网络视频,视频播放支持加解锁、倍速等功能。

2)技术方案
- 基于AVPlayer实现视频播放能力。
- 使用XComponent渲染视频画面。
- ArkUI搭建控制层操作能力。
四、模板代码
1.工程结构(下载模板)
详细代码结构如下所示:
|
ExerciseAndFitness ├──commons │ ├──lib_account/src/main/ets // 账号登录模块 │ │ ├──components │ │ │ └──AgreePrivacyBox.ets // 隐私同意勾选 │ │ ├──constants // 常量 │ │ ├──pages │ │ │ ├──HuaweiLoginPage.ets // 华为账号登录页面 │ │ │ ├──OtherLoginPage.ets // 其他方式登录页面 │ │ │ └──ProtocolWebView.ets // 协议H5 │ │ ├──services // 服务api │ │ ├──utils │ │ │ ├──ErrorCodeHelper.ets // 错误码处理工具类 │ │ │ ├──HuaweiAuthUtils.ets // 华为认证工具类 │ │ │ ├──LoginSheetUtils.ets // 统一登录半模态弹窗 │ │ │ └──WXApiUtils.ets // 微信登录事件处理类 │ │ └──viewmodels // 视图模型 │ │ │ ├──lib_api/src/main/ets // 服务端api模块 │ │ ├──database // 数据库 │ │ ├──params // 请求响应参数 │ │ └──services // 服务api │ │ │ ├──lib_common/src/main/ets // 基础模块 │ │ ├──components // 通用组件 │ │ ├──constants // 通用常量 │ │ ├──dialogs // 通用弹窗 │ │ ├──mocks // 模拟数据 │ │ ├──models // 状态观测模型 │ │ ├──types // 类型 │ │ └──utils // 通用方法 │ │ │ └──lib_widget/src/main/ets // 通用UI模块 │ └──components │ └──NavHeaderBar.ets // 自定义标题栏 │ ├──components │ ├──aggregated_payment // 通用支付组件 │ ├──module_chart // 图表组件 │ ├──module_feedback // 意见反馈组件 │ ├──module_imagepreview // 图片预览组件 │ ├──module_player // 播放组件 │ ├──module_search // 搜索组件 │ └──module_transition // 动画组件 │ ├──features │ ├──home/src/main/ets // 首页模块 │ │ ├──pages │ │ │ ├──CommonKnowledgeActivatePage.ets // 激活页 │ │ │ ├──CommonKnowledgeDrawPage.ets // 拉伸页 │ │ │ ├──CommonKnowledgeEquipPage.ets // 装备页 │ │ │ ├──CommonKnowledgePage.ets // 常识页面 │ │ │ ├──CommonKnowledgeStrainPage.ets // 避免受伤页 │ │ │ ├──CommonKnowledgeWarmPage.ets // 热身页 │ │ │ ├──CommonKnowledgeYogaPage.ets // 瑜伽页 │ │ │ └──HomePage.ets // 首页页面 │ │ └──viewmodels │ │ └──CommonKnowledgePageVM.ets // 常识页面视图模块 │ ├──course/src/main/ets // 课程模块 │ │ ├──pages │ │ │ ├──AdvancedCoursePage.ets // 进阶课程页面 │ │ │ ├──CourseDetailPage.ets // 课程详情页面 │ │ │ ├──CoursePage.ets // 课程页面 │ │ │ ├──CoursePlayer.ets // 课程视频 │ │ │ ├──CourseSearch.ets // 课程搜索 │ │ │ ├──FilterPage.ets // 课程筛选 │ │ │ └──HotCoursePage.ets // 热门课程 │ │ └──viewmodels │ │ ├──AddDateDialog.ets // 添加日程 │ │ ├──AdvancedCoursePageVM.ets // 进阶课程视图模型 │ │ ├──Calendar.ets // 日历 │ │ ├──CoursePageVM.ets // 课程视图模型 │ │ ├──CustomDialog.ets // 自定义弹窗 │ │ └──HotCoursePageVM.ets // 热门课程视图模型 │ │ │ ├──mine/src/main/ets // 我的模块 │ │ ├──components │ │ │ ├──CourseItemComponent.ets // 课程组件 │ │ │ ├──LeftSwipeComponent.ets // 左滑组件 │ │ │ ├──VipBenefits.ets // VIP权益 │ │ │ ├──VipOpen.ets // 开通VIP │ │ │ ├──VipOperation.ets // VIP操作 │ │ │ └──VipPackage.ets // VIP套餐包 │ │ ├──constants // 常量 │ │ ├──model // 模型 │ │ ├──preview // 预览 │ │ ├──types // 类型 │ │ ├──utils // 工具类 │ │ ├──viewmodels // 视图模型 │ │ └──pages │ │ ├──HistoryPage.ets // 我的足迹 │ │ ├──MarkPage.ets // 我的收藏 │ │ ├──RecordPage.ets // 运动记录 │ │ ├──VIPPage.ets // 会员页面 │ │ └──MinePage.ets // 我的页面 │ │ │ ├──plan/src/main/ets // 计划模块 │ │ ├──pages │ │ │ ├──CoursePlanPage.ets // 课程计划详情页面 │ │ │ ├──PlanPage.ets // 计划主页 │ │ │ └──RecommendationCoursePage.ets // 推荐课程页面 │ │ └──viewmodels │ │ └──PlanPageVM.ets // 计划视图模块 │ │ │ └──setting/src/main/ets // 设置模块 │ ├──components │ │ ├──SettingCard.ets // 设置卡片 │ │ └──SettingSelectDialog.ets // 设置选项弹窗 │ │──pages │ │ ├──SettingAbout.ets // 关于页面 │ │ ├──SettingH5.ets // H5页面 │ │ ├──SettingNetwork.ets // 视频播放设置页面 │ │ ├──SettingPage.ets // 设置页面 │ │ ├──SettingPersonal.ets // 编辑个人信息页面 │ │ └──SettingPrivacy.ets // 隐私设置页面 │ ├──types // 类型 │ └──viewmodels // 视图模型 │ └──products └──phone/src/main/ets // phone模块 ├──common │ ├──AppTheme.ets // 应用主题色 │ ├──AppUtils.ets // 应用设置工具类 │ ├──Constants.ets // 业务常量 │ ├──NetworkUtils.ets // 网络工具类 │ ├──Types.ets // 数据模型 │ ├──WantUtils.ets // want工具类 │ └──WindowUtils.ets // 窗口工具类 ├──components │ └──CustomTabBar.ets // 应用底部Tab │──pages │ ├──AgreeDialogPage.ets // 隐私同意弹窗 │ ├──GuidePage_*.ets // 引导页面 │ ├──Index.ets // 入口页面 │ ├──IndexPage.ets // 应用主页面 │ ├──PrivacyPage.ets // 查看隐私协议页面 │ ├──SafePage.ets // 隐私同意页面 │ └──SplashPage.ets // 开屏广告页面 └──viewmodels // 视图模型 |
2.关键代码解读
本篇代码非应用的全量代码,只包括应用的部分能力的关键代码。
1)图表组件
|
markerBuilder() { // 是否在图表content内&&是否有数据 if (this.customUiInfo.isInbounds && this.customUiInfo.data && this.customUiInfo.y) { Column() { Text() { Span(`${this.customUiInfo.data.getY()}分钟`); } .fontSize($r('sys.float.Body_M')); } .borderRadius(99) .justifyContent(FlexAlign.Center) .padding(12) .backgroundColor(Color.White) .shadow({ radius: 20, offsetX: 0, offsetY: 0, color: '#33000000', }) .width(this.customUiInfo.width) .height(this.customUiInfo.height) .margin({ left: this.customUiInfo.x, top: this.customUiInfo.y }) .onAppear(() => { const timer = setTimeout(() => { this.customUiInfo.showUi = false; clearTimeout(timer); }, 2000); }) .alignItems(HorizontalAlign.Start) .onClick(() => { this.customUiInfo.showUi = false; }); } } async initChartSetting() { // 显示描述 this.model.getDescription()?.setEnabled(false); // 初始化动画时长 this.model.animateY(1000); // 显示图例 this.model.getLegend()?.setEnabled(false); // 设置额外偏移量 this.model.setExtraOffsets(10, 10, 10, 5); // 绘制柱状体阴影背景 this.model.setDrawBarShadow(false); // 柱状体上方显示数值 this.model.setDrawValueAboveBar(false); // 为图表添加数据选择的监听器 this.model.setOnChartValueSelectedListener(this._valueSelectedListener); // 绘制图表背景色 this.model.setDrawGridBackground(false); // 设置左轴 const leftAxis = this.model.getAxisLeft(); if (leftAxis) { leftAxis.setAxisMinimum(0); leftAxis.setGridColor(ChartColor.argb(60, 0, 0, 0)); leftAxis.setTextColor(ChartColor.argb(200, 0, 0, 0)); leftAxis.setAxisLineColor(ChartColor.rgb(255, 255, 255)); leftAxis.setLabelXOffset(-10); } // 设置右轴不显示 this.model.getAxisRight()?.setEnabled(false); const xAxis = this.model.getXAxis(); if (xAxis) { xAxis.setValueFormatter(new MyAxisValueFormatter(this.chartData.month)); xAxis.setPosition(XAxisPosition.BOTTOM); xAxis.setDrawGridLines(false); xAxis.setAxisLineColor(ChartColor.argb(60, 0, 0, 0)); xAxis.setTextColor(ChartColor.argb(200, 0, 0, 0)); } // 初始化图表数据 this.getChartData(); } async getChartData() { const month = new Date(this.chartData.month); this.model.getXAxis()?.setValueFormatter(new MyAxisValueFormatter(this.chartData.month)); const totalDayNum = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate(); this.displayData = []; for (let i = 1; i <= totalDayNum; i++) { const bill = this.chartData.data.find((item) => { const day = item.date.split('-')[2]; return Number(day) === i; }); if (bill) { this.displayData.push({ date: bill.date, value: bill.value, }); } else { const date = dayjs().set('month', month.getMonth()).set('date', i); this.displayData.push({ date: date.format('YYYY-MM-DD'), value: 0, }); } } await this.setData(); } async setData() { let dataSetList: JArrayList<IBarDataSet> = new JArrayList<IBarDataSet>(); let values: JArrayList<BarEntry> = new JArrayList<BarEntry>(); for (let i = 0; i < this.displayData.length; i++) { values.add(new BarEntry(i + 1, this.displayData[i].value)); } let dataSet: BarDataSet = new BarDataSet(values, 'DataSet'); dataSet.setColorByColor(this.initColor); dataSet.setDrawValues(false); dataSet.setHighLightColor(this.highlightColor); // 显示数据格式化 dataSetList.add(dataSet); let barData: BarData = new BarData(dataSetList); barData.setBarWidth(0.85); if (this.model) { this.model.setData(barData); } return barData; } |
2)视频播放组件
|
// 设置窗口方向 setR(orientation: number) { window.getLastWindow(getContext(this)).then((win) => { win.setPreferredOrientation(orientation).then(() => { }).catch(() => { }) }).catch(() => { }) } // 设置沉浸式窗口 setFullScreen(isLayoutFullScreen: boolean) { window.getLastWindow(getContext(this)).then((win: window.Window) => { win.setWindowLayoutFullScreen(isLayoutFullScreen).then(() => { }).catch(() => { }) }).catch(() => { }) this.fullScreen(isLayoutFullScreen) this.setComponentHeight() } regDisplayListener(): void { display.on('foldStatusChange', async (curFoldStatus: display.FoldStatus) => { // 同一个状态重复触发不做处理 if (this.curFoldStatus === curFoldStatus) { return; } this.curFoldStatus = curFoldStatus; this.setComponentHeight() }) } setComponentHeight() { let screenWidth = display.getDefaultDisplaySync().width if (this.curFoldStatus === display.FoldStatus.FOLD_STATUS_FOLDED || this.curFoldStatus === display.FoldStatus.FOLD_STATUS_UNKNOWN) { this.componentHeight = '100%' } else { if (this.isLayoutFullScreen) { let height = (this.getUIContext().px2vp(screenWidth) - 32) * 9 / 16 this.componentHeight = height.toString() + 'vp' } else { this.componentHeight = '100%' } } } async aboutToAppear() { if (this.movieItems && this.movieItems.length > 0) { this.playerVM.movieItemList = this.movieItems this.playerVM.movieItem = this.playerVM.movieItemList[this.playerIndex] } PipManager.getInstance().init(getContext(this), this.playerVM.navId) window.getLastWindow(getContext(this)).then((currentWindow: window.Window) => { this.mWindow = currentWindow; // 获取窗口属性 let properties = currentWindow.getWindowProperties(); let rect = properties.windowRect; if (this.getUIContext().px2vp(rect.height) <= 680) { this.windowStatusType = 5 this.playerVM.windowStatusType = this.windowStatusType this.isLayoutFullScreen = false this.setR(1) this.setFullScreen(false) } currentWindow.on('windowStatusChange', (windowStatusType) => { this.windowStatusType = windowStatusType this.playerVM.windowStatusType = this.windowStatusType if (windowStatusType === 5) { this.isLayoutFullScreen = false this.setR(1) this.setFullScreen(false) } }); }) this.isLayoutFullScreen = true this.setR(this.isLayoutFullScreen ? 2 : 1) this.setFullScreen(this.isLayoutFullScreen) emitter.on('playerPause', (event: emitter.EventData) => { let isPlaying = event.data!.isPlaying as boolean if (isPlaying) { PipManager.getInstance().player.isResetPause = false PipManager.getInstance().player.isPlaying = true } else { PipManager.getInstance().player.isResetPause = true PipManager.getInstance().player.isPlaying = false } }) emitter.on('autoStartPip', () => { if (this.playerVM.autoStartEnabled) { if (PipManager.getInstance().player.isPipOpen) { } else { PipManager.getInstance().startPip() PipManager.getInstance().updatePiPControlStatus(); } } else { if (PipManager.getInstance().player.isPipOpen) { } else { PipManager.getInstance().player.isPlaying = false } } }) emitter.on('chapterDetailBack', () => { if (!PipManager.getInstance().player.isPipOpen) { // 释放AVPlayer资源 PipManager.getInstance().stopPip() PipManager.getInstance().player.isCreate = true PipManager.getInstance().player.release(); } }) if (display.isFoldable()) { this.regDisplayListener(); } this.curFoldStatus = display.getFoldStatus() this.setComponentHeight() } aboutToDisappear() { // 释放AVPlayer资源 PipManager.getInstance().player.isCreate = true PipManager.getInstance().player.release(); this.setR(1) this.setFullScreen(true) this.isLayoutFullScreen = false } @Builder xComponentView() { Column() { XComponent({ type: XComponentType.SURFACE, controller: PipManager.getInstance().getXComponentController() }) .id('XComponent01') .layoutWeight(1) .onLoad(() => { this.surfaceId = PipManager.getInstance().getXComponentController().getXComponentSurfaceId(); // 将surfaceId设置给媒体源 PipManager.getInstance().getXComponentController().onSurfaceCreated(this.surfaceId) PipManager.getInstance().player.init(this.courseId, this.playerVM.movieItem.movieUrl, this.playerVM.movieItem.learnProgress) PipManager.getInstance().player.callbackTimeUpdate = (vol: number, total: number) => { this.callbackTimeUpdate(vol, total) this.playerVM.movieItem.learnProgress = Math.floor(vol / total * 100) } }) } .width('100%') .height(this.componentHeight) } |
以上代码展示了运动健身应用的核心功能实现,包括运动记录柱状图、视频播放等技术方案。
3.模板集成
本模板提供了两种代码集成方式,供开发者自由使用。
1)整体集成(下载模板)
开发者可以选择直接基于模板工程开发自己的应用工程。
- 模板代码获取:
- 打开模板工程,根据README说明中的快速入门章节,将自己的应用信息配置在模板工程内,即可运行并查看模板效果。

- 对接开发者自己的服务器接口,转换数据结构,展示真实的云侧数据。
在commons/lib_api/src/main/ets/params/response/AuthorResponse.ets文件中将云侧开发者自定义的数据结构转换为端侧数据结构。

2)按需集成
若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。
- 组件代码获取:
- 下载组件源码,根据README中的说明,将组件包配置在自己的工程中。
- 根据API参考和示例代码,将组件集成在自己的对应场景中。
以上是第13期“运动健身”行业优秀案例的内容,更多行业敬请期待~
欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解XX行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~
同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新&活动不错过!

👉HarmonyOS官方模板优秀案例系列持续更新, 点击查看往期案例汇总贴, 点击收藏 “
”
方便查找!
👉【集成有礼】HarmonyOS官方模板集成创新活动,挥洒创意,赢精美大礼!点击参加
👉【HarmonyOS行业解决方案】为各行业鸿蒙应用提供全流程技术方案。点击查看
更多推荐


所有评论(0)