JS - BMI計算機

JS - BMI計算機

DEMO

目標

用JavaScript做一個BMI計算機,而且可以顯示過往的測驗結果。
這次一邊整理筆記一邊改寫,將當初很多段重複又累贅的地方重寫,心中很感慨,原來我也是有進步的。

預覽

首圖

結果頁

難點:

1.顯示結果的圓圈會根據結果數字的狀態變色
2.列表的顏色也要跟著變

思考流程:

1.先製作一個介面,使用者可以在相應的欄位輸入身高體重,然後按下結果按鈕

2.使用js計算使用者輸入的資料,並且將這些資料以物件的形式,儲存成一個陣列來管理。

3.這個陣列的資料同時也會儲存進local storage中,不過會是以字串的形式

4.處理完的BMI結果會呈現在欄位右方的圓圈,還有BMI狀態。

5.底下會生成一個相應的列表,記載BMI指數, BMI狀態、身高、體重、檢測日期。
圓圈以及列表都會根據BMI狀態改變顏色

6.頁尾

7.結果圓圈的右下方有一個重新施測的紐,我想讓它活潑一點,所以想加一個旋轉的小動畫

8.我想要限制列表的數量,讓它不要過長。超過十五筆的話,就從最早的那一條開始刪掉。

9.我想讓列表從最新一筆紀錄開始呈現,而不是最後一筆

10.我想讓中間的列表在使用者第一次打開網頁時隱藏起來,當有紀錄時才開啟

開始動手作

1.先製作一個介面,使用者可以在相應的欄位輸入身高體重,然後按下結果按鈕

我將網頁分成三個部分,第一個是欄位區,叫header,中間要放檢測紀錄,就叫history,最後是頁尾,就叫footer。

先分好之後,就開始製作欄位。考慮到這個區塊的各個元素要相互對齊,我新增一個div區塊叫做cal,將這個區塊的display設定成flex,然後使用以下語法:

1
2
align-items: center;
justify-content: space-evenly;

我之前常常搞不清楚Items和content的區別,為了以防忘記.在這裡寫起來
items
align - items和justify-items只能作用於單行

content
若有多行元素時,就應使用align-content / justify-content

然後,要記得,flex這個屬性必須掛在外容器上!
flexbox參考網頁

接下來,因為需要用到js抓取輸入的資料,各個輸入欄位我都下了不同的class,之後按鈕也要做變化,所以也先下了一個class。

1
2
3
4
5
6
7
var height = document.querySelector(".height"); //使用者身高
var weight = document.querySelector(".weight"); //使用者體重
var resultBtn = document.querySelector(".result-btn"); //結果按鈕
var list = document.querySelector(".list"); //歷史列表
var his = document.querySelector(".history"); //要呈現列表的區塊

//有了class,就可以抓取DOM元素,進行接下來JS的操作。

2.使用js計算使用者輸入的資料,並且將這些資料以物件的形式,儲存成一個陣列來管理。

為了要儲存並且更好管理將來的資料,我先宣告一個陣列:

1
2
//若是資料庫中有資料,就轉成物件存進Data, 若沒有就創造一個空陣列
var data = JSON.parse(localStorage.getItem("BMIlist")) || [];

這個陣列會從資料庫獲得資料,然後把資料轉化成物件儲存在自己裡面。
為什麼不能直接宣告空陣列呢?因為我希望使用者在第二次第三次打開這個網頁時,都能看到自己以前的紀錄,所以要讀取資料庫的資料。
這個陣列中的物件,我計劃要有以下的屬性:

BMIstatus(BMI狀態), BMIindex(BMI指數), height, weight, time, color

接下來是計算使用者資料。我寫了一個function。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function calculate(e){
//data陣列中的各個屬性處理
//1. 身高, 體重, BMI結果
var h = parseInt(height.value)/100;
var w = parseInt(weight.value);
var result = w/(h*h);
result = result.toFixed(2);
console.log(result);
var status = "";
var color = "";
switch (true){
case result <= 18.5:
status = "過輕";
color = "#31baf9";
break;
case 18.5 < result && result <=25:
status = "理想"
color = "#86d73f";
break;
case 25 < result && result <=30:
status = "過重";
color = "#ff982d";
break;
case 30 < result && result <=35:
status = "輕度肥胖";
color = "#ff6c03";
break;
case 35 < result && result <=40:
status = "中度肥胖";
color = '#ff6c03';
break;
case 40 < result:
status = "重度肥胖"
color = "#ff1200";
break;
default:
alert('資料有誤');
break;

}

//獲得當前日期
var date = new Date();
var day = date.getDate();
var month = date.getMonth()+1;
var year = date.getFullYear();

//將計算出的資料存入一個物件中
var BMIdata = {
height:h,
weight:w,
BMI:result,
BMIstatus: status,
time: month + "-" + day + "-" + year,
color: color,};

//將最新的資料插入陣列中第一個,讓列表能從最新一筆開始
data.splice(0, 0, BMIdata); //將新的BMIdata(物件)存進data
updateList(data);
localStorage.setItem("BMIlist", JSON.stringify(data));
if(data.length > 15){
clear();};

switchBTN();
}

一點小提醒:
1.我發現如果不寫parseInt的話,height.value傳進來的會是字串而不是數字(我想是因為,.value本來就是抓取文字欄位元素的關係),為了保險起見,兩個需要傳入的變數我都加上parseInt。順帶一提,parseInt是將字串轉換為數字的一個function。

2.為了讓數字容易運算,結果看起來乾淨漂亮,我在result的數字做了toFixed(2)的處理-讓落落長的數字取到剩下小數點後兩位。

3.switch case的使用:原本我以為只能在有特定值時才可以使用,但其實換個角度想

1
2
3
4
if(result <= 18.5){
status = "過輕";
color = "#31baf9";
}

這個意思不就代表 當result <=18.5這個表達式 === true嗎?
所以可以改寫為

1
2
3
4
switch(true){
case result <= 18.5:
//some action
}

如此一來,物件中的BMIstatus, BMIindex, height, weight都有了,差一個time

在js中要取得當前時間,必須這樣寫:

1
2
3
4
5
//獲得當前日期
var date = new Date();
var day = date.getDate(); //取得日期
var month = date.getMonth()+1; //取得月份
var year = date.getFullYear(); //取得年分

要先呼叫一個新的Date物件,然後就可以使用下面幾個函示來取得你想要的內容。對了,getMonth()中,月份的計算是從0開始,一月(0)到十二月(11),所以要是你想要正確的數字的話,你應該要記得加一。(那為什麼日期不也從0開始……?)

1
2
3
4
5
6
7
8
var BMIdata = {
height:h,
weight:w,
BMI:result,
BMIstatus: status,
time: month + "-" + day + "-" + year,
color: color,
};

把物件中的各屬性配上之前寫的變數。

順帶一提,為了避免使用者沒有填入數字或是只寫零,我又加了一個function。

1
2
3
4
5
6
7
8
9
10
11
12
resultBtn.addEventListener("click", check); 
//按下按鈕後,先檢查是否有輸入數字,或是是否有輸入0,無前述狀況再執行計算
function check(e){
if(height.value == "" || weight.value ==""||height.value ==0 || weight.vaue == 0){
alert("數字不正確");
}
else{
calculate(height.value, weight.value);

}
};
//使用者送出的資料會先跑進這個Function裡檢查有沒有零或是根本沒有填入,若是如此就會跳出一個提醒,若是都有填數字就會開始計算

3.這個陣列的資料同時也會儲存進local storage中,不過會是以字串的形式

處理好陣列後,我們要來把它新增進資料庫中

1
calStorage.setItem("BMIlist", JSON.stringify(data));

在資料庫中新增一個叫做BMIlist的欄位,把data陣列中的物件字串化後傳進去。
這樣一來,當function calculate把資料都計算完,並且把資料儲存進陣列時,資料庫也會一起被更新。

4.底下會生成一個相應的列表,記載BMI指數, BMI狀態、身高、體重、檢測日期。

圓圈以及列表都會根據BMI狀態改變顏色

我在做的時候,因為感覺旁邊的結果圓圈比較難,所以先從列表開始做。
我寫了一個Function來處理這件事,這個function叫做updateList(data),括號中間的data實際上跟上面提到的data陣列是不同的東西,之後我們還要再呼叫一次這個Function並且在括號中傳入data陣列。我只是想提醒我自己接下來要傳進去的是什麼東西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function updateList(data){
//先抓取資料庫中資料

str = "";
//跑迴圈, 對每一個data中的元素都做處理
for(var i=0; i< data.length; i++){
his.style.display = "block";
let text = '';
//使用一個template function來統一處理html
function template(item){
return text =`<li data-num =${i} class = listItem style = 'border-left: 7px ${data[i].color} solid'><div class = 'BMIstatus'><span style = 'font-size: 20px'> ${data[i].BMIstatus}</span></div><div class = 'result'>BMI <span style = 'font-size :20px; margin-left: 10px'>${data[i].BMI}</span></div><div class = 'weight-kg'>weight<span style = 'font-size :20px;margin-left: 10px'>${data[i].weight}kg</span></div><div class = 'height-cm'>height<span style = 'font-size :20px; margin-left: 10px'>${data[i].height*100}cm</span></div><div class = 'test-date'>${data[i].time}</div></li>`
}
//抓取陣列資料,根據抓到資料的bmistatus分類並列表
switch (data[i].BMIstatus){
case "過輕":
template(data[i]);
break;

case "理想":
template(data[i]);
break;

case "過重":
template(data[i]);
break;

case "輕度肥胖":
template(data[i]);
break;

case "中度肥胖":
template(data[i]);
break;

case "重度肥胖":
template(data[i]);
break;

default:
alert('something is wrong!');
break;

}

str += text;


};
list.innerHTML = str;

};

在這邊最重要的做法就是,使用innerHTML來把新增的列表元素加進ul列表中。所以我先設定一個空字串(str)的變數,在跑接下來的迴圈時,會依序加進相應的內容,最後再將這個字串變成html元素塞進ul列表中。

這樣一來,列表就完成了!這個function被放在程式碼最上面(因為我希望使用者一打開網頁時就能出現列表),還有Calculate函式中(這樣有新的資料傳進資料庫時,列表就能馬上更新)。

5. 我想要限制列表的數量,讓它不要過長。超過十五筆的話,就從最早的那一條開始刪掉。

列表完成之後,在測試時發現,要是一直增加資料而沒有一個限制的話,列表就會變的落落長。要避免這件事,我決定限制陣列的長度,寫了一個function

1
2
3
4
5
6
7
8
9
 if(data.length > 15){
clear();
};
function clear(){
for(var i=0; i< data.length; i++){
data.splice(i, 1);
};
localStorage.setItem("BMIlist", JSON.stringify(data));
}

clear的功能是,把陣列的元素都跑一遍後,從最後那一筆刪掉一筆,然後用修剪過的陣列再更新一次資料庫。
之後當陣列大於15時,就呼叫這個函式,把陣列修剪掉。
看到這裡你可能覺得很奇怪,把最後那一筆刪掉的話,不就等於把最新的那筆資料刪掉嗎?
這一步驟跟下一步驟環環相扣,請看:

6.我想讓列表從最新一筆紀錄開始呈現,而不是最後一筆

網頁在這一步已經完成了有七成了吧。看著列表都能一一出現我已經很滿意,但是因為列表是由陣列的0~14開始跑,我想一想覺得不太對勁:最新的資料卻要滑到最下面才能看到,不是很不方便嗎?

為了解決這個問題,我一開始的解法是使用
data.reverse()
反轉陣列。讓最後一筆能夠成為陣列第一筆。
結果我發現,因為有許多function都在執行這個陣列,一邊反轉的話,另一邊就會出錯。

最後,我發現事情可以不用這麼複雜。

1
2
data.splice(0, 0, BMIdata); 
splice( x, y, item)

x是你想插入的位置,y是你想從那位置之後刪掉的資料數目,item是你想插進去的資料。
使用splice,讓新的資料從第零筆插入,成為第零筆就好,最後一筆成為第一筆,原本的倒數第二成為第二筆,如此往後推進。
也因此在上一步驟中的迴圈,我把最後一筆刪掉,其實也等於是把最早的刪掉。

其實我在這裡犯過不少愚蠢的錯……

7.處理完的BMI結果會呈現在欄位右方的圓圈,還有BMI狀態。

不知道為什麼,我一直很害怕這個部分,所以我拖到很後面才做。
這一步中,我們要把原本的結果按鈕刪掉,然後新增一個會變色的圓圈圈,圈圈中間有BMI指數,還有一個重新開始的小按鈕,圓圈旁邊跟著一段顯示結果的文字。

圓圈

先寫一個function,叫做switchBTN(){}

接著來刪掉原本的按鈕。

1
2
3
4
var getParent = resultBtn.parentNode;
getParent.removeChild(resultBtn);
var div = document.createElement("div");
div.setAttribute("class", "show");

我先取得按鈕的父元素(ParentNode),然後移除按鈕(remoceChild),接著新增一個div元素,給他一個Class名稱(我已經預先在css中設定好外觀了)。

接下來,因為這個圓圈有一些必須隨著資料變動的要素,所以就再跑一次迴圈,抓取各元素中的特定值(我要找的是bmi指數以及狀態)

接下來主要分兩部分,先寫個函式把接下來會重複的動作統一處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//這個函式其實一直在做一樣的事, 就是根據傳進來的資料, 還有這筆資料的各種屬性來渲染不同的html
function btnHandle(item){
str = "<p >"+item.BMI+"</p><p style = 'font-size: 14px;'>BMI</p><a href = 'index.html'></a>";
div.innerHTML = str;
div.style.color = item.color;
div.style.border = `5px solid ${item.color}`;
div.style.position = "relative";
var a = div.querySelector("a");
a.style.backgroundColor = item.color;
getParent.appendChild(div);

//將str的字串新增到div元素中,設定css樣式

var p = document.createElement("div");
p.setAttribute("class", "statusText")
var pStr = `<p> ${item.BMIstatus}</p>`
p.innerHTML = pStr;
p.style.color = item.color;
div.appendChild(p);
}

然後是條件判斷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
switch (data[0].BMIstatus){
case "過輕":
btnHandle(data[0]);
break;
case "理想":
btnHandle(data[0]);
break;

case "過重":
btnHandle(data[0]);
break;

case "輕度肥胖":
btnHandle(data[0]);
break;

case "中度肥胖":
btnHandle(data[0]);
break;

case "重度肥胖":
btnHandle(data[0]);
break;

default:
alert('something is wrong!');
break;
}

這個功能基本上就完成了。
在這裡的做法跟做列表有點像,都使用到一個把字串塞進空字串中,然後創造html元素的做法。不過在這裡我沒有跑for迴圈,而是用switch來判斷當下的狀態,因為前面我把最新的資料都變成第零筆,所以我只要判斷第零筆的BMI狀態就好。

舉上面的例子,假如第零筆(也就是最新這一筆)的BMI狀態是過輕的話,我就在str中放進兩個p段落(一個是指數,一個是小小的bmi字樣)和一個a連結(重新開始的按鈕),這幾個元素的外觀我都在css中設定好了。

把str的HTML元素用innerHTML的方法設定好之後,接下來是根據該狀態來變更顏色。想要用js變更css樣式的話,就先把要變更的元素抓取過來,然後加上.style.color/ .border/….等等css語法。
a連結也設定好之後,使用appendChild把這些新增好的元素加進剛剛的Parent node之下,就可以了。

別忘了,還有旁邊的狀態要寫!

8.頁尾

頁尾只有一個Logo要放,但是原本給的logo是透明的,不是這個顏色。
我使用了css中的filter來調整,可以上codepen找css filter。

9.結果圓圈的右下方有一個重新施測的紐,我想讓它活潑一點,所以想加一個旋轉的小動畫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.show a:hover{
animation: spin 1.5s infinite;
transition: ease;

}

/* 讓按鈕活潑 */
@keyframes spin{
0%{   動畫的第0格時
transform: rotate(0);
}
50%{ 動畫的中間
transform: rotate(360deg);
}
100%{ 動畫的結尾
transform: rotate(360deg);
}
}

我想做的是:圖案旋轉一圈後,停止一下,再開始新的loop。
除了要把iteration設定成infinite(無限)之外,因為要有間隔,所以50%和100%時做的是一樣的事,讓它停在原地。

10.我想讓中間的列表在使用者第一次打開網頁時隱藏起來,當有紀錄時才開啟

到這裡網頁都完成了,但是當使用者第一次打開這個網頁時,中間的history區域因為沒有內容,會一片空白,我覺得這樣很奇怪,所以想要先把它隱藏起來,等到有資料進入時再出現。

沒想到我在這裡也花了很久!

我一開始的做法是:把history區塊整個刪掉,先做條件判斷(當Data的長度>=1時),然後再一一新增回來。但是因為data有兩筆時也會大於一,有三筆時也會大於一,也就是說,只要有新的資料傳進來,這個條件判斷就會永遠成立,那就等於沒有用了!

最後才靈光一閃

his.style.display = "none";

我在程式馬最上面加了這一行,代表:「在網頁打開時,history列表是關起來的」,然後在updateList函式中,我才把它打開,這樣在生成列表的時候,history區塊也會跟著出現了。

心得

其實距我做完這份作業已經過去幾個月了,那時候剛碰js,做得小心翼翼又戰戰兢兢的,還老是很悲觀地覺得我一定不行。能夠完成真的很開心!而且老實說,我那時的程式碼超冗長,哈哈,這次一邊整理筆記一邊改寫,將當初很多段重複又累贅的地方重寫,心中很感慨,原來我也是有進步的。算是在最近有些停滯又迷惘的日子提供一點新的動力。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×