超簡單學習 Vue.js 系列 | Vue Gym Leaders 道館排版


  1. 道館排版 Vue Multiway Buttons
  2. 開發說明
    1. 環境設定
    2. 專案結構
    3. Google Font API
    4. Router Link Active
    5. Flexbox Order
    6. Vue Components
      1. Components 使用 objects 而非 array
      2. 將資料傳入 Componenets
      3. Props 轉 data
    7. Dynamic Image Src
    8. Python Get Images
    9. Bootstrap 5 Layout

Vue.js 簡單的示範專案,本次是為練習使用 Bootstrap 5, Flexbox 等 CSS 技巧,並混合資料處理以及 Vue 的框架使用,使用 Vue CLI 實作將所學習的知識以及實作需要的功能,藉此將知識實際結合,記得牢、想得到、用得出來。

logo

道館排版 Vue Multiway Buttons

技術關鍵字 說明
Vue Components Reuse Templates Pass Data By Props
Flexbox Order order-md-1 搭配 Vue Dynimac CSS
Vue Router Active Link .router-link-active
Requests Data Python Request Get Images
Google Font API Press Start 2P
Dynamic Image Src Require Resources
Bootstrap 5 Layout container-fluid, m-auto

開發說明

環境設定

本次的專案使用 Vue CLI 開發,並且另外安裝 Bootstrap 5 用於協助版面設計。Vue CLI 選用 Vue3 / Vue-Router / Babel。

vue create
npm install bootstrap

專案結構

├─components
│      LeaderPokeBoxes.vue
│
├─data
│      leaders.json
│
├─router
│      index.js
│
├─views
│       Generation1.vue
│       Home.vue
│       Letsgo.vue
|  App.vue
│  main.js
│
├─assets

主要的設計在於 LeaderPokeBoxes.vue,其中 leaders.json 是從 bulbagarden手工整理的資料。

Google Font API

為了要讓整個網頁更有電玩效果,使用 Google Font API 將網頁的字型改為 Press Start 2P,視覺效果超好 😁

在 App.vue 的 Style 引入,讓全域的 Vue Componenet 都受此影響

@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');

#app {
  font-family: 'Press Start 2P', sans-serif, Avenir, Helvetica, Arial, sans-serif;
}

要處理導覽按鈕列的按鈕 Active 效果,在 Vue-Router 點選的 Route 會自動加上 router-link-active CSS Class,因此可以直接藉由定義此 CSS Class 來達到 Active 的按鈕的效果,這邊使用的方式就是比照 Bootstrap btn-primary 的 CSS,但未針對 focus 的 pseudo selector 去做處理。

.router-link-active:hover, .router-link-active:focus, .router-link-active {
  color: #fff;
  background-color: #0d6efd;
  border-color: #0d6efd;
}

Flexbox Order

為了要讓版面排版有交錯的效果,使館主與所持有的隊伍左右、右左、左右的方式交互排列。使用 Flexbox 的 order 輕鬆達成。讓道館主的 Html Tag 加上 Dynamic CSS,判斷目前 v-for 的 index,來達到交錯的效果。

此外搭配 Bootstrap 的 RWD breakpoints 只又在 medium 以上才顯示交錯效果,否則在寬度小的裝置上,不會是左右排列,故不需要交錯的顯示方式。

<template v-for="(leader, index) in leadersData" :key="leader">
<div class="row mb-5 align-items-center p-3">
    <div class="col-md-4" :class="{ 'order-md-1': index % 2 === 1}">
        ...
    </div>
    <div class="col-11 col-md-8 border shadow pb-5 m-auto rounded-3">
        ...
    </div>
</div>
</template>

Vue Components

隨著專案的規模增加,利用 Componenets 的方式切割關注點,會讓開發與維護更為方便。但實際在使用 Components 的時候卡 Bug 一小段時間,特此筆記。

Components 使用 objects 而非 array

因為 Componenets 是必須註冊於 Root Element 上,所以是以物件的形式進行註冊,物件的內容包含 Template, Props 等資訊。

import LeaderPokeBoxes from '../components/LeaderPokeBoxes'

// ✔️
export default {
    name: "Generation1",
    components: { LeaderPokeBoxes },
}

// ❌
export default {
    name: "Generation1",
    components: [ LeaderPokeBoxes ],
}

將資料傳入 Componenets

<template>
  <h1 class="mb-5">Generation 1</h1>
  <LeaderPokeBoxes :leadersData="leaders['Gen1']"/>
</template>

Props 轉 data

Props 可以直接再 Compoenents 的資料中使用,不需要透過 Data function,但如果要轉換,必須搭配 This 使用。

Componenets\LeaderPokeBoxes.vue

<template v-for="(leader, index) in leadersData" :key="leader">
<div class="row mb-5 align-items-center p-3">
    <div class="col-md-4" :class="{ 'order-md-1': index % 2 === 1}">
        ...
    </div>
    <div class="col-11 col-md-8 border shadow pb-5 mx-auto rounded-3">
        ...
    </div>
</div>
</template>
export default {
    name: 'LeaderPokeBoxes',
    props: ['leadersData'],
    data() {
      return {
        dataFromProps: this.leadersData
      }
    }
}

結論:使用 Vue Componenets 先用靜態 Components 再使用 Props 減少除錯時間。

Dynamic Image Src

為了要讓圖片路徑從 Json 當中讀取,並且動態的綁訂在 Image Tag 上,並須使用 Vue Bind 語法,並且再搭配 require function 的方式將資源載入。

但目前有一個問題是,如果圖片檔不存在,會直接讓 Vue CLI 專案無法通過,如果有 Missing Image 的替代圖片讓專案仍可以正常運作會是更好的方式。

<img alt="Gym Leader" :src="require('../assets/' + leader['leader-name'] + '.png')"
 class="gym-leader">

Python Get Images

import requests
import os

headers = {
    'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36"
}

imgBanks = ['https://.../bulbasaur.png',]


os.chdir(r'D:\pokes')

for url in imgBanks:
  img = requests.get(url)
  if img.status_code != 200:
      print(url)
  else:
    extension = url.split('.')[-1]
    name = url.split('/')[-1].replace('.png', '')
    with open(f"{name}.{extension}", "wb") as f:
        f.write(img.content)
        f.close()

Bootstrap 5 Layout

利用 container-lg 來達到 RWD 的技巧,因為 container CSS Class 會限制 Div 的最大 Width,來達到左右留白的效果,但在螢幕寬度不足的情況下,不需要這個留白,因此預設 container-fluid 只又當螢幕尺寸達 992 px 以上時,才限制最大寬度。

參考 Boostrap 5 的 Souce,Container 的留白如下

container size width max-width
sm 576 540
md 768 720
lg 992 960
xl 1200 1140
xxl 1400 1320
<div class="container-fluid container-lg">
  ...
</div>

本次的 Layout 核心就是下列的 Html 以及 CSS 組合。每 Row 代表的是一位 Leader,並且用 Col 切割為 4 Col 與 8 Col,但在 Medium 寬度之下的裝置,則是單欄顯示,但是限制 PokeBox 只佔 11 Col 用以達到稍微置中與留白的效果。

最外層的 Row 本身就是 Flex,使用 align-items-center 可以讓 leader-img 與 pokeBoxs 任一方高度較少者置中對齊另一方,預設是 Stretch,另可考慮 Start 或者 End,但本次設計是採用 Center 的方式。

而在 PokeBoxs 上,加上了 shadow 讓整個版面立體起來,並且加上 mx-auto 讓行動裝置等寬度未達 Medium 以 11 Col 顯示時,能夠自動左右 Margin 達到置中效果,以讓 Shadow 效果可以完整呈現。另外使用了 pb-5 來向下內距撐出高度,並使用 rounded-3 來修飾圓角 (Radius) 增加活潑的效果 🙂

PokeBoxes 中採用了 Nested Row 來排版 Poke 的顯示方式,預設是 6 Col,所以行動裝置每列只會顯示兩個 Pokes,但 Large 以上螢幕寬度的裝置則可以顯示三個 Pokes。

<div class="row mb-5 align-items-center p-3">
    <div class="col-md-4" :class="{ 'order-md-1': index % 2 === 1}">
        <img alt="Gym Leader" :src="leader.img" class="gym-leader">
        <h2 class="gym-leader-name">{{ leader['leader-name'] }}</h2>
    </div>
    <div class="col-11 col-md-8 border shadow pb-5 mx-auto rounded-3">
        <div class="gym-name">{{ leader['gym-name'] }}</div>
        <div class="row">
            <div class="col-6 col-lg-4" v-for="poke in leader['pokemons']" :key="poke">
                <img :src="poke.name" class="img-fluid">
                <div class="poke-name">{{ poke.name }}</div>
                <div>Lv.{{ poke.lv }}</div>
            </div>
        </div>
    </div>
</div>