# 【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)