[Vue 空氣品質 api 練習]

壹、作業目標

  • 將資料內容透過 “元件” 呈現
  • 製作城市過濾選項
  • 將外層的資料透過 Props 傳到內層
  • 將內層資料透過 emit 傳遞到外層 (作為另一個關注城市的呈現)
  • 依據不同污染呈現不同色彩
  • 透過 localStorage 儲存上次關注的城市

貳、前言

這個作業算是第一個我以 Vue 完成的小專案,剛從原生 JS 轉換過來框架,有許多概念都滿模糊的。
所以一開始滿痛苦的,這個小專案也花了我兩天的時間。
附上codepen


參、程式發想流程


開發心得: 起頭還是要從最簡單的開始,難的事留到後面想,有時候前面的路鋪好了,後面難走的路也會變順。

一、 使用撈出空汙 JSON 的資料呈現在畫面上。

二、 將各縣市的資料撈出來過濾,去除重複的縣市。

三、 選擇下拉式選單縣市,下方呈現出對應的縣市空污資料。

四、 點選星號,可以將下方的資料新增到關注名單。而下方被點選的資料會消失。

五、 點選上方關注資料的星號,那筆關注資料會被取消,回到下方的分類資料。

六、 特定的空氣品質,給予對應的背景顏色。

七、 將關注的資料儲存到 localStorage 裡,並且網頁下次打開,關注欄也會呈現最新的空污資訊。


肆、程式撰寫細節

一、 撈出空汙 JSON 的資料呈現在畫面上。

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
<div class="card-columns">
// 使用 v-for 建議都綁定 :key,以防之後可能會使用到不被快速置換的標籤
// :pm-data="item" 綁定後,就會把 item 在 filterData 讀到的資料傳到 pm-data
<county-list v-for="(item,key) in filterData" :pm-data="item" :key="key" @star-click="toggleFocus">
</county-list>
</div>

//建立 x-template 模板
<script type="text/x-template" id="listTemplate">
<div class="card" :class="statusColor">
<div class="card-header">{{ pmData.County }} - {{ pmData.SiteName }}
<a href="#" class="float-right" @click.prevent="staredCounty">
<slot>
<i class="far fa-star"></i>
</slot>
</a>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li>AQI 指數: {{ pmData.AQI }}</li>
<li>PM 2.5: {{ pmData['PM2.5_AVG'] }}
// pmData['PM2.5_AVG'] 因為有特殊標點符號,要改成括號'[]'
<li>說明: {{ pmData.Status }}</li>
</ul>
{{ pmData.PublishTime }}
</div>
</div>
</script>

<script>

//使用 props 將外層資料傳到內層(x-template 裡面)
Vue.component('county-list', {
props:['pmData'],
template: '#listTemplate',
})

var app = new Vue({
el: '#app',
data: {
filterData: [],
},
created: function() {
const vm = this;
const api = 'http://opendata2.epa.gov.tw/AQI.json';

// 把 ajax 資料放到 filterData 裡面
$.get(api).then(function( response ) {
vm.filterData = response;
}
},
});
</script>

二、 將各縣市的資料撈出來過濾,去除重複的縣市。

1
2
3
4
5
6
7
8
9
10
//擷取下拉式選單縣市資料
let locationArray = [];// 創建出一個陣列,用來存放擷取出來的縣市
for(let i=0 ; i<vm.data.length ; i++){
locationArray.push(vm.data[i].County);
// 把全部縣市放到locationArray裡面
}
vm.location = locationArray.filter(function(element,index,array){
//使用 filter 把重複的縣市過濾掉
return array.indexOf(element) === index;
});

三、 選擇下拉式選單縣市,下方呈現出對應的縣市空污資料。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<select name="" id="" class="form-control mb-3" @change="filterCounty">
//當下拉式選單被選取時,觸發@change="filterCounty"
<option value="">--- 請選擇城市 ---</option>
<option :value="item" v-for="(item,key) in location" :key="key">{{ item }}</option>
</select>

<script>
methods: {
filterCounty: function(e){
const vm = this;
vm.filterData = [];
for(let i=0 ; i<vm.data.length ; i++){
if(e.target.value == vm.data[i].County){ //當下拉式選單選取的縣市等於資料中的縣市時
vm.filter = e.target.value;
vm.filterData.push(vm.data[i]); //將資料中的縣市,加入到 filterData 裡面
};
};
}
},
</script>

四、 點選星號,可以將下方的資料新增到關注名單。而下方被點選的資料會消失。

五、 點選上方關注資料的星號,那筆關注資料會被取消,回到下方的分類資料。

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
66
67
68
69
70
71
<h4>關注城市</h4>
<div class="card-columns">
<county-list v-for="(item,key) in stared" :pm-data="item" :key="key" @star-click="toggleFocus">
// 3-1.staredCountystar-click 在這
<i class="fas fa-star"></i>
</county-list>
</div>
</div>
<hr>
<!-- 城市列表 -->
<div class="card-columns">
<!-- 使用 v-for 建議都綁定 :key,以防之後可能會使用到不被快速置換的標籤 -->
<!-- :pm-data="item" 綁定後,就會把 item 在 filterData 讀到的資料傳到 pm-data -->
<county-list v-for="(item,key) in filterData" :pm-data="item" :key="key" @star-click="toggleFocus">
// 3-2.staredCountystar-click 在這,他會觸發外層的資料 toggleFocus
</county-list>
</div>
</div>

<script type="text/x-template" id="listTemplate">
<div class="card" :class="statusColor">
<div class="card-header">{{ pmData.County }} - {{ pmData.SiteName }}
<a href="#" class="float-right" @click.prevent="staredCounty"> // 2.staredCounty 在這!!
<slot>
<i class="far fa-star"></i>
</slot>
</a>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li>AQI 指數: {{ pmData.AQI }}</li>
<li>PM 2.5: {{ pmData['PM2.5_AVG'] }}
<li>說明: {{ pmData.Status }}</li>
</ul>
{{ pmData.PublishTime }}
</div>
</div>
</script>

<script>
Vue.component('county-list', {
props:['pmData'],
template: '#listTemplate',
methods:{
staredCounty: function(){
this.$emit('star-click',this.pmData);
// 1. staredCounty 使用 emit 觸發 star-click 外層的資料,同時將 pmData 的資料傳出去
}
},
})
var app = new Vue({
methods: {
//**這邊的 toggleFocus 給加上關注還有取消關注同時使用
toggleFocus: function(focusList){ // 4. focusList 接收到的資料就是來自 (1.) 的 pmData
const vm = this;
// 5.先去搜尋關注名單裡面有沒有這筆資料
if(vm.stared.indexOf(focusList) == -1){ //6. 沒有,加入
this.stared.push(focusList);
this.filterData.splice(this.filterData.indexOf(focusList), 1);
//7.將資料加入關注名單的同時,要將非關注的資料移除
}else if(vm.stared.indexOf(focusList) != -1){ //8. 有,移除
this.stared.splice(this.stared.indexOf(focusList), 1);
if(focusList.County == vm.filter){
this.filterData.push(focusList);
}//9.注意,移除時,要是在同一個縣市的資料才能 push 進去,不然就會發生你頁面在台中市,
//當你點選取消苗栗縣的關注時,苗栗的這筆資料就會加到台中的非關注頁面
}
}
}
})
</script>

六、 特定的空氣品質,給予對應的背景顏色。

這個我放在 component 裡面直接處理掉,因為它屬於內層的資料

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
<script type="text/x-template" id="listTemplate">
<div class="card" :class="statusColor">// 1.這邊動態加上 class 的狀態
<div class="card-header">{{ pmData.County }} - {{ pmData.SiteName }}
<a href="#" class="float-right" @click.prevent="staredCounty"> // 2.staredCounty 在這!!
<slot>
<i class="far fa-star"></i>
</slot>
</a>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li>AQI 指數: {{ pmData.AQI }}</li>
<li>PM 2.5: {{ pmData['PM2.5_AVG'] }}
<li>說明: {{ pmData.Status }}</li>
</ul>
{{ pmData.PublishTime }}
</div>
</div>
</script>

<script>
Vue.component('county-list', {
props:['pmData'],
template: '#listTemplate',
methods:{
staredCounty: function(){
this.$emit('star-click',this.pmData);
}
},
computed: {
//2. 宣告兩個陣列 level、cssColor
statusColor: function(){
let level = ['良好','普通','對敏感族群不健康','對所有族群不健康','非常不健康','危害']
let cssColor = ['','status-aqi2','status-aqi3','status-aqi4','status-aqi5','status-aqi6']
return cssColor[level.indexOf(this.pmData.Status)];
// 3. 使用 indexOf 去找到資料的狀態,對應 level 的 index,去選擇要對應哪一個顏色
}
},
})
</script>

七、 將關注的資料儲存到 localStorage 裡,並且網頁下次打開,關注欄也會呈現最新的空污資訊。

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
var app = new Vue({
el: '#app',
data: {
data: [],
filterData: [],
location: [],
stared: [],
filter: '',
},
methods: {
filterCounty: function(e){
const vm = this;
vm.filterData = [];
for(let i=0 ; i<vm.data.length ; i++){
if(e.target.value == vm.data[i].County){
vm.filter = e.target.value;
vm.filterData.push(vm.data[i]);
};
};

//4-2. 切換城市時,會將所有這個名稱的城市放到下方未關注的欄位,不管這個城市是否被選取了,
//這時又得把這個程式寫一次,過濾掉已經被關注的資料
for(let i=0 ; i<localStorage.length ; i++){
for(let j=0;j<vm.filterData.length;j++){
if(vm.filterData[j].SiteId == JSON.parse(localStorage.getItem(localStorage.key(i))).SiteId){
vm.filterData.splice(vm.filterData.indexOf(vm.filterData[j]),1);
}
}
}
},
toggleFocus: function(focusList){
const vm = this;

if(vm.stared.indexOf(focusList) == -1){
this.stared.push(focusList);
this.filterData.splice(this.filterData.indexOf(focusList), 1);
localStorage.setItem(focusList.SiteId,JSON.stringify(focusList));
//1. 當加入關注名單時,也將這份名單存到 localStorage,這邊使用的 Key 是此筆資料的 ID,這樣能保有唯一性
}else if(vm.stared.indexOf(focusList) != -1){
if(focusList.County == vm.filter){
this.filterData.push(focusList);
}
this.stared.splice(this.stared.indexOf(focusList), 1);
localStorage.removeItem(focusList.SiteId);
// 2. 當取消關注名單時,就將這筆 localStorage 的資料刪除
}
}
},
created: function() {
const vm = this;
const api = 'http://opendata2.epa.gov.tw/AQI.json';

$.get(api).then(function( response ) {
vm.filterData = response;
vm.data = JSON.parse(JSON.stringify(response));

//4-1. 每次 AJAX 取資料的時候都會取全部的資料,但重新整理後,已經被標記的資料,不能出現在非關注的城市資料中
for(let i=0 ; i<localStorage.length ; i++){
for(let j=0;j<vm.filterData.length;j++){
if(vm.filterData[j].SiteId == JSON.parse(localStorage.getItem(localStorage.key(i))).SiteId){
vm.filterData.splice(vm.filterData.indexOf(vm.filterData[j]),1);
}
}
}


//5.localStorage 同步更新的功能,因為儲存到 localStorage 裡面的資料不會改變,
//因此,這段程式在每次重新整理的時候,會檢視 localStorage 裡面有什麼資料,他會再去讀取一次 AJAX 上的資料覆蓋上去
for(let i=0 ; i<localStorage.length ; i++){
let updateId = JSON.parse(localStorage.getItem(localStorage.key(i))).SiteId;
for(let j = 0 ; j<vm.data.length ; j++){
if(vm.data[j].SiteId == updateId){
localStorage.setItem ( vm.data[j].SiteId , JSON.stringify(vm.data[j]) );
}
}
}
});
},

mounted() {
const vm = this;
for(let i=0 ; i<localStorage.length ; i++){
vm.stared.push(JSON.parse(localStorage.getItem(localStorage.key(i)))); //重新整理後 push localStorage 的資料
}//3. 在重新整理後,要將儲存在 localStorage 的資料渲染到畫面上
},

});
</script>

八、bug 修正

  • 當全部的縣市裡全部的資料都被關注的時候,這時下拉式選單中的這個縣市會消失。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    created: function() {
    const vm = this;
    const api = 'http://opendata2.epa.gov.tw/AQI.json';

    $.get(api).then(function( response ) {
    vm.filterData = response;
    //使用物件深拷貝,拷貝一份新的物件,這樣就不會有物件傳參考的問題,
    //如果不拷貝一份物件,當南投區域全部被打星號,跑到關注時,下次重新整理,下拉式選單的南投就會不見
    vm.data = JSON.parse(JSON.stringify(response));

    //擷取下拉式選單縣市資料,這時的 data 就獨立出來一個陣列,和 response 資料就不會有物件傳參考的問題。
    let locationArray = [];
    for(let i=0 ; i<vm.data.length ; i++){
    locationArray.push(vm.data[i].County);
    }
    vm.location = locationArray.filter(function(element,index,array){
    //原本我直接用 indexOf(response.County),但 indexOf 不能用在物件裡面的值。
    return array.indexOf(element) === index;
    });
    });
    },
0%