# 【Vue電商】Vue 元件 - 章節作業:各地空氣質素監察
###### tags: `vue`
作業任務:
- 資料內容以元件呈現
- 使用emit,把內層資料傳送到外層
- 抓取API
- 按空氣質素呈現不同顏色
- 按選單所點選的地區,過濾資料
- localstorage記錄之前關注的城市
## 抓取API
```javascript=
mounted: function() {
const vm = this;
const api = 'https://opendata.epa.gov.tw/api/v1/AQI?%24skip=0&%24top=1000&%24format=json';
// 使用 jQuery ajax
$.get(api)
.then(function(response) {
vm.data = response;
console.log(response)
})
.catch(function(error){
console.log(error);
})
}
```
注意,**AJAX抓取資料**必須至少要等到**created之後才可以進行**,所以抓API的程式碼,需要寫在`mounted`或`created`裏。
jQuery的`get`方法:`$.get(url)`
jQuery的`then`方法,會回傳一個promise物件,第一個function帶出promise物件裏resolved的值,第二個function帶出promise物件裏rejected的值。
你也可以使用`catch`方法去接收rejected的值。
## 把各地區的名稱抓出來,放到選單選項裏
抓取API後,需要抓出裏面的所有地區名稱,並放到選單選項裏。注意,因為會有重複的地區的名稱出現,例如高雄市也有高雄市 - 前金、高雄市 - 左營等等,所以需要抓取**唯一值**,避免重複。
利用`new Set()`的方法就可取得唯一值,因為`Set`物件只容許唯一值,不能有重複的值。之後再利用展開運算子把它轉為陣列。
注意,展開運算子(spread)只能展開可以被迭代的東西,物件本身是不能被迭代,因此無法使用展開運算子,而Set是可以被迭代的:
![](https://i.imgur.com/VVt3BcZ.png)
[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator)
![](https://i.imgur.com/0YSVHYB.png)
[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)
所以可以寫成這樣:
```javascript=
// API 來源
// https://opendata.epa.gov.tw/Data/Contents/AQI/
var app = new Vue({
el: '#app',
data: {
data: [],
starList: [],
filter: '',
},
// 請在此撰寫 JavaScript
computed: {
filterCounty: function() {
const allCounty = this.data.map( item => item.County);
// Set 物件只會容許儲存單一值
// 用展開運算子,把Set物件轉為陣列
return new Set([...allCounty]);
}
},
mounted: function() {
const vm = this;
const api = 'https://opendata.epa.gov.tw/api/v1/AQI?%24skip=0&%24top=1000&%24format=json';
// 使用 jQuery ajax,抓取API
$.get(api)
.then(function(response) {
vm.data = response;
console.log(response)
})
.catch(function(error){
console.log(error);
})
}
});
```
之後在select用`v-for`把地區的唯一名稱印出來:
```htmlembedded=
<div id="app">
<select name="county" id="county" class="form-control mb-3">
<option value="" disabled selected>--- 請選擇城市 ---</option>
<option v-for="county in filterCounty"> {{ county }} </option>
</select>
...
</div>
```
![](https://i.imgur.com/EpQpR2f.jpg)
## 資料內容以元件呈現
抓到所有資料後,把所有資料當一個`card`元件來呈現,裏面放滿需要顯示的資料,例如地區名稱、AQI指數等等。之前在該`card`元件上使用`v-for`迭代資料,把一個個card渲染出來。
注意,**使用`v-for`時需要綁上`key`值,`key`值需要是唯一值**,下文會再詳細講解這部分:
```htmlembedded=
<div id="app">
...
<div class="card-columns">
<card v-for="item in data" :air-quality-data="item" :key="item.SiteId"></card>
</div>
</div>
```
建立template:
```htmlembedded=
<script type="text/x-template" id="cardTemplate">
<div class="card status-aqi2">
<div class="card-header"> {{ airQualityData.County }} - {{ airQualityData.SiteName }}
<a href="#" class="float-right">
<i class="far fa-star"></i>
</a>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li>AQI 指數: {{ airQualityData.AQI }} </li>
<li>PM2.5: {{ airQualityData['PM2.5']}} </li>
<li>說明: {{ airQualityData.Status }}</li>
</ul>
{{ airQualityData.PublishTime }}
</div>
</div>
</script>
```
註冊元件:
```javascript=
Vue.component('card',{
template: '#cardTemplate',
props: ['airQualityData']
})
```
結果:
![](https://i.imgur.com/yjcA0Uw.png)
## 按選單點選的地區過濾資料
首先在select的標籤用`v-model`綁定外層資料`filter`:
```htmlembedded=
<select name="county" id="county" class="form-control mb-3" v-model="filter">
...
</select>
```
然後按`filter`的名稱,在`computed`裏建立函式,過濾資料:
```javascript=
var app = new Vue({
el: '#app',
data: {
data: [],
filter: '',
},
computed: {
filterCounty: function() {
const allCounty = this.data.map( item => item.County);
// Set 物件只會容許儲存單一值
// 用展開運算子,把Set物件轉為陣列
return new Set([...allCounty]);
},
filterData: function() {
const vm = this;
// 剛載入畫面後,預設顯示所有資料
if(this.filter === '') {
return this.data
// 按filter的地區名稱去撈資料
}else{
return this.data.filter( item => item.County === vm.filter)
}
}
},
mounted: function() {
...
}
});
```
之前我們是在data裏撈資料顯示,現在改為在filterData裏撈資料:
```htmlembedded=
<div id="app">
<select name="county" id="county" class="form-control mb-3" v-model="filter">
<option value="" disabled selected>--- 請選擇城市 ---</option>
<option v-for="county in filterCounty" :value="county"> {{ county }} </option>
</select>
<div>
<h4>關注城市</h4>
...
</div>
<hr>
<div class="card-columns">
<!-- 在filterData 裏撈資料 -->
<card v-for="item in filterData" :air-quality-data="item" :key="item.SiteId" @star-list="editStarList"></card>
</div>
</div>
```
## 按空氣質素狀態去標示不同顏色
取每筆資料中的`Status`屬性去做判斷,決定該card需要什麼背景顏色。
```javascript=
Vue.component('card',{
template: '#cardTemplate',
props: ['airQualityData'],
computed: {
statusColor: function() {
switch (this.airQualityData.Status) {
case '良好':
return '';
case '普通':
return 'status-aqi2';
case '對敏感族群不健康':
return 'status-aqi3';
case '對所有族群不健康':
return 'status-aqi4';
case '非常不健康':
return 'status-aqi5';
case '危害':
return 'status-aqi6';
}
},
}
});
```
Template裏動態綁定class:
```htmlembedded=
<script type="text/x-template" id="cardTemplate">
<!-- 動態綁定class -->
<div class="card" :class="statusColor">
<div class="card-header"> {{ airQualityData.County }} - ...</div>
...
</div>
</script>
```
結果:
![](https://i.imgur.com/BSk5oEu.png)
## 加入關注城市(重要!)
要做的功能:
- 點選星星時,會把該城市加入關注城市
- 再次點選星星時,會把該城市從關注城市裏移除
- 星星的樣式會由空心變成實心
- localStorage會儲存之前的關注城市
### 點選星星時,會把該城市加入關注城市
在card元件裏寫click事件,並從內層把該城市的SiteName資料一併帶出去外層:
```htmlembedded=
<script type="text/x-template" id="cardTemplate">
<div class="card" :class="statusColor">
<div class="card-header"> {{ airQualityData.County }} - {{ airQualityData.SiteName }}
<!-- 綁定內層的starEmit函式 -->
<a href="#" class="float-right" @click.prevent="starEmit">
<i class="far fa-star"></i>
</a>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li>AQI 指數: {{ airQualityData.AQI }} </li>
<li>PM2.5: {{ airQualityData['PM2.5']}} </li>
<li>說明: {{ airQualityData.Status }}</li>
</ul>
{{ airQualityData.PublishTime }}
</div>
</div>
</script>
```
在內層的starEmit會觸發外層事件star-list,再觸發外層的editStarList函式。當觸發內層的starEmit時,會一併把該資料的SiteName帶出去外層(因為之後需要SiteName去做判斷)。
注意,要讀取SiteName的資料,需要寫成`this.airQualityData.SiteName`,因為早前我們在外層的card元件裏,把`airQualityData`綁定了`v-for="item in filterData"`裏的item,這個item就是每一筆被迭代的資料。
```javascript=
Vue.component('card',{
template: '#cardTemplate',
props: ['airQualityData'],
computed: {
statusColor: function() {
switch (this.airQualityData.Status) {
case '良好':
return '';
case '普通':
return 'status-aqi2';
case '對敏感族群不健康':
return 'status-aqi3';
case '對所有族群不健康':
return 'status-aqi4';
case '非常不健康':
return 'status-aqi5';
case '危害':
return 'status-aqi6';
}
},
},
methods: {
//把SiteName資料傳去外層
starEmit: function() {
this.$emit('star-list',this.airQualityData.SiteName);
}
}
});
```
外層card元件觸發editStarList:
```htmlembedded=
<div id="app">
...
<div class="card-columns">
<card v-for="item in filterData" :air-quality-data="item" :key="item.SiteId" @star-list="editStarList"></card>
</div>
</div>
```
editStarList函式:
當按下星星,就會把該城市的SiteName加到starSiteName裏。
在加入之前,會做if else判斷,避免重複加入城市。利用`indexOf()`去檢查在starSiteName陣列裏是否已經有該SiteName,如沒有就新增,如有就刪除(因為當再次點選星星,就代表想要在關注城市裏移除該城市)。
```javascript=
var app = new Vue({
el: '#app',
data: {
data: [],
// 預設是撈取存在localStorage的資料
starSiteName: JSON.parse(localStorage.getItem('AQIstarList')) || [],
filter: '',
},
// 請在此撰寫 JavaScript
computed: {
filterCounty: function() {
const allCounty = this.data.map( item => item.County);
// Set 物件只會容許儲存單一值
// 用展開運算子,把Set物件轉為陣列
return new Set([...allCounty]);
},
filterData: function() {
const vm = this;
// 剛載入畫面後,預設顯示所有資料
if(this.filter === '') {
return this.data
// 按filter的地區名稱去撈資料
}else{
return this.data.filter( item => item.County === vm.filter)
}
},
starList: function() {
const vm = this;
return this.data.filter( item => vm.starSiteName.indexOf(item.SiteName) !== -1);
}
},
methods: {
editStarList: function(siteName) {
// 判斷是否有重複的site name,如沒有就加入,如有就刪除該筆城市資料
if(this.starSiteName.indexOf(siteName) === -1){
this.starSiteName.push(siteName);
}
else {
this.starSiteName.splice(this.starSiteName.indexOf(siteName), 1);
}
localStorage.setItem('AQIstarList', JSON.stringify(this.starSiteName));
}
},
mounted: function() {
const vm = this;
const api = 'https://opendata.epa.gov.tw/api/v1/AQI?%24skip=0&%24top=1000&%24format=json';
// 使用 jQuery ajax
$.get(api)
.then(function(response) {
vm.data = response;
console.log(response)
})
.catch(function(error){
console.log(error);
})
}
});
```
### 憑藉starSiteName裏的SiteName,取回一筆筆完整的資料(做作業時有卡關)
只把SiteName加進陣列是不夠的,因為還要把整筆資料顯示出來,所以我們要憑著在starSiteName裏的SiteName,取回一筆筆完整的資料。
在computed裏,建立starList,透過使用`filter`和`indexOf`方法,過濾data裏的資料,如果該筆資料的SiteName是存在在`starSiteName`裏(即是indexOf不會等於-1),就回傳該筆資料:
```javascript=
var app = new Vue({
el: '#app',
data: {
data: [],
starSiteName: JSON.parse(localStorage.getItem('AQIstarList')) || [],
filter: '',
},
// 請在此撰寫 JavaScript
computed: {
filterCounty: function() {
const allCounty = this.data.map( item => item.County);
// Set 物件只會容許儲存單一值
// 用展開運算子,把Set物件轉為陣列
return new Set([...allCounty]);
},
filterData: function() {
const vm = this;
// 剛載入畫面後,預設顯示所有資料
if(this.filter === '') {
return this.data
// 按filter的地區名稱去撈資料
}else{
return this.data.filter( item => item.County === vm.filter)
}
},
starList: function() {
const vm = this;
// 如果該筆資料的SiteName是存在在starSiteName裏,就回傳該筆資料
return this.data.filter( item => vm.starSiteName.indexOf(item.SiteName) !== -1);
}
},
methods: {
editStarList: function(siteName) {
// 判斷是否有重複的site name,如沒有就加入,如有就刪除該筆城市資料
if(this.starSiteName.indexOf(siteName) === -1){
// console.log('index:' + this.starSiteName.indexOf(item));
this.starSiteName.push(siteName);
}
else {
// console.log('index:' + this.starSiteName.indexOf(item));
this.starSiteName.splice(this.starSiteName.indexOf(siteName), 1);
}
localStorage.setItem('AQIstarList', JSON.stringify(this.starSiteName));
}
},
mounted: function() {
...
}
});
```
之後`starList`就會是一筆筆完整的資料,這時候在「關注城市」的card元件裏,改為在`starList`裏,把資料印出來:
```htmlembedded=
<div id="app">
<select name="county" id="county" class="form-control mb-3" v-model="filter">
<option value="" disabled selected>--- 請選擇城市 ---</option>
<option v-for="county in filterCounty" :value="county"> {{ county }} </option>
</select>
<div>
<h4>關注城市</h4>
<div class="card-columns">
<!-- 改在starList 裏撈資料 -->
<card v-for="item in starList" :air-quality-data="item" :key="item.SiteId" @star-list="editStarList"></card>
</div>
</div>
<hr>
<div class="card-columns">
<card v-for="item in filterData" :air-quality-data="item" :key="item.SiteId" @star-list="editStarList"></card>
</div>
</div>
```
結果:
![](https://i.imgur.com/3XVSO7m.gif)
### 把星星icon從空心變實心
這個功能有兩個做法都能做到,一是使用`<slot>`,直接把在template中的空心星星替換成實心星星。
二是動態綁定class,透過判斷該筆資料的SiteName目前是否在外層資料的starSiteName,來決定該icon是要綁上實心星星還是空心星星的class名稱。
這裏示範第二個做法:
```javascript=
Vue.component('card',{
template: '#cardTemplate',
// 新增一個叫starSiteNameList的props
// 用來連接外層資料的starSiteName
props: ['airQualityData','starSiteNameList'],
computed: {
statusColor: function() {
switch (this.airQualityData.Status) {
case '良好':
return '';
case '普通':
return 'status-aqi2';
case '對敏感族群不健康':
return 'status-aqi3';
case '對所有族群不健康':
return 'status-aqi4';
case '非常不健康':
return 'status-aqi5';
case '危害':
return 'status-aqi6';
}
},
addStarIcon: function() {
// 判斷這筆資料的SiteName是否在外層資料starSiteName裏面
// 如果是,就用實心星星的class
return this.starSiteNameList.indexOf(this.airQualityData.SiteName) === -1 ? 'far fa-star' : 'fas fa-star'
}
},
methods: {
starEmit: function() {
this.$emit('star-list',this.airQualityData.SiteName);
}
}
});
```
在template裏動態綁定addStarIcon所回傳的值:
```htmlembedded=
<script type="text/x-template" id="cardTemplate">
<div class="card" :class="statusColor">
<div class="card-header"> {{ airQualityData.County }} - {{ airQualityData.SiteName }}
<a href="#" class="float-right" @click.prevent="starEmit">
<!-- 動能綁定addStarIcon -->
<i :class="addStarIcon"></i>
</a>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li>AQI 指數: {{ airQualityData.AQI }} </li>
<li>PM2.5: {{ airQualityData['PM2.5']}} </li>
<li>說明: {{ airQualityData.Status }}</li>
</ul>
{{ airQualityData.PublishTime }}
</div>
</div>
</script>
```
別忘記在card元件裏,需要定義剛才所新增的props,starSiteNameList,它需要連接外層資料starSiteName:
```htmlembedded=
<div id="app">
<select name="county" id="county" class="form-control mb-3" v-model="filter">
<option value="" disabled selected>--- 請選擇城市 ---</option>
<option v-for="county in filterCounty" :value="county"> {{ county }} </option>
</select>
<div>
<h4>關注城市</h4>
<!-- 定義starSiteNameList -->
<div class="card-columns">
<card v-for="item in starList" :air-quality-data="item" :star-site-name-list="starSiteName" :key="item.SiteId" @star-list="editStarList"></card>
</div>
</div>
<hr>
<div class="card-columns">
<!-- 定義starSiteNameList -->
<card v-for="item in filterData" :air-quality-data="item" :star-site-name-list="starSiteName" :key="item.SiteId" @star-list="editStarList"></card>
</div>
</div>
```
結果:
![](https://i.imgur.com/Na6SGn0.gif)
## 當資料被加入關注城市,就只會顯示在關注城市裏
上面的結果可見,當一筆資料被加進關注城市後,下方位置仍然會重複顯示該筆資料,如果要做到不重複顯示,我們就需要修改filterData:
```javascript=
computed: {
...,
filterData: function() {
const vm = this;
// 剛載入畫面後,預設顯示所有資料
if(this.filter === '') {
// 不顯示已被加進關注城市的城市
return this.data.filter( item => vm.starSiteName.indexOf(item.SiteName) === -1 )
// 按filter的地區名稱去撈資料
}else{
const filterList = this.data.filter( item => item.County === vm.filter)
// 不顯示已被加進關注城市的城市
return filterList.filter( item => vm.starSiteName.indexOf(item.SiteName) === -1 )
}
},
...
}
```
結果:
![](https://i.imgur.com/PsOYBxd.gif)