Try   HackMD

【Vue電商】Vue 元件 - 章節作業:各地空氣質素監察

tags: vue

作業任務:

  • 資料內容以元件呈現
  • 使用emit,把內層資料傳送到外層
  • 抓取API
  • 按空氣質素呈現不同顏色
  • 按選單所點選的地區,過濾資料
  • localstorage記錄之前關注的城市

抓取API

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的程式碼,需要寫在mountedcreated裏。

jQuery的get方法:$.get(url)
jQuery的then方法,會回傳一個promise物件,第一個function帶出promise物件裏resolved的值,第二個function帶出promise物件裏rejected的值。

你也可以使用catch方法去接收rejected的值。

把各地區的名稱抓出來,放到選單選項裏

抓取API後,需要抓出裏面的所有地區名稱,並放到選單選項裏。注意,因為會有重複的地區的名稱出現,例如高雄市也有高雄市 - 前金、高雄市 - 左營等等,所以需要抓取唯一值,避免重複。

利用new Set()的方法就可取得唯一值,因為Set物件只容許唯一值,不能有重複的值。之後再利用展開運算子把它轉為陣列。

注意,展開運算子(spread)只能展開可以被迭代的東西,物件本身是不能被迭代,因此無法使用展開運算子,而Set是可以被迭代的:

MDN

MDN

所以可以寫成這樣:

// 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把地區的唯一名稱印出來:

<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>

資料內容以元件呈現

抓到所有資料後,把所有資料當一個card元件來呈現,裏面放滿需要顯示的資料,例如地區名稱、AQI指數等等。之前在該card元件上使用v-for迭代資料,把一個個card渲染出來。

注意,使用v-for時需要綁上key值,key值需要是唯一值,下文會再詳細講解這部分:

<div id="app"> ... <div class="card-columns"> <card v-for="item in data" :air-quality-data="item" :key="item.SiteId"></card> </div> </div>

建立template:

<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>

註冊元件:

Vue.component('card',{ template: '#cardTemplate', props: ['airQualityData'] })

結果:

按選單點選的地區過濾資料

首先在select的標籤用v-model綁定外層資料filter

<select name="county" id="county" class="form-control mb-3" v-model="filter"> ... </select>

然後按filter的名稱,在computed裏建立函式,過濾資料:

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裏撈資料:

<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需要什麼背景顏色。

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:

<script type="text/x-template" id="cardTemplate"> <!-- 動態綁定class --> <div class="card" :class="statusColor"> <div class="card-header"> {{ airQualityData.County }} - ...</div> ... </div> </script>

結果:

加入關注城市(重要!)

要做的功能:

  • 點選星星時,會把該城市加入關注城市
  • 再次點選星星時,會把該城市從關注城市裏移除
  • 星星的樣式會由空心變成實心
  • localStorage會儲存之前的關注城市

點選星星時,會把該城市加入關注城市

在card元件裏寫click事件,並從內層把該城市的SiteName資料一併帶出去外層:

<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就是每一筆被迭代的資料。

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:

<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,如沒有就新增,如有就刪除(因為當再次點選星星,就代表想要在關注城市裏移除該城市)。

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,透過使用filterindexOf方法,過濾data裏的資料,如果該筆資料的SiteName是存在在starSiteName裏(即是indexOf不會等於-1),就回傳該筆資料:

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裏,把資料印出來:

<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>

結果:

把星星icon從空心變實心

這個功能有兩個做法都能做到,一是使用<slot>,直接把在template中的空心星星替換成實心星星。

二是動態綁定class,透過判斷該筆資料的SiteName目前是否在外層資料的starSiteName,來決定該icon是要綁上實心星星還是空心星星的class名稱。

這裏示範第二個做法:

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所回傳的值:

<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:

<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>

結果:

當資料被加入關注城市,就只會顯示在關注城市裏

上面的結果可見,當一筆資料被加進關注城市後,下方位置仍然會重複顯示該筆資料,如果要做到不重複顯示,我們就需要修改filterData:

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 ) } }, ... }

結果: