使用 Vue.js 實作郵局三倍券存量地圖紀錄

繼前面兩篇 使用 Vue.js + Element UI 製作 Todo List 全紀錄使用 Vue.js 串接 Twitch API 顯示熱門遊戲及直撥頻道,今天再來繼續 Vue.js 的修行

為甚麽想做這個呢?因爲昨天半夜睡不著覺,無聊的時候滑著手機看新聞才發現,哦~原來今天 7/15 是開始領三倍券的日子,所以上到政府資料開放平臺找看看有沒有相關的 API 可以接,結果真的有找到全國郵局即時振興三倍券販賣存量,還有之前沒有跟到寫口罩地圖,所以更加想練習看看,如果文章內有錯誤的地方,請多多包涵 :)

1. Demo & Source Code

Demo

Source Code

2. 縣市地區選單

為了在前端渲染出各縣市地區的下拉選項,當然不可能每次都發請求去要回 JSON 來渲染,所以我們先下載已經整理好的縣市地區資料檔並存到 src/assets 底下

接著將縣市地區的 JSON 檔 Import 進來,並綁定到 Vue Data 中,這邊也建立 select 空物件,以便將下拉選單的值綁定進來

<script>
import cityName from "@/assets/CityCountyData.json";

export default {
  name: "Home",
  data() {
    return {
      cityName,
      select: {},
    };
  },
}
</script>

接著在前端將選項渲染出來,這邊搭配使用的 Element UI 的框架,縣市地區很簡單的搭配 v-for 迴圈把縣市地區渲染出來就可以,那至於地區該怎麼辦?因爲必須判斷縣市再來選擇出相依的地區,所以這邊利用了 Array.prototype.find(),這邊建立一個 city 的 callback,當 city.CityName 等於所選的縣市 select.city 時會回傳陣列的索引值,所以接著在回傳該索引值中的 AreaList 就可以渲染出相依的縣市地區

<el-select
  v-model="select.city"
  placeholder="請選擇縣市"
  size="mini"
>
  <el-option
    v-for="item in cityName"
    :key="item.CityName"
    :label="item.CityName"
    :value="item.CityName"
  ></el-option>
</el-select>

<el-select
  v-model="select.area"
  placeholder="請選擇地區"
  v-if="select.city"
  size="mini"
>
  <el-option
    v-for="item in cityName.find(
      (city) => city.CityName === select.city
    ).AreaList"
    :key="item.AreaName"
    :label="item.AreaName"
    :value="item.AreaName"
  ></el-option>
</el-select>

對了,在地區的下拉選項中,我加入了一個 v-if,當 select.city 有值的時候才會渲染地區的下拉選項,不這麼做的話會因爲渲染時沒有值導致錯誤

3. Import Axios & Test API

完成下拉選單後就可以來串接 API,首先先安裝 Axios 並註冊到 Vue 中

npm install --save axios vue-axios
// src/main.js
import axios from "axios";
import VueAxios from "vue-axios";

Vue.use(VueAxios, axios);

在需要引入的組件中引入,在 Data 中先建立空的陣列以便儲存 API 資料,並在 mounted() 鉤子函數中呼叫 getPostData(),以便在元素載入後自動呼叫 getPostData() 拿資料回來

import axios from "axios";

export default {
  name: "Home",
  data() {
    return {
      postData: [],
    };
  },
mounted() {
    this.getPostData();
  },
methods: {
  async getPostData() {
    await axios
      .get("https://3000.gov.tw/hpgapi-openmap/api/getPostData")
      .then((response) => {
        this.postData = response.data;
        console.log(this.postData);
      })
      .catch((error) => {
        console.log(error);
      });
    },
  }
}

4. Import OpenStreetMap & Leaflet

完成下拉選單及串接 API 後就可以來渲染地圖的部份,這邊使用的是 OpenStreetMap,這是一套開源的地圖工具,並且是免費的,這邊需要搭配 Leaflet 一起使用

首先先裝 Leaflet 到我們的專案中並在組件引入

npm install leaflet
import L from "leaflet";

並加入需要的 CSS,加入到 public/index.html 中

 <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
   integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
   crossorigin=""/>

5. 渲染地圖

在完成 Leaflet 的前置作業後,我們就可以先來測試渲染地圖出來看看,我們要先在網頁模版中加入容器,並記得加上容器的高度,否則是看不到的

 <div id="map"></div>

<style>
  #id {
    height: 500px;
  }
</style>

接著先建立一個變數 openStreetMap

let openStreetMap = {};

export default {
  name: "Home",
  data() {
    return {
      cityName,
      postData: [],
      select: {},
      userData: {},
    };
  },

建立一個初始化 Function 在 mounted() 中

  mounted() {
      openStreetMap = L.map("map", {
        center: [25.0401, 121.5120],
        zoom: 13,
      });

      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution:
          'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
        maxZoom: 20,
      }).addTo(openStreetMap);
  }

這時重新整理就可以看到地圖正確渲染出來,並定位到指定經緯度上

6. 建立地標到地圖中

首先先在 computed 計算屬性中,加入一個方法來篩選資料,這邊使用到 Array.prototype.filter(),在 post 這個 callback 加入判斷如果前端有沒有選擇地區的話,就回傳 post.hsnNm 等於 select.city 的元素並建構成新的陣列,反之回傳地區的元素並建構新的陣列

  computed: {
    postOffice() {
      return this.postData.filter((post) => {
        if (!this.select.area) {
          return post.hsnNm === this.select.city;
        }
        return post.townNm === this.select.area;
      });
    },
  },
  watch: {
    postOffice(val) {
      this.updateMap();
    },
  },
  methods: {
    updateMap() {
      // clear markers
      openStreetMap.eachLayer((layer) => {
        if (layer instanceof L.Marker) {
          openStreetMap.removeLayer(layer);
        }
      });
      // add markers
      this.postOffice.forEach((post) => {
        L.marker([post.latitude, post.longitude]).addTo(openStreetMap)
          .bindPopup(`<p><strong style="font-size: 20px;">${
          post.storeNm
        }</strong></p>
          <strong style="font-size: 16px; color: #d45345;">三倍券剩餘:${
            post.total ? `${post.total} 張` : "未取得資料"
          }</strong><br>
          地址: ${post.addr}<br>
          電話: ${post.tel}<br>
          <small>最後更新時間: ${post.updateTime}</small>`);
      });
    },
  }

接著配合 watch 來追蹤 postOffice() 的值是否有變化,有變化的話就依據新的資料建立地標到地圖中

7. 取得使用者的座標

在上面我們的寫法是將預設座標經緯度固定了,現在我們希望使用者在進入網頁時會跳出詢問視窗,是否授權給予座標資訊

我們在 methods 中加入一個 getCurrent 方法,當使用者同意給予座標資訊後會將經緯度綁定到 userData.longitude 及 userData.latitude 並 呼叫 createMap() 定位到使用者的經緯度上,反之使用者拒絕或獲取位置失敗會自訂一個位置並呼叫 createMap() 來定位

  mounted() {
    this.getPostData();
    this.getCurrent();
  },
  methods: {
    getCurrent() {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          //locationSuccess
          (position) => {
            this.userData.longitude = position.coords.longitude;
            this.userData.latitude = position.coords.latitude;
            this.createMap();
          },
          //locationError
          (error) => {
            this.userData.latitude = 25.042474;
            this.userData.longitude = 121.513729;
            this.createMap();
          }
        );
      }
    },
    createMap() {
      openStreetMap = L.map("map", {
        center: [this.userData.latitude, this.userData.longitude],
        zoom: 13,
      });

      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution:
          'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
        maxZoom: 20,
      }).addTo(openStreetMap);
    },
  }

一開始我們是將渲染地圖的方法直接寫在 mounted() 中,現在記得要搬回 methods 中並獨立成一個方法

8. 總結

雖然我已經把三倍券綁定在信用卡上了 XD 無法參與到領實體券,但至少有參與製作三倍券地圖,稍稍彌補了之前沒有練習到口罩地圖的遺憾,雖然這個程式很陽春,但也可以熟悉 Array 的常用方法及使用 Leaflet 的方式,這在未來實作時也會時常用到,也是很不錯的練習 :)

References

Vue.js 新手如何製作口罩地圖?一起來貢獻小小力量吧!|專欄文章|五倍紅寶石

Leave a comment