iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Modern Web

30天30個前端任務系列 第 5

#4. Covid 19 Tracker(Vue版)

今天任務的實作內容,主要是參考這支影片
影片中使用的程式碼風格是Vue的Option API,在我的專案中則是換成Composition API。Demo Link

Vue Components 規劃

https://ithelp.ithome.com.tw/upload/images/20210905/20130534pp2nSYpfEJ.png

以下來看每個組件的程式碼

Covid19Tracker.vue

// src/views/Covid19Tracker.vue
<template>
  <home-btn/>
  <div class="mt-20 bg-gray-200 h-full w-[80vw] shadow-md rounded-md">
    <!-- Header -->
    <header class="w-full text-center bg-green-800 text-white p-4 mb-10 rounded-t-md">
      <div class="relative flex flex-row justify-center text-3xl md:text-5xl font-bold mb-3">
        <Icon icon="fa-solid:viruses" class="mr-2 pb-1" height="36"/>
        <p>Covid-19 Tracker</p>
      </div>
      <p>API By <a class="text-blue-300" href="https://covid19api.com" target="_blank">covid10api.com</a></p>
    </header>

    <!-- Main -->
    <main class="p-10 pt-1" v-if="!loading">
      <DataTitle :dataDate="dataDate" :text="title" />
      <DataBoxes :stats="status" />
      // 將get-country屬性綁定函式getCountryData,然後將該屬性送給子組件
      <CountrySelect :countries="countries" @get-country="getCountryData" />

      <button
        v-if="status.Country"
        class="bg-blue-700 text-white rounded p-3 mt-10 focus:outline-none hover:bg-blue-500"
        @click="clearCountryData"
      >
        Clear Country
      </button>
    </main>

    <main v-else class="flex flex-col align-center justify-center text-center pb-10">
      <div class="text-gray-600 text-3xl m-10">
        Fetching Data...
      </div>
    </main>

  </div>
</template>

<script>
import { Icon } from '@iconify/vue'
import HomeBtn from '../components/HomeBtn.vue'
import CountrySelect from '../components/covid19-tracker/CountrySelect.vue'
import DataBoxes from '../components/covid19-tracker/DataBoxes.vue'
import DataTitle from '../components/covid19-tracker/DataTitle.vue'
import { ref } from 'vue'

export default {
  name: 'Covid 19 Tracker',
  components: {
    Icon,
    HomeBtn,
    DataTitle,
    DataBoxes,
    CountrySelect
  },
  setup () {
    // 透過 ref()來包裝原始型別、物件、陣列
    const loading = ref(true)
    const title = ref('Global')
    const dataDate = ref('')
    const status = ref({})
    const countries = ref([])

    // 透過api取得資料
    const fetchCovidData = async () => {
      const res = await fetch('https://api.covid19api.com/summary')
      return await res.json()
    }

    // 取得特定國家的疫情資料
    const getCountryData = country => {
      status.value = country
      title.value = country.Country
    }

    // 重新透過api取得資料,然後將value改為Global
    const clearCountryData = async () => {
      loading.value = true
      const data = await fetchCovidData()
      title.value = 'Global'
      status.value = data.Global
      loading.value = false
    }

    const baseSetup = async () => {
      const data = await fetchCovidData()
      dataDate.value = data.Date
      status.value = data.Global
      countries.value = data.Countries
      loading.value = false
    }
    // 呼叫函式baseSetup來調用fetchCovidData
    baseSetup()

    // 回傳定義好的資料,渲染到template上的子組件
    return {
      loading,
      title,
      dataDate,
      status,
      countries,
      getCountryData,
      clearCountryData
    }
  }
}
</script>

DataTitle.vue

// src/components/covid19-tracker/DataTitle.vue
<template>
  <div class="text-center">
    <h2 class="text-3xl font-bold">{{ text }}</h2>
    <div class="text-2xl mt-4 mb-10">
      {{ timestamp }}
    </div>
  </div>
</template>

<script>
import dayjs from 'dayjs'
import { computed } from 'vue'

export default {
  name: 'DataTitle',
  props: ['text', 'dataDate'],
  setup (props) {
    const { dataDate } = { props }
    return {
      // 透過day.js顯示日期
      timestamp: computed(() => dayjs(dataDate).format('MMMM D YYYY, h:mm:ss a'))
    }
  }
}
</script>

<style>
</style>

DataBoxes.vue

// src/components/covid19-tracker/DataBoxes.vue
<template>
<div class="grid md:grid-cols-2 gap-4">
  <!-- box1 -->
  <div class="shadow-md bg-blue-100 p-10 text-center rounded">
    <h3 class="text-3xl text-blue-900 font-bold mb-4">Cases</h3>
    <div class="text-2xl mb-4">
      <span class="font-bold">New:</span>
      {{ numberWithCommas(stats.NewConfirmed) }}
    </div>
    <div class="text-2xl mb-4">
      <span class="font-bold">Total:</span>
      {{ numberWithCommas(stats.TotalConfirmed) }}
    </div>
  </div>
  <!-- box2 -->
  <div class="shadow-md bg-blue-200 p-10 text-center rounded">
    <h3 class="text-3xl text-blue-900 font-bold mb-4">Deaths</h3>
    <div class="text-2xl mb-4">
      <span class="font-bold">New:</span>
      {{ numberWithCommas(stats.NewDeaths) }}
    </div>
    <div class="text-2xl mb-4">
      <span class="font-bold">Total:</span>
      {{ numberWithCommas(stats.TotalDeaths) }}
    </div>
  </div>
</div>
</template>

<script>
export default {
  name: 'DataBoxes',
  // 從父組件取得stats資料
  props: ['stats'],
  setup () {
    return {
      numberWithCommas (x) {
        return x.toString()
        // 用正規表達式將數字標記千位、百萬位...
          .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
      }
    }
  }
}
</script>

CountrySelect.vue

// src/components/covid19-tracker/CountrySelect.vue
<template>
  <select @change="onChange"
    v-model="selected"
    class="form-select mt-10 block w-full border p-3 rounded outline-none cursor-pointer"
  >
    <!-- selected最初的value是0,因此最初會先顯示Select Country -->
    <option value="0">Select Country</option>
    <!-- 用v-for迴圈將countries陣列渲染出來 -->
    <option v-for="country in countries" :value="country.ID" :key="country.ID">
      {{ country.Country}}
    </option>
  </select>
</template>

<script>
import { ref } from 'vue'

export default {
  name: 'CountrySelect',
  props: ['countries'],
  setup (props, { emit }) {
    const selected = ref(0)
    return {
      selected,
      onChange () {
        // 透過array.find比對出ID與selected.value相符的資料
        const country = props.countries.find((item) => item.ID === selected.value)
        // 使用從父組件送來的屬性get-country,該屬性會呼叫父組件的函式getCountryData,然後將country作為參數引用之。
        emit('get-country', country)
      }
    }
  }
}
</script>

同樣是這個Covid 19 Tracker,在我的一篇筆記有紀錄Vue的程式碼風格,依序練習自Option API、Composition API到Script Setup版本。

心得

  1. 用過Vue 3的Composition API就不太想回去Option API了。
  2. 這次使用的API的資料更新速度比較慢,下次會改用disease.sh來玩玩看。

明日任務

  1. Q&A區塊效果(點擊Q會展開A)
  2. 圖片模糊載入效果(Vue版)
  3. 卡片擴展效果(Vue版)

上一篇
#3. Expanding Cards(原生JS版)+ 用tailwindcss玩grid排版
下一篇
#5. Q&A Section(原生JS版)、#2. Blurring Loading(Vue版)、#3. Expanding Cards(Vue版)
系列文
30天30個前端任務19

尚未有邦友留言

立即登入留言