本实训案例旨在构建一款,可运行于鸿蒙智能手表设备上的,呼吸训练应用。借助该应用,用户可以跟随界面指引进行呼吸训练,并在训练后对包括情绪、心率、活动分布、压力分布以及最大摄氧量等在内的,健康数据进行查看。该应用的功能并非重点。通过本实训案例,任何对计算机编程稍有基础的学习者,都能够快速上手基于鸿蒙操作系统的应用开发,并对HUAWEI DevEco Studio开发环境、JavaScript编程语言,有一个初步的了解和掌握。
本实训案例所要实现的呼吸训练应用由以下九个页面构成:
它们的跳转关系如下图所示:
为了方便不同环境下的学习者开发测试本案例中的代码,在接下来几个报告页面中使用的数据,都是由程序随机生成的测试数据,而并非采集自真实的设备。相对于本实训案例所要达到的目标,这些数据的来源和真实性并不重要,重要的是让学习者掌握在鸿蒙系统中,对数据进行分析和可视化的方法。
用HUAWEI DevEco Studio创建一个面向Lite Wearable设备的应用,将页面中默认显示的“Hello World”改为“你好鸿蒙”。
<div class="container"> <text class="title">你好{{title}}</text> </div>
export default { data: { title: "鸿蒙" } }
在页面中添加一个按钮,在其被按下后打印日志。
<div class="container"> <text class="title">你好{{title}}</text> <input class="btn" type="button" value="按我" onclick="onClick" /> </div>
.container { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .title { width: 200px; height:100px; font-size: 30px; text-align: center; } .btn { width: 200px; height: 50px; }
export default { data: { title: "鸿蒙" }, onClick() { console.log("我被按了"); } }
添加训练页面,其中包含一个按钮。按主页面中的按钮,跳转到训练页面。按训练页面中的按钮,返回到主页面。
<div class="container"> <text class="title">训练页面</text> <input class="btn" type="button" value="返回" onclick="onClick" /> </div>
.container { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .title { width: 200px; height:100px; font-size: 30px; text-align: center; } .btn { width: 200px; height: 50px; }
import router from "@system.router" export default { onClick() { router.replace({uri: "pages/index/index"}) } }
import router from "@system.router" export default { data: { title: "鸿蒙" }, onClick() { router.replace({uri: "pages/training/training"}) } }
在应用被创建和销毁时,在每个页面被初始化、就绪、显示和销毁时打印日志。
export default { onCreate() { console.log("应用正在创建..."); }, onDestroy() { console.log("应用正在销毁..."); } };
import router from "@system.router" export default { data: { title: "鸿蒙" }, onInit() { console.log("主页面正在初始化..."); }, onReady() { console.log("主页面就绪..."); }, onShow() { console.log("主页面正在显示..."); }, onDestroy() { console.log("主页面正在销毁..."); }, onClick() { router.replace({uri: "pages/training/training"}) } }
import router from "@system.router" export default { onInit() { console.log("训练页面正在初始化..."); }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); }, onDestroy() { console.log("训练页面正在销毁..."); }, onClick() { router.replace({uri: "pages/index/index"}) } }
在主页面的中心位置显示应用徽标,并在其左右两侧添加两个选择器,其中:
<div class="divCol"> <div class="divRow"> <picker-view class="pvDuration" range="{{durationRange}}" /> <text class="txt">分</text> <image class="img" src="/common/logo.png" /> <picker-view class="pvRhythm" range="{{rhythmRange}}" /> </div> <input class="btn" type="button" value="按我" onclick="onClick" /> </div>
.divCol { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .divRow { width: 454px; height: 250px; flex-direction: row; justify-content: center; align-items: center; } .pvDuration { width: 30px; height: 250px; } .txt { width: 50px; height: 36px; text-align: center; } .img { width: 208px; height: 208px; } .pvRhythm { width: 80px; height: 250px; } .btn { width: 200px; height: 50px; }
import router from "@system.router" export default { data: { durationRange: ["1", "2", "3"], rhythmRange: ["较慢", "舒缓", "较快"] }, onInit() { console.log("主页面正在初始化..."); }, onReady() { console.log("主页面就绪..."); }, onShow() { console.log("主页面正在显示..."); }, onDestroy() { console.log("主页面正在销毁..."); }, onClick() { router.replace({uri: "pages/training/training"}) } }
将主页面中左右两个选择器的默认选项分别设为2分和舒缓,并在选项改变时打印其值。
<div class="divCol"> <div class="divRow"> <picker-view class="pvDuration" range="{{durationRange}}" selected="1" onchange="onDurationChange"/> <text class="txt">分</text> <image class="img" src="/common/logo.png" /> <picker-view class="pvRhythm" range="{{rhythmRange}}" selected="1" onchange="onRhythmChange"/> </div> <input class="btn" type="button" value="按我" onclick="onClick" /> </div>
import router from "@system.router" export default { data: { durationRange: ["1", "2", "3"], rhythmRange: ["较慢", "舒缓", "较快"] }, onInit() { console.log("主页面正在初始化..."); }, onReady() { console.log("主页面就绪..."); }, onShow() { console.log("主页面正在显示..."); }, onDestroy() { console.log("主页面正在销毁..."); }, onDurationChange(pv) { console.log("时长改变:" + pv.newSelected); }, onRhythmChange(pv) { console.log("节奏改变:" + pv.newSelected); }, onClick() { router.replace({uri: "pages/training/training"}) } }
按下按钮从主页面跳转到训练页面,同时将两个选择器的值也传给训练页面,并在训练页面被初始化时打印这两个值。
<div class="divCol"> <div class="divRow"> <picker-view class="pvDuration" range="{{durationRange}}" selected="{{durationSelected}}}" onchange="onDurationChange"/> <text class="txt">分</text> <image class="img" src="/common/logo.png" /> <picker-view class="pvRhythm" range="{{rhythmRange}}" selected="{{rhythmSelected}}" onchange="onRhythmChange"/> </div> <input class="btn" type="button" value="按我" onclick="onClick" /> </div>
import router from "@system.router" export default { data: { durationRange: ["1", "2", "3"], durationSelected: 1, rhythmRange: ["较慢", "舒缓", "较快"], rhythmSelected: 1 }, onInit() { console.log("主页面正在初始化..."); }, onReady() { console.log("主页面就绪..."); }, onShow() { console.log("主页面正在显示..."); }, onDestroy() { console.log("主页面正在销毁..."); }, onDurationChange(pv) { console.log("时长改变:" + pv.newSelected); this.durationSelected = pv.newSelected; }, onRhythmChange(pv) { console.log("节奏改变:" + pv.newSelected); this.rhythmSelected = pv.newSelected; }, onClick() { router.replace({uri: "pages/training/training", params: { "durationSelected": this.durationSelected, "rhythmSelected": this.rhythmSelected}}); } }
import router from "@system.router" export default { onInit() { console.log("训练页面正在初始化..."); console.log("时长参数:" + this.durationSelected); console.log("节奏参数:" + this.rhythmSelected); }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); }, onDestroy() { console.log("训练页面正在销毁..."); }, onClick() { router.replace({uri: "pages/index/index"}) } }
将主页面和训练页面中按钮上的文本分别改为“开始”和“重新开始”,字体调大,黑色背景。增加训练页面中文本和按钮的间距。
.divCol { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .divRow { width: 454px; height: 250px; flex-direction: row; justify-content: center; align-items: center; } .pvDuration { width: 30px; height: 250px; } .txt { width: 50px; height: 36px; text-align: center; } .img { width: 208px; height: 208px; } .pvRhythm { width: 80px; height: 250px; } .btn { width: 200px; height: 50px; font-size: 38px; background-color: #000000; border-color: #000000; }
.container { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .title { width: 200px; height:100px; font-size: 30px; text-align: center; } .btn { width: 300px; height: 50px; font-size: 38px; background-color: #000000; border-color: #000000; margin-top: 40px; }
在训练页面上根据从主页面传入的,训练时长选择器的值,显示训练需要持续的秒数。
训练时长选择器的值 | 训练时长选择器文本 | 训练需要持续的秒数 |
---|---|---|
0 | 1 | 60 |
1 | 2 | 120 |
2 | 3 | 180 |
<div class="container"> <text class="title">总共需要坚持{{duration}}秒</text> <input class="btn" type="button" value="重新开始" onclick="onClick" /> </div>
import router from "@system.router" export default { data: { duration: 0 }, onInit() { console.log("训练页面正在初始化..."); console.log("时长参数:" + this.durationSelected); console.log("节奏参数:" + this.rhythmSelected); if (this.durationSelected == 0) this.duration = 60; else if (this.durationSelected == 1) this.duration = 120; else if (this.durationSelected == 2) this.duration = 180; }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); }, onDestroy() { console.log("训练页面正在销毁..."); }, onClick() { router.replace({uri: "pages/index/index"}) } }
一进入训练页面即开始计时,显示本次训练剩余的秒数,逐秒递减,减到零为止。
<div class="container"> <text class="title">再坚持{{duration}}秒</text> <input class="btn" type="button" value="重新开始" onclick="onClick" /> </div>
import router from "@system.router" var timer = null; export default { data: { duration: 0 }, onInit() { console.log("训练页面正在初始化..."); console.log("时长参数:" + this.durationSelected); console.log("节奏参数:" + this.rhythmSelected); if (this.durationSelected == 0) this.duration = 60; else if (this.durationSelected == 1) this.duration = 120; else if (this.durationSelected == 2) this.duration = 180; }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); timer = setInterval(this.onTimeout, 1000); }, onDestroy() { console.log("训练页面正在销毁..."); clearInterval(timer); timer = null; }, onClick() { router.replace({uri: "pages/index/index"}) }, onTimeout() { this.duration--; if (this.duration == 0) { clearInterval(timer); timer = null; } } }
当本次训练的剩余秒数减到零时,隐藏剩余秒数文本,即不显示“再坚持0秒”。
<div class="container"> <text class="title" show="{{visible}}">再坚持{{duration}}秒</text> <input class="btn" type="button" value="重新开始" onclick="onClick" /> </div>
import router from "@system.router" var timer = null; export default { data: { visible: true, duration: 0 }, onInit() { console.log("训练页面正在初始化..."); console.log("时长参数:" + this.durationSelected); console.log("节奏参数:" + this.rhythmSelected); if (this.durationSelected == 0) this.duration = 60; else if (this.durationSelected == 1) this.duration = 120; else if (this.durationSelected == 2) this.duration = 180; }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); timer = setInterval(this.onTimeout, 1000); }, onDestroy() { console.log("训练页面正在销毁..."); clearInterval(timer); timer = null; }, onClick() { router.replace({uri: "pages/index/index"}) }, onTimeout() { this.duration--; if (this.duration == 0) { clearInterval(timer); timer = null; this.visible = false; } } }
在训练过程中交替显示“吸气”和“呼气”,每次呼吸持续的秒数,由从主页面传入的,呼吸节奏选择器的值决定。最后显示“已完成”。
呼吸节奏选择器的值 | 呼吸节奏选择器文本 | 每次呼吸持续的秒数 |
---|---|---|
0 | 较慢 | 6 |
1 | 舒缓 | 4 |
2 | 较快 | 2 |
<div class="container"> <text class="txtBreath">{{breath}}</text> <text class="txtDuration" show="{{visible}}"> 再坚持{{duration}}秒 </text> <input class="btn" type="button" value="重新开始" onclick="onClick" /> </div>
.container { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .txtBreath { width: 454px; height:46px; font-size: 38px; text-align: center; margin-bottom: 10px; } .txtDuration { width: 400px; height:40px; font-size: 30px; text-align: center; } .btn { width: 300px; height: 50px; font-size: 38px; background-color: #000000; border-color: #000000; margin-top: 40px; }
import router from "@system.router" var timerDuration = null; var timerRhythm = null; var rhythm = 0; var times = 0; export default { data: { breath: "吸气", visible: true, duration: 0 }, onInit() { console.log("训练页面正在初始化..."); console.log("时长参数:" + this.durationSelected); console.log("节奏参数:" + this.rhythmSelected); if (this.durationSelected == 0) this.duration = 60; else if (this.durationSelected == 1) this.duration = 120; else if (this.durationSelected == 2) this.duration = 180; if (this.rhythmSelected == 0) rhythm = 6; else if (this.rhythmSelected == 1) rhythm = 4; else if (this.rhythmSelected == 2) rhythm = 2; times = this.duration / rhythm; }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); timerDuration = setInterval(this.onDurationTimeout, 1000); timerRhythm = setInterval(this.onRhythmTimeout, rhythm * 1000); }, onDestroy() { console.log("训练页面正在销毁..."); clearInterval(timerDuration); timerDuration = null; clearInterval(timerRhythm); timerRhythm = null; }, onClick() { router.replace({uri: "pages/index/index"}) }, onDurationTimeout() { this.duration--; if (this.duration == 0) { clearInterval(timerDuration); timerDuration = null; this.visible = false; } }, onRhythmTimeout() { times--; if (times == 0) { clearInterval(timerRhythm); timerRhythm = null; this.breath = "已完成"; } else if (this.breath == "吸气") this.breath = "呼气"; else if (this.breath == "呼气") this.breath = "吸气"; } }
在每次吸气和呼气的过程中,实时显示进度百分比。
<div class="container"> <text class="txtBreath">{{breath}}({{percent}}%)</text> <text class="txtDuration" show="{{visible}}"> 再坚持{{duration}}秒 </text> <input class="btn" type="button" value="重新开始" onclick="onClick" /> </div>
import router from "@system.router" var timerDuration = null; var timerRhythm = null; var timerPercent = null; var rhythm = 0; var times = 0; export default { data: { breath: "吸气", percent: 0, visible: true, duration: 0 }, onInit() { console.log("训练页面正在初始化..."); console.log("时长参数:" + this.durationSelected); console.log("节奏参数:" + this.rhythmSelected); if (this.durationSelected == 0) this.duration = 60; else if (this.durationSelected == 1) this.duration = 120; else if (this.durationSelected == 2) this.duration = 180; if (this.rhythmSelected == 0) rhythm = 6; else if (this.rhythmSelected == 1) rhythm = 4; else if (this.rhythmSelected == 2) rhythm = 2; times = this.duration / rhythm; }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); timerDuration = setInterval(this.onDurationTimeout, 1000); timerRhythm = setInterval(this.onRhythmTimeout, rhythm * 1000); timerPercent = setInterval(this.onPercentTimeout, rhythm / 100 * 1000); }, onDestroy() { console.log("训练页面正在销毁..."); clearInterval(timerDuration); timerDuration = null; clearInterval(timerRhythm); timerRhythm = null; clearInterval(timerPercent); timerPercent = null; }, onClick() { router.replace({uri: "pages/index/index"}) }, onDurationTimeout() { this.duration--; if (this.duration == 0) { clearInterval(timerDuration); timerDuration = null; this.visible = false; } }, onRhythmTimeout() { times--; if (times == 0) { clearInterval(timerRhythm); timerRhythm = null; clearInterval(timerPercent); timerPercent = null; this.breath = "已完成"; this.percent = 100; } else if (this.breath == "吸气") { this.breath = "呼气"; this.percent = 0; } else if (this.breath == "呼气") { this.breath = "吸气"; this.percent = 0; } }, onPercentTimeout() { this.percent++; } }
在训练页面上显示顺时针旋转的应用徽标,吸一口气旋转一周,呼一口气旋转一周。
<div class="container"> <image class="img" src="/common/logo.png" style="animation-duration: {{period}}; animation-iteration-count: {{cycles}};" /> <text class="txtBreath">{{breath}}({{percent}}%)</text> <text class="txtDuration" show="{{visible}}"> 再坚持{{duration}}秒 </text> <input class="btn" type="button" value="重新开始" onclick="onClick" /> </div>
.container { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .img { width: 208; height: 208; margin-bottom: 10px; animation-name: round; } @keyframes round { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .txtBreath { width: 454px; height:46px; font-size: 38px; text-align: center; margin-bottom: 10px; } .txtDuration { width: 400px; height:40px; font-size: 30px; text-align: center; } .btn { width: 300px; height: 50px; font-size: 38px; background-color: #000000; border-color: #000000; margin-top: 40px; }
import router from "@system.router" var timerDuration = null; var timerRhythm = null; var timerPercent = null; var rhythm = 0; var times = 0; export default { data: { period: "", cycles: 0, breath: "吸气", percent: 0, visible: true, duration: 0 }, onInit() { console.log("训练页面正在初始化..."); console.log("时长参数:" + this.durationSelected); console.log("节奏参数:" + this.rhythmSelected); if (this.durationSelected == 0) this.duration = 60; else if (this.durationSelected == 1) this.duration = 120; else if (this.durationSelected == 2) this.duration = 180; if (this.rhythmSelected == 0) rhythm = 6; else if (this.rhythmSelected == 1) rhythm = 4; else if (this.rhythmSelected == 2) rhythm = 2; times = this.duration / rhythm; this.period = rhythm + "s"; this.cycles = times; }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); timerDuration = setInterval(this.onDurationTimeout, 1000); timerRhythm = setInterval(this.onRhythmTimeout, rhythm * 1000); timerPercent = setInterval(this.onPercentTimeout, rhythm / 100 * 1000); }, onDestroy() { console.log("训练页面正在销毁..."); clearInterval(timerDuration); timerDuration = null; clearInterval(timerRhythm); timerRhythm = null; clearInterval(timerPercent); timerPercent = null; }, onClick() { router.replace({uri: "pages/index/index"}) }, onDurationTimeout() { this.duration--; if (this.duration == 0) { clearInterval(timerDuration); timerDuration = null; this.visible = false; } }, onRhythmTimeout() { times--; if (times == 0) { clearInterval(timerRhythm); timerRhythm = null; clearInterval(timerPercent); timerPercent = null; this.breath = "已完成"; this.percent = 100; } else if (this.breath == "吸气") { this.breath = "呼气"; this.percent = 0; } else if (this.breath == "呼气") { this.breath = "吸气"; this.percent = 0; } }, onPercentTimeout() { this.percent++; } }
添加倒计时页面,显示三行固定文本。按主页面中的按钮,跳转到倒计时页面。
<div class="container"> <text class="txt">请保持静止</text> <text class="txt">3秒后跟随训练指引</text> <text class="txt">进行吸气和呼气</text> </div>
.container { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .txt { width: 454px; height: 50px; font-size: 38px; text-align: center; margin-top: 10px; }
export default { data: { } }
import router from "@system.router" export default { data: { durationRange: ["1", "2", "3"], durationSelected: 1, rhythmRange: ["较慢", "舒缓", "较快"], rhythmSelected: 1 }, onInit() { console.log("主页面正在初始化..."); }, onReady() { console.log("主页面就绪..."); }, onShow() { console.log("主页面正在显示..."); }, onDestroy() { console.log("主页面正在销毁..."); }, onDurationChange(pv) { console.log("时长改变:" + pv.newSelected); this.durationSelected = pv.newSelected; }, onRhythmChange(pv) { console.log("节奏改变:" + pv.newSelected); this.rhythmSelected = pv.newSelected; }, onClick() { router.replace({uri: "pages/countdown/countdown", params: { "durationSelected": this.durationSelected, "rhythmSelected": this.rhythmSelected}}); } }
一进入倒计时页面即开始倒计时,实时显示剩余的秒数,逐秒递减,减到零为止,“0”不显示。
<div class="container"> <image class="img" src="/common/countdown_{{seconds}}.png" /> <text class="txt">请保持静止</text> <text class="txt">{{seconds}}秒后跟随训练指引</text> <text class="txt">进行吸气和呼气</text> </div>
.container { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .img { width: 100px; height: 100px; margin-bottom: 30px; } .txt { width: 454px; height: 50px; font-size: 38px; text-align: center; margin-top: 10px; }
var timer = null; export default { data: { seconds: 3 }, onShow() { timer = setInterval(this.onTimeout, 1000); }, onTimeout() { this.seconds--; if (this.seconds == 0) { clearInterval(timer); timer = null; this.seconds = ""; } } }
倒计时结束后,从倒计时页面自动跳转到训练页面,同时传递主页面中两个选择器的值。
import router from "@system.router"; var timer = null; export default { data: { seconds: 3 }, onShow() { timer = setInterval(this.onTimeout, 1000); }, onTimeout() { this.seconds--; if (this.seconds == 0) { clearInterval(timer); timer = null; this.seconds = ""; router.replace({uri: "pages/training/training", params: { "durationSelected": this.durationSelected, "rhythmSelected": this.rhythmSelected}}); } } }
添加情绪页面
<div class="container" onswipe="onSwipe"> <text class="title">第1个训练报告页面</text> </div>
import router from "@system.router"; export default { onSwipe(e) { if (e.direction == "left") router.replace({uri: "pages/index/index"}); } }
<div class="container" onswipe="onSwipe"> <image class="img" src="/common/logo.png" style="animation-duration: {{period}}; animation-iteration-count: {{cycles}};" /> <text class="txtBreath">{{breath}}({{percent}}%)</text> <text class="txtDuration" if="{{visible}}"> 再坚持{{duration}}秒 </text> <text class="txtReport" else>右滑查看训练报告</text> <input class="btn" type="button" value="重新开始" onclick="onClick" /> </div>
.container { width: 454px; height: 454px; flex-direction: column; justify-content: center; align-items: center; } .img { width: 208; height: 208; margin-bottom: 10px; animation-name: round; } @keyframes round { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .txtBreath { width: 454px; height:46px; font-size: 38px; text-align: center; margin-bottom: 10px; } .txtDuration { width: 400px; height:40px; font-size: 30px; text-align: center; } .txtReport { width: 400px; height:40px; font-size: 30px; text-align: center; color: #ffa500; } .btn { width: 300px; height: 50px; font-size: 38px; background-color: #000000; border-color: #000000; margin-top: 40px; }
import router from "@system.router" var timerDuration = null; var timerRhythm = null; var timerPercent = null; var rhythm = 0; var times = 0; export default { data: { period: "", cycles: 0, breath: "吸气", percent: 0, visible: true, duration: 0 }, onInit() { console.log("训练页面正在初始化..."); console.log("时长参数:" + this.durationSelected); console.log("节奏参数:" + this.rhythmSelected); if (this.durationSelected == 0) this.duration = 60; else if (this.durationSelected == 1) this.duration = 120; else if (this.durationSelected == 2) this.duration = 180; if (this.rhythmSelected == 0) rhythm = 6; else if (this.rhythmSelected == 1) rhythm = 4; else if (this.rhythmSelected == 2) rhythm = 2; times = this.duration / rhythm; this.period = rhythm + "s"; this.cycles = times; }, onReady() { console.log("训练页面就绪..."); }, onShow() { console.log("训练页面正在显示..."); timerDuration = setInterval(this.onDurationTimeout, 1000); timerRhythm = setInterval(this.onRhythmTimeout, rhythm * 1000); timerPercent = setInterval(this.onPercentTimeout, rhythm / 100 * 1000); }, onDestroy() { console.log("训练页面正在销毁..."); clearInterval(timerDuration); timerDuration = null; clearInterval(timerRhythm); timerRhythm = null; clearInterval(timerPercent); timerPercent = null; }, onSwipe(e) { if (e.direction == "right") router.replace({uri: "pages/emotion/emotion"}); }, onClick() { router.replace({uri: "pages/index/index"}) }, onDurationTimeout() { this.duration--; if (this.duration == 0) { clearInterval(timerDuration); timerDuration = null; this.visible = false; } }, onRhythmTimeout() { times--; if (times == 0) { clearInterval(timerRhythm); timerRhythm = null; clearInterval(timerPercent); timerPercent = null; this.breath = "已完成"; this.percent = 100; } else if (this.breath == "吸气") { this.breath = "呼气"; this.percent = 0; } else if (this.breath == "呼气") { this.breath = "吸气"; this.percent = 0; } }, onPercentTimeout() { this.percent++; } }
将情绪页面的标题文本修改为“情绪”。
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="title">情绪</text> </div> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .title { font-size: 38px; margin-top: 40px; }
在情绪页面上显示情绪列表,列表中包含四种情绪的指数范围
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">情绪</text> </div> <list class="lst"> <list-item class="item" for="{{emotions}}"> <div class="divEmotion"> <text class="txtEmotion">{{$item}}</text> </div> </list-item> </list> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .lst { width: 320px; height: 220px; } .item { width: 320px; height: 55px; } .divEmotion { width: 320px; height: 50px; justify-content: space-between; align-items: center; } .txtEmotion { font-size: 24px; color: gray; }
import router from "@system.router"; export default { data: { emotions: [ "焦虑 80-99", "紧张 60-79", "正常 30-59", "放松 01-29"] }, onSwipe(e) { if (e.direction == "left") router.replace({uri: "pages/index/index"}); } }
在情绪列表中,显示每种情绪出现的百分比。
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">情绪</text> </div> <list class="lst"> <list-item class="item" for="{{emotions}}"> <div class="divEmotion"> <text class="txtEmotion">{{$item.label}}</text> <text class="txtEmotion">{{$item.ratio}}%</text> </div> </list-item> </list> </div>
import router from "@system.router"; export default { data: { emotions: [ {label: "焦虑 80-99", ratio: 0}, {label: "紧张 60-79", ratio: 0}, {label: "正常 30-59", ratio: 0}, {label: "放松 01-29", ratio: 0}] }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, ratios(emotions) { let anxiety = 0; let tension = 0; let normal = 0; let relaxed = 0; for (let i = 0; i < emotions.length; i++) if (80 <= emotions[i] && emotions[i] <= 99) anxiety++; else if (60 <= emotions[i] && emotions[i] <= 79) tension++; else if (30 <= emotions[i] && emotions[i] <= 59) normal++; else relaxed++; this.emotions[0].ratio = Math.round( anxiety / emotions.length * 100); this.emotions[1].ratio = Math.round( tension / emotions.length * 100); this.emotions[2].ratio = Math.round( normal / emotions.length * 100); this.emotions[3].ratio = Math.round( relaxed / emotions.length * 100); }, onInit() { let emotions = []; for (let i = 0; i < 48; i++) emotions.push(this.rand(1, 99)); this.ratios(emotions); }, onSwipe(e) { if (e.direction == "left") router.replace({uri: "pages/index/index"}); } }
在情绪列表中,用不同颜色的进度条表示每种情绪出现的百分比
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">情绪</text> </div> <list class="lst"> <list-item class="item" for="{{emotions}}"> <div class="divEmotion"> <text class="txtEmotion">{{$item.label}}</text> <text class="txtEmotion">{{$item.ratio}}%</text> </div> <progress class="prg" percent="{{$item.ratio}}" style="color: {{$item.color}}" /> </list-item> </list> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .lst { width: 320px; height: 220px; } .item { width: 320px; height: 55px; flex-direction: column; } .divEmotion { width: 320px; height: 50px; justify-content: space-between; align-items: center; } .txtEmotion { font-size: 24px; color: gray; } .prg { width: 320px; height: 5px; }
import router from "@system.router"; export default { data: { emotions: [ {label: "焦虑 80-99", ratio: 0, color: "#ffa500"}, {label: "紧张 60-79", ratio: 0, color: "#ffff00"}, {label: "正常 30-59", ratio: 0, color: "#00ffff"}, {label: "放松 01-29", ratio: 0, color: "#4169e1"}] }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, ratios(emotions) { let anxiety = 0; let tension = 0; let normal = 0; let relaxed = 0; for (let i = 0; i < emotions.length; i++) if (80 <= emotions[i] && emotions[i] <= 99) anxiety++; else if (60 <= emotions[i] && emotions[i] <= 79) tension++; else if (30 <= emotions[i] && emotions[i] <= 59) normal++; else relaxed++; this.emotions[0].ratio = Math.round( anxiety / emotions.length * 100); this.emotions[1].ratio = Math.round( tension / emotions.length * 100); this.emotions[2].ratio = Math.round( normal / emotions.length * 100); this.emotions[3].ratio = Math.round( relaxed / emotions.length * 100); }, onInit() { let emotions = []; for (let i = 0; i < 48; i++) emotions.push(this.rand(1, 99)); this.ratios(emotions); }, onSwipe(e) { if (e.direction == "left") router.replace({uri: "pages/index/index"}); } }
添加心率页面
<div class="container" onswipe="onSwipe"> <text class="title">第2个训练报告页面</text> </div>
import router from "@system.router"; export default { onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/emotion/emotion" }); break; } } }
import router from "@system.router"; export default { data: { emotions: [ {label: "焦虑 80-99", ratio: 0, color: "#ffa500"}, {label: "紧张 60-79", ratio: 0, color: "#ffff00"}, {label: "正常 30-59", ratio: 0, color: "#00ffff"}, {label: "放松 01-29", ratio: 0, color: "#4169e1"}] }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, ratios(emotions) { let anxiety = 0; let tension = 0; let normal = 0; let relaxed = 0; for (let i = 0; i < emotions.length; i++) if (80 <= emotions[i] && emotions[i] <= 99) anxiety++; else if (60 <= emotions[i] && emotions[i] <= 79) tension++; else if (30 <= emotions[i] && emotions[i] <= 59) normal++; else relaxed++; this.emotions[0].ratio = Math.round( anxiety / emotions.length * 100); this.emotions[1].ratio = Math.round( tension / emotions.length * 100); this.emotions[2].ratio = Math.round( normal / emotions.length * 100); this.emotions[3].ratio = Math.round( relaxed / emotions.length * 100); }, onInit() { let emotions = []; for (let i = 0; i < 48; i++) emotions.push(this.rand(1, 99)); this.ratios(emotions); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "top": router.replace({ uri: "pages/heartrate/heartrate" }); break; } } }
将心率页面的标题文本修改为“心率”,同时显示心率的最大值、最小值和平均值。
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">心率</text> </div> <div class="divChart"></div> <list class="lst"> <list-item class="item" for="{{maxmin}}"> <image class="icon" src="/common/heartrate_{{$item.icon}}.png" /> <text class="txtMaxmin">{{$item.value}}</text> </list-item> </list> <div class="divAverage"> <text class="txtLabel">平均</text> <text class="txtAverage">{{average}}</text> <text class="txtLabel">次/分</text> </div> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .divChart { width: 400px; height: 180px; } .lst { width: 200px; height: 45px; flex-direction: row; } .item { width: 100px; height: 45px; justify-content: center; align-items: center; } .icon { width: 32px; height: 32px; } .txtMaxmin { width: 48px; font-size: 24px; letter-spacing: 0px; } .divAverage { width: 220px; height: 55px; justify-content: space-between; align-items: center; } .txtLabel { font-size: 24px; color: gray; } .txtAverage { font-size: 38px; letter-spacing: 0px; }
import router from "@system.router"; export default { data: { maxmin: [ {icon: "max", value: 0}, {icon: "min", value: 0}], average: 0 }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, maxminavg(heartrates) { this.maxmin[0].value = Math.max.apply(null, heartrates); this.maxmin[1].value = Math.min.apply(null, heartrates); for (let i = 0; i < heartrates.length; i++) this.average += heartrates[i]; this.average = Math.round(this.average / heartrates.length); }, onInit() { let heartrates = []; for (let i = 0; i < 100; i++) heartrates.push(this.rand(73, 159)); this.maxminavg(heartrates); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/emotion/emotion" }); break; } } }
用曲线图的形式展现心率数据。
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">心率</text> </div> <chart class="cht" options="{{options}}" datasets="{{datasets}}" /> <list class="lst"> <list-item class="item" for="{{maxmin}}"> <image class="icon" src="/common/heartrate_{{$item.icon}}.png" /> <text class="txtMaxmin">{{$item.value}}</text> </list-item> </list> <div class="divAverage"> <text class="txtLabel">平均</text> <text class="txtAverage">{{average}}</text> <text class="txtLabel">次/分</text> </div> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .cht { width: 400px; height: 180px; } .lst { width: 200px; height: 45px; flex-direction: row; } .item { width: 100px; height: 45px; justify-content: center; align-items: center; } .icon { width: 32px; height: 32px; } .txtMaxmin { width: 48px; font-size: 24px; letter-spacing: 0px; } .divAverage { width: 220px; height: 55px; justify-content: space-between; align-items: center; } .txtLabel { font-size: 24px; color: gray; } .txtAverage { font-size: 38px; letter-spacing: 0px; }
import router from "@system.router"; export default { data: { options: {xAxis: {}, yAxis: {max: 160}}, datasets: [{data: [], gradient: true}], maxmin: [ {icon: "max", value: 0}, {icon: "min", value: 0}], average: 0 }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, maxminavg() { this.maxmin[0].value = Math.max.apply(null, this.datasets[0].data); this.maxmin[1].value = Math.min.apply(null, this.datasets[0].data); for (let i = 0; i < this.datasets[0].data.length; i++) this.average += this.datasets[0].data[i]; this.average = Math.round(this.average / this.datasets[0].data.length); }, onInit() { for (let i = 0; i < 100; i++) this.datasets[0].data.push(this.rand(73, 159)); this.maxminavg(); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/emotion/emotion" }); break; } } }
添加活动页面
<div class="container" onswipe="onSwipe"> <text class="title">第3个训练报告页面</text> </div>
import router from "@system.router"; export default { onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/heartrate/heartrate" }); break; } } }
import router from "@system.router"; export default { data: { options: {xAxis: {}, yAxis: {max: 160}}, datasets: [{data: [], gradient: true}], maxmin: [ {icon: "max", value: 0}, {icon: "min", value: 0}], average: 0 }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, maxminavg() { this.maxmin[0].value = Math.max.apply(null, this.datasets[0].data); this.maxmin[1].value = Math.min.apply(null, this.datasets[0].data); for (let i = 0; i < this.datasets[0].data.length; i++) this.average += this.datasets[0].data[i]; this.average = Math.round(this.average / this.datasets[0].data.length); }, onInit() { for (let i = 0; i < 100; i++) this.datasets[0].data.push(this.rand(73, 159)); this.maxminavg(); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/emotion/emotion" }); break; case "top": router.replace({ uri: "pages/motion/motion" }); break; } } }
将活动页面的标题文本修改为“活动”,同时显示时间标签及动与静所占的比例。
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">活动</text> </div> <div class="divChart"></div> <div class="divTime"> <text class="txtTime" for="{{times}}">{{$item}}</text> </div> <list class="lst"> <list-item class="item", for="{{motions}}"> <image class="icon" src="/common/{{$item.icon}}.png" /> <text class="txtLabel">{{$item.label}}</text> <text class="txtRatio">{{$item.ratio}}%</text> </list-item> </list> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .divChart { width: 340px; height: 150px; } .divTime { width: 340px; height: 25px; justify-content: space-between; align-items: center; } .txtTime { font-size: 18px; letter-spacing: 0px; color: gray; } .lst { width: 196px; height: 110px; margin-top: 10px; } .item { width: 196px; height: 45px; justify-content: space-between; align-items: center; } .icon { width: 32px; height: 32px; } .txtLabel { width: 64px; font-size: 24px; letter-spacing: 0px; margin-left: 10px; } .txtRatio { width: 90px; font-size: 30px; letter-spacing: 0px; text-align: right; }
import router from "@system.router"; export default { data: { times: ["07:00", "12:00", "17:00", "22:00"], motions: [ {icon: "motion", label: "活动", ratio: 0}, {icon: "motionless", label: "静止", ratio: 0}] }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, ratios(motions) { for (let i = 0; i < motions.length; i++) this.motions[motions[i]].ratio++; this.motions[0].ratio = Math.round( this.motions[0].ratio / motions.length * 100); this.motions[1].ratio = Math.round( this.motions[1].ratio / motions.length * 100); }, onInit() { let motions = []; for (let i = 0; i < 20; i++) motions.push(this.rand(0, 1)); this.ratios(motions); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/heartrate/heartrate" }); break; } } }
用条形图的形式展现活动数据。叠压两种颜色的条形图,分别表示活动和静止的时间分布。
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">活动</text> </div> <stack class="stk"> <chart class="cht" type="bar" options="{{options}}" datasets="{{dsMotion}}" /> <chart class="cht" type="bar" options="{{options}}" datasets="{{dsMotionless}}" /> </stack> <div class="divTime"> <text class="txtTime" for="{{times}}">{{$item}}</text> </div> <list class="lst"> <list-item class="item", for="{{motions}}"> <image class="icon" src="/common/{{$item.icon}}.png" /> <text class="txtLabel">{{$item.label}}</text> <text class="txtRatio">{{$item.ratio}}%</text> </list-item> </list> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .stk { width: 340px; height: 150px; } .cht { width: 340px; height: 150px; } .divTime { width: 340px; height: 25px; justify-content: space-between; align-items: center; } .txtTime { font-size: 18px; letter-spacing: 0px; color: gray; } .lst { width: 196px; height: 110px; margin-top: 10px; } .item { width: 196px; height: 45px; justify-content: space-between; align-items: center; } .icon { width: 32px; height: 32px; } .txtLabel { width: 64px; font-size: 24px; letter-spacing: 0px; margin-left: 10px; } .txtRatio { width: 90px; font-size: 30px; letter-spacing: 0px; text-align: right; }
import router from "@system.router"; export default { data: { options: {xAxis: {axisTick: 20}, yAxis: {max: 1}}, dsMotion: [{data: []}], dsMotionless: [{data: [], fillColor: "#696969"}], times: ["07:00", "12:00", "17:00", "22:00"], motions: [ {icon: "motion", label: "活动", ratio: 0}, {icon: "motionless", label: "静止", ratio: 0}] }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, ratios() { let nsamples = this.options.xAxis.axisTick; for (let i = 0; i < nsamples; i++) this.motions[this.dsMotionless[0].data[i]].ratio++; this.motions[0].ratio = Math.round( this.motions[0].ratio / nsamples * 100); this.motions[1].ratio = Math.round( this.motions[1].ratio / nsamples * 100); }, onInit() { for (let i = 0; i < this.options.xAxis.axisTick; i++) { let r = this.rand(0, 1); this.dsMotion[0].data.push(1 - r); this.dsMotionless[0].data.push(r); } this.ratios(); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/heartrate/heartrate" }); break; } } }
添加压力页面
<div class="container" onswipe="onSwipe"> <text class="title">第4个训练报告页面</text> </div>
import router from "@system.router"; export default { onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/motion/motion" }); break; } } }
import router from "@system.router"; export default { data: { options: {xAxis: {axisTick: 20}, yAxis: {max: 1}}, dsMotion: [{data: []}], dsMotionless: [{data: [], fillColor: "#696969"}], times: ["07:00", "12:00", "17:00", "22:00"], motions: [ {icon: "motion", label: "活动", ratio: 0}, {icon: "motionless", label: "静止", ratio: 0}] }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, ratios() { let nsamples = this.options.xAxis.axisTick; for (let i = 0; i < nsamples; i++) this.motions[this.dsMotionless[0].data[i]].ratio++; this.motions[0].ratio = Math.round( this.motions[0].ratio / nsamples * 100); this.motions[1].ratio = Math.round( this.motions[1].ratio / nsamples * 100); }, onInit() { for (let i = 0; i < this.options.xAxis.axisTick; i++) { let r = this.rand(0, 1); this.dsMotion[0].data.push(1 - r); this.dsMotionless[0].data.push(r); } this.ratios(); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/heartrate/heartrate" }); break; case "top": router.replace({ uri: "pages/pressure/pressure" }); break; } } }
将压力页面的标题文本修改为“压力”,同时显示时间标签及情绪指数的最大值和最小值。
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">压力</text> </div> <div class="divChart"></div> <div class="divTime"> <text class="txtTime" for="{{times}}">{{$item}}</text> </div> <list class="lst"> <list-item class="item" for="{{maxmin}}"> <image class="icon" src="/common/{{$item.icon}}.png" /> <text class="txtMaxmin">{{$item.value}}</text> </list-item> </list> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .divChart { width: 340px; height: 150px; } .divTime { width: 340px; height: 25px; justify-content: space-between; align-items: center; } .txtTime { font-size: 18px; letter-spacing: 0px; color: gray; } .lst { width: 200px; height: 45px; margin-top: 30px; flex-direction: row; } .item { width: 100px; height: 45px; justify-content: center; align-items: center; } .icon { width: 32px; height: 32px; } .txtMaxmin { width: 48px; font-size: 24px; letter-spacing: 0px; }
import router from "@system.router"; export default { data: { times: ["00:00", "06:00", "12:00", "18:00", "24:00"], maxmin: [ {icon: "", value: 0}, {icon: "", value: 0}], }, rank(emotion) { if (80 <= emotion && emotion <= 99) return "anxiety"; if (60 <= emotion && emotion <= 79) return "tension"; if (30 <= emotion && emotion <= 59) return "normal"; if ( 1 <= emotion && emotion <= 29) return "relaxed"; }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, distribute(emotions) { this.maxmin[0].value = Math.max.apply(null, emotions); this.maxmin[1].value = Math.min.apply(null, emotions); this.maxmin[0].icon = this.rank(this.maxmin[0].value) + "_max"; this.maxmin[1].icon = this.rank(this.maxmin[1].value) + "_min"; }, onInit() { let emotions = []; for (let i = 0; i < 48; i++) emotions.push(this.rand(1, 99)); this.distribute(emotions); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/motion/motion" }); break; } } }
用条形图的形式展现情绪数据。用四种颜色表示不同情绪的时间分布
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">压力</text> </div> <chart class="cht" type="bar" options="{{options}}" datasets="{{datasets}}" /> <div class="divTime"> <text class="txtTime" for="{{times}}">{{$item}}</text> </div> <list class="lst"> <list-item class="item" for="{{maxmin}}"> <image class="icon" src="/common/{{$item.icon}}.png" /> <text class="txtMaxmin">{{$item.value}}</text> </list-item> </list> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .cht { width: 340px; height: 150px; } .divTime { width: 340px; height: 25px; justify-content: space-between; align-items: center; } .txtTime { font-size: 18px; letter-spacing: 0px; color: gray; } .lst { width: 200px; height: 45px; margin-top: 30px; flex-direction: row; } .item { width: 100px; height: 45px; justify-content: center; align-items: center; } .icon { width: 32px; height: 32px; } .txtMaxmin { width: 48px; font-size: 24px; letter-spacing: 0px; }
import router from "@system.router"; export default { data: { options: {xAxis: {axisTick: 1}, yAxis: {max: 100}}, datasets: [], times: ["00:00", "06:00", "12:00", "18:00", "24:00"], maxmin: [ {icon: "", value: 0}, {icon: "", value: 0}], }, rank(emotion) { if (80 <= emotion && emotion <= 99) return "anxiety"; if (60 <= emotion && emotion <= 79) return "tension"; if (30 <= emotion && emotion <= 59) return "normal"; if ( 1 <= emotion && emotion <= 29) return "relaxed"; }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, distribute(emotions) { emotions.forEach (emotion => { let bar = {data: [emotion], fillColor: ""}; switch (this.rank(emotion)) { case "anxiety": bar.fillColor = "#ffa500"; break; case "tension": bar.fillColor = "#ffff00"; break; case "normal": bar.fillColor = "#00ffff"; break; case "relaxed": bar.fillColor = "#4169e1"; break; } this.datasets.push(bar); }); this.maxmin[0].value = Math.max.apply(null, emotions); this.maxmin[1].value = Math.min.apply(null, emotions); this.maxmin[0].icon = this.rank(this.maxmin[0].value) + "_max"; this.maxmin[1].icon = this.rank(this.maxmin[1].value) + "_min"; }, onInit() { let emotions = []; for (let i = 0; i < 48; i++) emotions.push(this.rand(1, 99)); this.distribute(emotions); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/motion/motion" }); break; } } }
添加最大摄氧量页面
<div class="container" onswipe="onSwipe"> <text class="title">第5个训练报告页面</text> </div>
import router from "@system.router"; export default { onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/pressure/pressure" }); break; } } }
import router from "@system.router"; export default { data: { options: {xAxis: {axisTick: 1}, yAxis: {max: 100}}, datasets: [], times: ["00:00", "06:00", "12:00", "18:00", "24:00"], maxmin: [ {icon: "", value: 0}, {icon: "", value: 0}], }, rank(emotion) { if (80 <= emotion && emotion <= 99) return "anxiety"; if (60 <= emotion && emotion <= 79) return "tension"; if (30 <= emotion && emotion <= 59) return "normal"; if ( 1 <= emotion && emotion <= 29) return "relaxed"; }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, distribute(emotions) { emotions.forEach (emotion => { let bar = {data: [emotion], fillColor: ""}; switch (this.rank(emotion)) { case "anxiety": bar.fillColor = "#ffa500"; break; case "tension": bar.fillColor = "#ffff00"; break; case "normal": bar.fillColor = "#00ffff"; break; case "relaxed": bar.fillColor = "#4169e1"; break; } this.datasets.push(bar); }); this.maxmin[0].value = Math.max.apply(null, emotions); this.maxmin[1].value = Math.min.apply(null, emotions); this.maxmin[0].icon = this.rank(this.maxmin[0].value) + "_max"; this.maxmin[1].icon = this.rank(this.maxmin[1].value) + "_min"; }, onInit() { let emotions = []; for (let i = 0; i < 48; i++) emotions.push(this.rand(1, 99)); this.distribute(emotions); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/motion/motion" }); break; case "top": router.replace({ uri: "pages/oxygen/oxygen" }); break; } } }
将最大摄氧量页面的标题文本修改为“最大摄氧量”,同时显示摄氧量值、量值单位和摄氧水平
1-10 | 11-20 | 21-30 | 31-40 | 41-50 | 51-60 | 61-70 |
---|---|---|---|---|---|---|
超低 | 低 | 较低 | 一般 | 高 | 优秀 | 卓越 |
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">最大摄氧量</text> </div> <div class="divImage"> <image class="img" src="/common/oxygen_{{ten}}.png" if="{{visible}}" /> <image class="img" src="/common/oxygen_{{one}}.png" /> </div> <text class="txtUnit">ml/kg/min</text> <text class="txtLevel">{{level}}水平</text> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .divImage { width: 160px; height: 180px; justify-content: center; align-items: center; } .img { width: 80px; height: 100px; } .txtUnit { width: 200px; height: 30px; font-size: 24px; text-align: center; color: gray; } .txtLevel { width: 200px; height: 40px; margin-top: 30px; text-align: center; }
import router from "@system.router"; export default { data: { ten: "", visible: false, one: "", level: "" }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, rank(oxygen) { let level = Math.floor((oxygen - 1) / 10); let levels = ["超低", "低", "较低", "一般", "高", "优秀", "卓越"]; this.level = levels[level]; }, onInit() { let oxygen = this.rand(1, 70); this.one = oxygen.toString(); if (this.one.length == 2) { this.ten = this.one[0]; this.visible = true; this.one = this.one[1]; } this.rank(oxygen); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/pressure/pressure" }); break; } } }
在最大摄氧量页面的外周显示七段等长的彩色圆弧,每段圆弧的弧心角均为40°。
颜色 | 起始角 | 终止角 |
---|---|---|
红色 | 220° | 260° |
橙色 | 260° | 300° |
土黄 | 300° | 340° |
黄色 | 340° | 20° |
绿色 | 20° | 60° |
青色 | 60° | 100° |
蓝色 | 100° | 140° |
<stack class="stk" onswipe="onSwipe"> <div class="divPage"> <div class="divTitle"> <text class="txtTitle">最大摄氧量</text> </div> <div class="divImage"> <image class="img" src="/common/oxygen_{{ten}}.png" if="{{visible}}" /> <image class="img" src="/common/oxygen_{{one}}.png" /> </div> <text class="txtUnit">ml/kg/min</text> <text class="txtLevel">{{level}}水平</text> </div> <progress class="prg" type="arc" for="{{progresses}}" percent="100" style="color: {{$item.color}}; start-angle: {{$item.start}};" /> </stack>
.stk { width: 454px; height: 454px; } .divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .divImage { width: 160px; height: 180px; justify-content: center; align-items: center; } .img { width: 80px; height: 100px; } .txtUnit { width: 200px; height: 30px; font-size: 24px; text-align: center; color: gray; } .txtLevel { width: 200px; height: 40px; margin-top: 30px; text-align: center; } .prg { width: 454px; height: 454px; center-x: 224px; center-y: 229px; radius: 220px; stroke-width: 18px; total-angle: 40deg; }
import router from "@system.router"; export default { data: { progresses: [ {color: "#ff0000", start: 220}, {color: "#ffa500", start: 260}, {color: "#ffd700", start: 300}, {color: "#ffff00", start: 340}, {color: "#adff2f", start: 20}, {color: "#00ffff", start: 60}, {color: "#4169e1", start: 100}], ten: "", visible: false, one: "", level: "" }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, rank(oxygen) { let level = Math.floor((oxygen - 1) / 10); let levels = ["超低", "低", "较低", "一般", "高", "优秀", "卓越"]; this.level = levels[level]; }, onInit() { let oxygen = this.rand(1, 70); this.one = oxygen.toString(); if (this.one.length == 2) { this.ten = this.one[0]; this.visible = true; this.one = this.one[1]; } this.rank(oxygen); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/pressure/pressure" }); break; } } }
将摄氧圆弧的彩色终点设置为最大摄氧量占其峰值(70)的百分比。
<stack class="stk" onswipe="onSwipe"> <div class="divPage"> <div class="divTitle"> <text class="txtTitle">最大摄氧量</text> </div> <div class="divImage"> <image class="img" src="/common/oxygen_{{ten}}.png" if="{{visible}}" /> <image class="img" src="/common/oxygen_{{one}}.png" /> </div> <text class="txtUnit">ml/kg/min</text> <text class="txtLevel">{{level}}水平</text> </div> <progress class="prg" type="arc" for="{{progresses}}" percent="{{$item.percent}}" style="background-color: {{$item.bgcolor}}; color: {{$item.color}}; start-angle: {{$item.start}};" /> </stack>
import router from "@system.router"; export default { data: { progresses: [ {percent: 0, bgcolor: "#202020", color: "#ff0000", start: 220}, {percent: 0, bgcolor: "#202020", color: "#ffa500", start: 260}, {percent: 0, bgcolor: "#202020", color: "#ffd700", start: 300}, {percent: 0, bgcolor: "#202020", color: "#ffff00", start: 340}, {percent: 0, bgcolor: "#202020", color: "#adff2f", start: 20}, {percent: 0, bgcolor: "#202020", color: "#00ffff", start: 60}, {percent: 0, bgcolor: "#202020", color: "#4169e1", start: 100}], ten: "", visible: false, one: "", level: "" }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, rank(oxygen) { let level = Math.floor((oxygen - 1) / 10); for (let i = 0; i < this.progresses.length; i++) if (i < level) this.progresses[i].percent = 100; else if (i == level) this.progresses[i].percent = ((oxygen - 1) % 10 + 1) * 10; else this.progresses[i].color = this.progresses[i].bgcolor; let levels = ["超低", "低", "较低", "一般", "高", "优秀", "卓越"]; this.level = levels[level]; }, onInit() { let oxygen = this.rand(1, 70); this.one = oxygen.toString(); if (this.one.length == 2) { this.ten = this.one[0]; this.visible = true; this.one = this.one[1]; } this.rank(oxygen); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/pressure/pressure" }); break; } } }
添加联系方式页面
<div class="container" onswipe="onSwipe"> <text class="title">联系方式页面</text> </div>
import router from "@system.router"; export default { onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/oxygen/oxygen" }); break; } } }
import router from "@system.router"; export default { data: { progresses: [ {percent: 0, bgcolor: "#202020", color: "#ff0000", start: 220}, {percent: 0, bgcolor: "#202020", color: "#ffa500", start: 260}, {percent: 0, bgcolor: "#202020", color: "#ffd700", start: 300}, {percent: 0, bgcolor: "#202020", color: "#ffff00", start: 340}, {percent: 0, bgcolor: "#202020", color: "#adff2f", start: 20}, {percent: 0, bgcolor: "#202020", color: "#00ffff", start: 60}, {percent: 0, bgcolor: "#202020", color: "#4169e1", start: 100}], ten: "", visible: false, one: "", level: "" }, rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, rank(oxygen) { let level = Math.floor((oxygen - 1) / 10); for (let i = 0; i < this.progresses.length; i++) if (i < level) this.progresses[i].percent = 100; else if (i == level) this.progresses[i].percent = ((oxygen - 1) % 10 + 1) * 10; else this.progresses[i].color = this.progresses[i].bgcolor; let levels = ["超低", "低", "较低", "一般", "高", "优秀", "卓越"]; this.level = levels[level]; }, onInit() { let oxygen = this.rand(1, 70); this.one = oxygen.toString(); if (this.one.length == 2) { this.ten = this.one[0]; this.visible = true; this.one = this.one[1]; } this.rank(oxygen); }, onSwipe(e) { switch (e.direction) { case "left": router.replace({ uri: "pages/index/index" }); break; case "bottom": router.replace({ uri: "pages/pressure/pressure" }); break; case "top": router.replace({ uri: "pages/contact/contact" }); break; } } }
将联系方式页面的标题文本修改为“联系我们”,同时显示二维码图和作者署名。
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">联系我们</text> </div> <div class="divQrcode"> <image class="img" src="/common/qrcode.png" /> </div> <text class="txtAuthor">达内集团C++教学部</text> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .divQrcode { width: 180px; height: 220px; justify-content: center; align-items: center; } .img { width: 180px; height: 180px; } .txtAuthor { width: 300px; height: 50px; font-size: 24px; margin-top: 20px; text-align: center; }
在情绪页面的底部增加两行提示文本
<div class="divPage" onswipe="onSwipe"> <div class="divTitle"> <text class="txtTitle">情绪</text> </div> <list class="lst"> <list-item class="item" for="{{emotions}}"> <div class="divEmotion"> <text class="txtEmotion">{{$item.label}}</text> <text class="txtEmotion">{{$item.ratio}}%</text> </div> <progress class="prg" percent="{{$item.ratio}}" style="color: {{$item.color}}" /> </list-item> </list> <div class="divTip"> <text class="txtTip">【上下滑动】切换报告页面</text> <text class="txtTip">【向左滑动】返回到主页面</text> </div> </div>
.divPage { width: 454px; height: 454px; flex-direction: column; justify-content: flex-start; align-items: center; } .divTitle { width: 300px; height: 130px; justify-content: center; align-items: center; } .txtTitle { font-size: 38px; margin-top: 40px; } .lst { width: 320px; height: 220px; } .item { width: 320px; height: 55px; flex-direction: column; } .divEmotion { width: 320px; height: 50px; justify-content: space-between; align-items: center; } .txtEmotion { font-size: 24px; color: gray; } .prg { width: 320px; height: 5px; } .divTip { width: 400px; height: 70px; flex-direction: column; justify-content: flex-end; align-items: center; } .txtTip { font-size: 18px; color: #ffa500; }
在这个项目中,我们实现了一款面向华为Lite Wearable设备的应用——呼吸训练。华为鸿蒙操作系统(HarmonyOS)目前支持的设备类型包括Lite Wearable、Wearable、Phone、Tablet和TV等至少五种。它们的应用开发语言和从开发环境中所获得的支持不尽相同。
设备类型 | 华为产品 | 开发语言 | 开发环境 | ||||
---|---|---|---|---|---|---|---|
JavaScript | Java | C/C++ | 预览器 | 模拟器 | 调试器 | ||
Lite Wearable | 智能手表 | 可用 | 不可用 | 不可用 | 支持 | 支持 | 支持 |
Wearable | 智能手表 | 可用 | 可用 | 不可用 | 支持 | 不支持 | 不支持 |
TV | 智慧屏 | 可用 | 可用 | 不可用 | 支持 | 不支持 | 不支持 |
从上表可以看出,目前华为开发环境对Lite Wearable设备的支持是最完善也是最成熟的。既可以在预览器中预览代码的运行效果,也可以在本机的模拟器中运行和调试代码,这给鸿蒙应用的开发人员带来了想当出色的使用体验。从上表中还可以看出,JavaScript语言是华为所有鸿蒙设备的通用编程语言,也是Lite Wearable设备唯一可用的编程语言。正是出于以上考虑,本实训案例面向Lite Wearable设备,使用JavaScript语言进行应用程序开发,具有足够的代表性和典型性。当然,随着华为针对鸿蒙产品更多开发工具包的发布,还会有更多面向不同设备,基于不同语言的实训案例提供给鸿蒙系统的学习者和开发者。