鸿蒙智能手表呼吸训练应用实训项目


1 项目概述

本实训案例旨在构建一款,可运行于鸿蒙智能手表设备上的,呼吸训练应用。借助该应用,用户可以跟随界面指引进行呼吸训练,并在训练后对包括情绪、心率、活动分布、压力分布以及最大摄氧量等在内的,健康数据进行查看。该应用的功能并非重点。通过本实训案例,任何对计算机编程稍有基础的学习者,都能够快速上手基于鸿蒙操作系统的应用开发,并对HUAWEI DevEco Studio开发环境、JavaScript编程语言,有一个初步的了解和掌握。

2 需求分析

本实训案例所要实现的呼吸训练应用由以下九个页面构成:

它们的跳转关系如下图所示:

graph TB index(主页面) countdown(倒计时页面) training(训练页面) emotion(情绪页面) heartrate(心率页面) motion(活动页面) pressure(压力页面) oxygen(最大摄氧量页面) contact(联系方式页面) index--开始-->countdown--3秒倒计时-->training--重新开始-->index training--右滑-->emotion--上滑-->heartrate--上滑-->motion--上滑-->pressure--上滑-->oxygen--上滑-->contact--下滑-->oxygen--下滑-->pressure--下滑-->motion--下滑-->heartrate--下滑-->emotion--左滑-->index heartrate--左滑-->index motion--左滑-->index pressure--左滑-->index oxygen--左滑-->index contact--左滑-->index

2.1 主页面

2.2 倒计时页面

2.3 训练页面

2.4 情绪页面

为了方便不同环境下的学习者开发测试本案例中的代码,在接下来几个报告页面中使用的数据,都是由程序随机生成的测试数据,而并非采集自真实的设备。相对于本实训案例所要达到的目标,这些数据的来源和真实性并不重要,重要的是让学习者掌握在鸿蒙系统中,对数据进行分析和可视化的方法。

2.5 心率页面

2.6 活动页面

2.7 压力页面

2.8 最大摄氧量页面

2.9 联系方式页面

3 开发步骤

3.1 你好鸿蒙

3.1.1 目标

用HUAWEI DevEco Studio创建一个面向Lite Wearable设备的应用,将页面中默认显示的“Hello World”改为“你好鸿蒙”。

3.1.2 知识

graph LR hml((hml)) css((css)) js((js)) page((页面)) hml--结构-->page css--样式-->page js--行为-->page

3.1.3 实现

3.1.3.1 index.hml
<div class="container">
    <text class="title">你好{{title}}</text>
</div>
3.1.3.2 index.js
export default {
    data: {
        title: "鸿蒙"
    }
}

3.1.4 效果

3.2 添加按钮

3.2.1 目标

在页面中添加一个按钮,在其被按下后打印日志。

3.2.2 知识

3.2.3 实现

3.2.3.1 index.hml
<div class="container">
    <text class="title">你好{{title}}</text>
    <input class="btn" type="button" value="按我" onclick="onClick" />
</div>
3.2.3.2 index.css
.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;
}
3.2.3.3 index.js
export default {
    data: {
        title: "鸿蒙"
    },
    onClick() {
        console.log("我被按了");
    }
}

3.2.4 效果

3.3 添加训练页面

3.3.1 目标

添加训练页面,其中包含一个按钮。按主页面中的按钮,跳转到训练页面。按训练页面中的按钮,返回到主页面。

3.3.2 知识

3.3.3 实现

3.3.3.1 training.hml
<div class="container">
    <text class="title">训练页面</text>
    <input class="btn" type="button" value="返回" onclick="onClick" />
</div>
3.3.3.2 training.css
.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;
}
3.3.3.3 training.js
import router from "@system.router"

export default {
    onClick() {
        router.replace({uri: "pages/index/index"})
    }
}
3.3.3.4 index.js
import router from "@system.router"

export default {
    data: {
        title: "鸿蒙"
    },
    onClick() {
        router.replace({uri: "pages/training/training"})
    }
}

3.3.4 效果

3.4 生命周期

3.4.1 目标

在应用被创建和销毁时,在每个页面被初始化、就绪、显示和销毁时打印日志。

3.4.2 知识

graph TB subgraph 应用 subgraph 页面1 oninit1(onInit) onready1(onReady) onshow1(onShow) ondestroy1(onDestroy) oninit1-->onready1-->onshow1-->ondestroy1 end subgraph 页面2 oninit2(onInit) onready2(onReady) onshow2(onShow) ondestroy2(onDestroy) oninit2-->onready2-->onshow2-->ondestroy2 end oncreate(onCreate) ondestroy(onDestroy) oncreate-->oninit1 ondestroy1-->oninit2 ondestroy2-->ondestroy end

3.4.3 实现

3.4.3.1 app.js
export default {
    onCreate() {
        console.log("应用正在创建...");
    },
    onDestroy() {
        console.log("应用正在销毁...");
    }
};
3.4.3.2 index.js
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"})
    }
}
3.4.3.3 training.js
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"})
    }
}

3.4.4 效果

3.5 在主页面上显示应用徽标和选择器

3.5.1 目标

在主页面的中心位置显示应用徽标,并在其左右两侧添加两个选择器,其中:

3.5.2 知识

3.5.3 实现

3.5.3.1 index.hml
<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>
3.5.3.2 index.css
.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;
}
3.5.3.3 index.js
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"})
    }
}

3.5.4 效果

3.6 选择器的值

3.6.1 目标

将主页面中左右两个选择器的默认选项分别设为2分和舒缓,并在选项改变时打印其值。

3.6.2 知识

3.6.3 实现

3.6.3.1 index.hml
<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>
3.6.3.2 index.js
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"})
    }
}

3.6.4 效果

3.7 将主页面选择器的值传给训练页面

3.7.1 目标

按下按钮从主页面跳转到训练页面,同时将两个选择器的值也传给训练页面,并在训练页面被初始化时打印这两个值。

3.7.2 知识

3.7.3 实现

3.7.3.1 index.hml
<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>
3.7.3.2 index.js
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}});
    }
}
3.7.3.3 training.js
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"})
    }
}

3.7.4 效果

3.8 页面美化

3.8.1 目标

将主页面和训练页面中按钮上的文本分别改为“开始”和“重新开始”,字体调大,黑色背景。增加训练页面中文本和按钮的间距。

3.8.2 知识

3.8.3 实现

3.8.3.1 index.css
.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;
}
3.8.3.2 training.css
.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;
}

3.8.4 效果

3.9 在训练页面上显示训练时长

3.9.1 目标

在训练页面上根据从主页面传入的,训练时长选择器的值,显示训练需要持续的秒数。

训练时长选择器的值 训练时长选择器文本 训练需要持续的秒数
0 1 60
1 2 120
2 3 180

3.9.2 知识

3.9.3 实现

3.9.3.1 training.hml
<div class="container">
    <text class="title">总共需要坚持{{duration}}秒</text>
    <input class="btn" type="button" value="重新开始" onclick="onClick" />
</div>
3.9.3.2 training.js
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"})
    }
}

3.9.4 效果

3.10 在训练页面上显示剩余秒数

3.10.1 目标

一进入训练页面即开始计时,显示本次训练剩余的秒数,逐秒递减,减到零为止。

3.10.2 知识

3.10.3 实现

3.10.3.1 training.hml
<div class="container">
    <text class="title">再坚持{{duration}}秒</text>
    <input class="btn" type="button" value="重新开始" onclick="onClick" />
</div>
3.10.3.2 training.js
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;
        }
    }
}

3.10.4 效果

3.11 在训练完成时隐藏剩余秒数

3.11.1 目标

当本次训练的剩余秒数减到零时,隐藏剩余秒数文本,即不显示“再坚持0秒”。

3.11.2 知识

3.11.3 实现

3.11.3.1 training.hml
<div class="container">
    <text class="title" show="{{visible}}">再坚持{{duration}}秒</text>
    <input class="btn" type="button" value="重新开始" onclick="onClick" />
</div>
3.11.3.2 training.js
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;
        }
    }
}

3.11.4 效果

3.12 在训练页面上根据呼吸节奏交替显示“吸气”和“呼气”

3.12.1 目标

在训练过程中交替显示“吸气”和“呼气”,每次呼吸持续的秒数,由从主页面传入的,呼吸节奏选择器的值决定。最后显示“已完成”。

呼吸节奏选择器的值 呼吸节奏选择器文本 每次呼吸持续的秒数
0 较慢 6
1 舒缓 4
2 较快 2

3.12.2 知识

3.12.3 实现

3.12.3.1 training.hml
<div class="container">
    <text class="txtBreath">{{breath}}</text>
    <text class="txtDuration" show="{{visible}}">
        再坚持{{duration}}秒
    </text>
    <input class="btn" type="button" value="重新开始" onclick="onClick" />
</div>
3.12.3.2 training.css
.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;
}
3.12.3.3 training.js
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 = "吸气";
    }
}

3.12.4 效果

3.13 在训练页面上显示每次吸气和呼吸的进度百分比

3.13.1 目标

在每次吸气和呼气的过程中,实时显示进度百分比。

3.13.2 知识

3.13.3 实现

3.13.3.1 training.hml
<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>
3.13.3.2 training.js
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++;
    }
}

3.13.4 效果

3.14 在训练页面上显示随同呼吸节奏旋转的应用徽标

3.14.1 目标

在训练页面上显示顺时针旋转的应用徽标,吸一口气旋转一周,呼一口气旋转一周。

3.14.2 知识

3.14.3 实现

3.14.3.1 training.hml
<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>
3.14.3.2 training.css
.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;
}
3.14.3.3 training.js
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++;
    }
}

3.14.4 效果

3.15 添加倒计时页面

3.15.1 目标

添加倒计时页面,显示三行固定文本。按主页面中的按钮,跳转到倒计时页面。

3.15.2 知识

3.15.3 实现

3.15.3.1 countdown.hml
<div class="container">
    <text class="txt">请保持静止</text>
    <text class="txt">3秒后跟随训练指引</text>
    <text class="txt">进行吸气和呼气</text>
</div>
3.15.3.2 countdown.css
.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;
}
3.15.3.3 countdown.js
export default {
    data: {
    }
}
3.15.3.4 index.js
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}});
    }
}

3.15.4 效果

3.16 在倒计时页面上显示剩余秒数

3.16.1 目标

一进入倒计时页面即开始倒计时,实时显示剩余的秒数,逐秒递减,减到零为止,“0”不显示。

3.16.2 知识

3.16.3 实现

3.16.3.1 countdown.hml
<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>
3.16.3.2 countdown.css
.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;
}
3.16.3.3 countdown.js
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 = "";
        }
    }
}

3.16.4 效果

3.17 倒计时页面在计时到期后跳转到训练页面

3.17.1 目标

倒计时结束后,从倒计时页面自动跳转到训练页面,同时传递主页面中两个选择器的值。

3.17.2 知识

3.17.3 实现

3.17.3.1 countdown.js
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}});
        }
    }
}

3.17.4 效果

3.18 添加情绪页面

3.18.1 目标

添加情绪页面

graph LR index((主页面)) training((训练页面)) emotion((情绪页面)) emotion---training emotion---index

3.18.2 知识

3.18.3 实现

3.18.3.1 emotion.hml
<div class="container" onswipe="onSwipe">
    <text class="title">第1个训练报告页面</text>
</div>
3.18.3.2 emotion.js
import router from "@system.router";

export default {
    onSwipe(e) {
        if (e.direction == "left")
            router.replace({uri: "pages/index/index"});
    }
}
3.18.3.3 training.hml
<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>
3.18.3.4 training.css
.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;
}
3.18.3.5 training.js
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++;
    }
}

3.18.4 效果

3.19 在情绪页面上显示标题文本

3.19.1 目标

将情绪页面的标题文本修改为“情绪”。

3.19.2 知识

3.19.3 实现

3.19.3.1 emotion.hml
<div class="divPage" onswipe="onSwipe">
    <div class="divTitle">
        <text class="title">情绪</text>
    </div>
</div>
3.19.3.2 emotion.css
.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;
}

3.19.4 效果

3.20 在情绪页面上显示情绪列表

3.20.1 目标

在情绪页面上显示情绪列表,列表中包含四种情绪的指数范围

3.20.2 知识

3.20.3 实现

3.20.3.1 emotion.hml
<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>
3.20.3.2 emotion.css
.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;
}
3.20.3.3 emotion.js
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"});
    }
}

3.20.4 效果

3.21 在情绪列表中添加情绪比率

3.21.1 目标

在情绪列表中,显示每种情绪出现的百分比。

3.21.2 知识

3.21.3 实现

3.21.3.1 emotion.hml
<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>
3.21.3.2 emotion.js
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"});
    }
}

3.21.4 效果

3.22 根据各情绪比率显示进度条

3.22.1 目标

在情绪列表中,用不同颜色的进度条表示每种情绪出现的百分比

3.22.2 知识

3.22.3 实现

3.22.3.1 emotion.hml
<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>
3.22.3.2 emotion.css
.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;
}
3.22.3.3 emotion.js
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"});
    }
}

3.22.4 效果

3.23 添加心率页面

3.23.1 目标

添加心率页面

graph LR index((主页面)) emotion((情绪页面)) heartrate((心率页面)) emotion---index heartrate---index

3.23.2 知识

3.23.3 实现

3.23.3.1 heartrate.hml
<div class="container" onswipe="onSwipe">
    <text class="title">第2个训练报告页面</text>
</div>
3.23.3.2 heartrate.js
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;
        }
    }
}
3.23.3.3 emotion.js
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;
        }
    }
}

3.23.4 效果

3.24 在心率页面上显示标题文本、心率峰值和心率均值

3.24.1 目标

将心率页面的标题文本修改为“心率”,同时显示心率的最大值、最小值和平均值。

3.24.2 知识

3.24.3 实现

3.24.3.1 heartrate.hml
<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>
3.24.3.2 heartrate.css
.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;
}
3.24.3.3 heartrate.js
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;
        }
    }
}

3.24.4 效果

3.25 在心率页面上显示心率曲线图

3.25.1 目标

用曲线图的形式展现心率数据。

3.25.2 知识

3.25.3 实现

3.25.3.1 heartrate.hml
<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>
3.25.3.2 heartrate.css
.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;
}
3.25.3.3 heartrate.js
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;
        }
    }
}

3.25.4 效果

3.26 添加活动页面

3.26.1 目标

添加活动页面

graph LR index((主页面)) heartrate((心率页面)) motion((活动页面)) heartrate---index motion---index

3.26.2 知识

3.26.3 实现

3.26.3.1 motion.hml
<div class="container" onswipe="onSwipe">
    <text class="title">第3个训练报告页面</text>
</div>
3.26.3.2 motion.js
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;
        }
    }
}
3.26.3.3 heartrate.js
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;
        }
    }
}

3.26.4 效果

3.27 在活动页面上显示标题文本、时间标签和动静比率

3.27.1 目标

将活动页面的标题文本修改为“活动”,同时显示时间标签及动与静所占的比例。

3.27.2 知识

3.27.3 实现

3.27.3.1 motion.hml
<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>
3.27.3.2 motion.css
.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;
}
3.27.3.3 motion.js
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;
        }
    }
}

3.27.4 效果

3.28 在活动页面上显示动静分布条形图

3.28.1 目标

用条形图的形式展现活动数据。叠压两种颜色的条形图,分别表示活动和静止的时间分布。

3.28.2 知识

3.28.3 实现

3.28.3.1 motion.hml
<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>
3.28.3.2 motion.css
.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;
}
3.28.3.3 motion.js
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;
        }
    }
}

3.28.4 效果

3.29 添加压力页面

3.29.1 目标

添加压力页面

graph LR index((主页面)) motion((活动页面)) pressure((压力页面)) motion---index pressure---index

3.29.2 知识

3.29.3 实现

3.29.3.1 pressure.hml
<div class="container" onswipe="onSwipe">
    <text class="title">第4个训练报告页面</text>
</div>
3.29.3.2 pressure.js
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;
        }
    }
}
3.29.3.3 motion.js
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;
        }
    }
}

3.29.4 效果

3.30 在压力页面上显示标题文本、时间标签和情绪峰值

3.30.1 目标

将压力页面的标题文本修改为“压力”,同时显示时间标签及情绪指数的最大值和最小值。

3.30.2 知识

3.30.3 实现

3.30.3.1 pressure.hml
<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>
3.30.3.2 pressure.css
.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;
}
3.30.3.3 pressure.js
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;
        }
    }
}

3.30.4 效果

3.31 在压力页面上显示情绪分布条形图

3.31.1 目标

用条形图的形式展现情绪数据。用四种颜色表示不同情绪的时间分布

3.31.2 知识

3.31.3 实现

3.31.3.1 pressure.hml
<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>
3.31.3.2 pressure.css
.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;
}
3.31.3.3 pressure.js
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;
        }
    }
}

3.31.4 效果

3.32 添加最大摄氧量页面

3.32.1 目标

添加最大摄氧量页面

graph LR index((主页面)) pressure((压力页面)) oxygen((最大摄氧量页面)) pressure---index oxygen---index

3.32.2 知识

3.32.3 实现

3.32.3.1 oxygen.hml
<div class="container" onswipe="onSwipe">
    <text class="title">第5个训练报告页面</text>
</div>
3.32.3.2 oxygen.js
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;
        }
    }
}
3.32.3.3 pressure.js
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;
        }
    }
}

3.32.4 效果

3.33 在最大摄氧量页面上显示标题文本、摄氧量值、量值单位和摄氧水平

3.33.1 目标

将最大摄氧量页面的标题文本修改为“最大摄氧量”,同时显示摄氧量值、量值单位和摄氧水平

1-10 11-20 21-30 31-40 41-50 51-60 61-70
超低 较低 一般 优秀 卓越

3.33.2 知识

3.33.3 实现

3.33.3.1 oxygen.hml
<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>
3.33.3.2 oxygen.css
.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;
}
3.33.3.3 oxygen.js
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;
        }
    }
}

3.33.4 效果

3.34 在最大摄氧量页面上显示摄氧圆弧

3.34.1 目标

在最大摄氧量页面的外周显示七段等长的彩色圆弧,每段圆弧的弧心角均为40°。

颜色 起始角 终止角
红色 220° 260°
橙色 260° 300°
土黄 300° 340°
黄色 340° 20°
绿色 20° 60°
青色 60° 100°
蓝色 100° 140°

3.34.2 知识

3.34.3 实现

3.34.3.1 oxygen.hml
<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>
3.34.3.2 oxygen.css
.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;
}
3.34.3.3 oxygen.js
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;
        }
    }
}

3.34.4 效果

3.35 根据最大摄氧量调整摄氧圆弧终点

3.35.1 目标

将摄氧圆弧的彩色终点设置为最大摄氧量占其峰值(70)的百分比。

3.35.2 知识

3.35.3 实现

3.35.3.1 oxygen.hml
<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>
3.35.3.2 oxygen.js
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;
        }
    }
}

3.35.4 效果

3.36 添加联系方式页面

3.36.1 目标

添加联系方式页面

graph LR index((主页面)) oxygen((最大摄氧量页面)) contact((联系方式页面)) oxygen---index contact---index

3.36.2 知识

3.36.3 实现

3.36.3.1 contact.hml
<div class="container" onswipe="onSwipe">
    <text class="title">联系方式页面</text>
</div>
3.36.3.2 contact.js
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;
        }
    }
}
3.36.3.3 oxygen.js
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;
        }
    }
}

3.36.4 效果

3.37 在联系方式页面上显示标题文本、二维码图和作者署名

3.37.1 目标

将联系方式页面的标题文本修改为“联系我们”,同时显示二维码图和作者署名。

3.37.2 知识

3.37.3 实现

3.37.3.1 contact.hml
<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>
3.37.3.2 contact.css
.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;
}

3.37.4 效果

3.38 在情绪页面上显示提示文本

3.38.1 目标

在情绪页面的底部增加两行提示文本

3.38.2 知识

3.38.3 实现

3.38.3.1 emotion.hml
<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>
3.38.3.2 emotion.css
.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;
}

3.38.4 效果

4 项目总结

在这个项目中,我们实现了一款面向华为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语言进行应用程序开发,具有足够的代表性和典型性。当然,随着华为针对鸿蒙产品更多开发工具包的发布,还会有更多面向不同设备,基于不同语言的实训案例提供给鸿蒙系统的学习者和开发者。

更多精彩,敬请期待……


达内集团C++教学部 2021年6月26日