iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Modern Web

從實務需求出發:前端視覺與 UI 互動開發實踐系列 第 9 篇

【Day 09】SVG 實作:化繁為簡,圖形元件化

  • 分享至 

  • xImage
  •  

還記得 Day 06 的標題 – 「我把圖片內嵌在 HTML 了!」嗎?
沒錯,SVG 能像 HTML 標籤一樣拆開來寫。以此類推,我們也能在前端框架的 HTML 區塊加上 inline SVG,用畫面控制的語法,輕鬆簡單地操作圖形內容。

今天來示範如何用 Vue 製作一個可以重複使用的 SVG 元件,我們開始吧!

行前說明

幹嘛這樣做?

事出必有因,來看看模擬設計稿,是不是有益智遊戲的即視感?

https://ithelp.ithome.com.tw/upload/images/20260923/20184345zyX09360aW.png

仔細看這些圖形,會發現它們其實規律:有明顯的順序、都是 3×3 的格子。差別只在於「哪幾格有出現」和「哪幾格是亮色」。

實際這組圖形總共有 30 種,如果按照一般做法,匯出 30 個 SVG 檔案,可能會碰到幾個問題:

  • 檔案多,命名和管理都麻煩
  • 設計稿只要改一個顏色或規則,30 個檔案都要重新匯出
  • 圖檔本身看不出圖案之間的規則,之後要新增也只能再請設計師出圖

我當時覺得既然有規則,那是不是就能讓程式來畫咧? 🤔
而且專案中至少六處會用到,做成元件後這些地方都能引用。不用重寫,豈不美哉?

元件規劃

思考如何讓圖形能在不同畫面中使用,有兩點要注意:

  1. 尺寸彈性:同一個圖示可能出現在列表裡、也可能出現在滿版畫面中。由於 SVG 是向量圖形,只要保留 viewBox、讓 width、height 交給外部決定,放大縮小都能清晰顯示。

  2. prop 設計:元件要接收什麼資料。有兩種方向:

設計方式 範例 優點 缺點
傳編號 <StarGrid :value="12" /> 使用端最簡單 元件內要自己把數字換算成畫面
傳格子設定 <StarGrid :cells="['t1', 'm2']" /> 元件單純,只負責畫 每個使用端都要知道格子代號和規則

最後選擇只接收數字。顯示邏輯都放在元件內,父元件只需要知道要顯示編號幾的圖示。讓父層的程式碼保持乾淨,需要修改規則的時候,也只要改一個地方就好。

實作

1. 找出規則,打造基底

第一步是觀察設計稿,找出變化的規則。為了方便閱讀,我幫每一格取一個代號,用「排」加「位置」組合。

T1  T2  T3    ← top
M1  M2  M3    ← middle
B1  B2  B3    ← bottom

圖形的顯示規則如下:

  • 圖形內的元素固定 3 * 3
  • 個位數 1~4:藍色星星依 T1 → T3 → B1 → B3 順序出現
  • 5:星星清空,改成正中間一顆月亮;6~9 再依相同順序加上藍星
  • 十位數:每多 10 多一顆黃星,依 T2 → M1 → M3 順序出現

接著從設計稿匯出完整形狀的圖形。每一格都是一個獨立的標籤:

<svg viewBox="0 0 88 82" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <path
      id="star"
      d="M12 1.8 L15.2 8.2 L22.3 9.3 L17.1 14.3 L18.3 21.3 L12 18 L5.7 21.3 L6.9 14.3 L1.7 9.3 L8.8 8.2 Z"
      fill="currentColor"
      stroke="currentColor"
      stroke-width="2"
      stroke-linejoin="round"
    />
  </defs>

  <!-- 第一排 -->
  <use href="#star" x="0"  y="0" color="#1e40af" />
  <use href="#star" x="32" y="0" color="#facc15" />
  <use href="#star" x="64" y="0" color="#1e40af" />

  <!-- 第二排:中間是月亮 -->
  <use href="#star" x="0"  y="29" color="#facc15" />
  <path
    transform="translate(32 29)"
    d="M21 12.8 A9 9 0 1 1 11.2 3 A7 7 0 0 0 21 12.8 Z"
    fill="#facc15"
  />
  <use href="#star" x="64" y="29" color="#facc15" />

  <!-- 第三排 -->
  <use href="#star" x="0"  y="58" color="#1e40af" />
  <use href="#star" x="32" y="58" color="#facc15" />
  <use href="#star" x="64" y="58" color="#1e40af" />
</svg>

範例 SVG 調整了幾處:

  • 設計稿匯出的 SVG 會有 width 與 height 設定。記得要移除,才能自適應父層給的容器空間。
  • 星星只在 <defs> 定義一次,每一格用 <use> 引用,就不用重複寫 8 次路徑
  • 星星的 fill、stroke 設成 currentColor,顏色交給每個 <use> 的 color 決定

2. 拆分元件,設定渲染邏輯

地基建立好了,接下來就是決定「每一格什麼時候要出現」。把前面整理的規則轉換成程式,可以發現就是把數字拆成十位數和個位數:

const tens = Math.floor(value / 10) // 十位數:幾顆黃星
const ones = value % 10             // 個位數

const hasMoon = ones >= 5           // 5 以上就有月亮
const blueCount = ones % 5          // 扣掉月亮代表的 5,剩下幾顆藍星

再搭配兩個「出現順序」的陣列,取前面幾個,就是這次要顯示的格子:

const BLUE_ORDER = ['t1', 't3', 'b1', 'b3']
const YELLOW_ORDER = ['t2', 'm1', 'm3']

BLUE_ORDER.slice(0, blueCount)  // 例如 3 → ['t1', 't3', 'b1']
YELLOW_ORDER.slice(0, tens)     // 例如 2 → ['t2', 'm1']

每個位置只會屬於其中一種顏色,所以 template 裡每一格的顏色是固定的,只需要用 v-if 判斷要不要出現。

HTML 的部分保持原本 SVG 結構。不用 v-for 迴圈產生格子,而是 9 格逐一寫出來。是因為希望每一格的位置一眼就看得出來,之後需要微調某一格,直接改那一行就好。

3. 用 props 控制顯示內容

最後把規則和 template 組起來,完整的元件如下:

<script setup>
import { computed, useId} from 'vue'

const props= defineProps({
  value: {
    type: Number,
    required: true,
    validator: (v) => Number.isInteger(v) && v >= 1 && v <= 30,
  },
})

const BLUE = '#1e40af'
const YELLOW = '#facc15'

const BLUE_ORDER = ['t1', 't3', 'b1', 'b3']
const YELLOW_ORDER = ['t2', 'm1', 'm3', 'b2']

const tens= computed(() => Math.floor(props.value / 10))
const ones= computed(() => props.value % 10)

const hasMoon= computed(() => ones.value>= 5)
const cells= computed(() => [
  ...BLUE_ORDER.slice(0, ones.value% 5),
  ...YELLOW_ORDER.slice(0, tens.value),
])

const has = (id) => cells.value.includes(id)

// 同一頁放很多個元件時,避免 <defs> 的 id 重複
const starId= `star-${useId()}`
</script>

<template>
  <svg
    viewBox="0 0 88 82"
    xmlns="http://www.w3.org/2000/svg"
  >
    <defs>
      <path
        :id="starId"
        d="M12 1.8 L15.2 8.2 L22.3 9.3 L17.1 14.3 L18.3 21.3 L12 18 L5.7 21.3 L6.9 14.3 L1.7 9.3 L8.8 8.2 Z"
        fill="currentColor"
        stroke="currentColor"
        stroke-width="2"
        stroke-linejoin="round"
      />
    </defs>

    <!-- 第一排 -->
    <use v-if="has('t1')" :href="`#${starId}`" x="0"  y="0" :color="BLUE" />
    <use v-if="has('t2')" :href="`#${starId}`" x="32" y="0" :color="YELLOW" />
    <use v-if="has('t3')" :href="`#${starId}`" x="64" y="0" :color="BLUE" />

    <!-- 第二排:中間是月亮 -->
    <use v-if="has('m1')" :href="`#${starId}`" x="0"  y="29" :color="YELLOW" />
    <path
      v-if="hasMoon"
      transform="translate(32 29)"
      d="M21 12.8 A9 9 0 1 1 11.2 3 A7 7 0 0 0 21 12.8 Z"
      :fill="YELLOW"
    />
    <use v-if="has('m3')" :href="`#${starId}`" x="64" y="29" :color="YELLOW" />

    <!-- 第三排 -->
    <use v-if="has('b1')" :href="`#${starId}`" x="0"  y="58" :color="BLUE" />
    <use v-if="has('b2')" :href="`#${starId}`" x="32" y="58" :color="YELLOW" />
    <use v-if="has('b3')" :href="`#${starId}`" x="64" y="58" :color="BLUE" />
  </svg>
</template>

幾個細節:

  • 聲明 value 只接受 1~30 的整數:用 validator 擋掉不合法的值,開發時傳錯就會在 console 看到警告
  • useId():同一頁放了很多個圖示時,如果每個都寫 id="star",所有 <use> 都會指向頁面上的第一個。用 Vue 3.5 的 useId() 讓每個元件的 id 都不一樣

使用起來非常單純,只要給數字:

<StarGrid :value="1" />

完成之後

這個元件剛做完沒多久,更動就來了:圖示要從 30 種增加到 40 種。
如果當初是匯出 SVG 檔,這時候就要再請設計師出 10 張圖、匯出、命名、放進專案 @@

好在有超前部署,規則都已經寫好在元件裡。只需要增加支援編號 40 的地方就好了:

const YELLOW_ORDER= ['t2', 'm1', 'm3', 'b2']  // 新增 'b2'

validator: (v) => Number.isInteger(v) && v >= 1 && v <= 40,  // 上限改成 40

nice
Nice!

template 和規則本身都不用動,引用的地方也不用動。
把 SVG 當成標籤來思考,先找出圖案的規則,再封裝成元件,圖案就從一張張圖檔,變成了一段邏輯。


.
.
.

這幾天陸續用 CSS 、HTML 來操作 SVG 圖形,是時候輪到 JavaScript 了。介紹如何用 JavaScript 直接操作 <path> 本身,讓圖形能動態改變形狀。我們明天見!


上一篇
【Day 08】 SVG 實作:是漸層,我加了漸層
下一篇
【Day 10】SVG 實作:路線圖(上) 在 path 上找一個家
系列文
從實務需求出發:前端視覺與 UI 互動開發實踐 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言