還記得 Day 06 的標題 – 「我把圖片內嵌在 HTML 了!」嗎?
沒錯,SVG 能像 HTML 標籤一樣拆開來寫。以此類推,我們也能在前端框架的 HTML 區塊加上 inline SVG,用畫面控制的語法,輕鬆簡單地操作圖形內容。
今天來示範如何用 Vue 製作一個可以重複使用的 SVG 元件,我們開始吧!
事出必有因,來看看模擬設計稿,是不是有益智遊戲的即視感?

仔細看這些圖形,會發現它們其實規律:有明顯的順序、都是 3×3 的格子。差別只在於「哪幾格有出現」和「哪幾格是亮色」。
實際這組圖形總共有 30 種,如果按照一般做法,匯出 30 個 SVG 檔案,可能會碰到幾個問題:
我當時覺得既然有規則,那是不是就能讓程式來畫咧? 🤔
而且專案中至少六處會用到,做成元件後這些地方都能引用。不用重寫,豈不美哉?
思考如何讓圖形能在不同畫面中使用,有兩點要注意:
尺寸彈性:同一個圖示可能出現在列表裡、也可能出現在滿版畫面中。由於 SVG 是向量圖形,只要保留 viewBox、讓 width、height 交給外部決定,放大縮小都能清晰顯示。
prop 設計:元件要接收什麼資料。有兩種方向:
| 設計方式 | 範例 | 優點 | 缺點 |
|---|---|---|---|
| 傳編號 | <StarGrid :value="12" /> |
使用端最簡單 | 元件內要自己把數字換算成畫面 |
| 傳格子設定 | <StarGrid :cells="['t1', 'm2']" /> |
元件單純,只負責畫 | 每個使用端都要知道格子代號和規則 |
最後選擇只接收數字。顯示邏輯都放在元件內,父元件只需要知道要顯示編號幾的圖示。讓父層的程式碼保持乾淨,需要修改規則的時候,也只要改一個地方就好。
第一步是觀察設計稿,找出變化的規則。為了方便閱讀,我幫每一格取一個代號,用「排」加「位置」組合。
T1 T2 T3 ← top
M1 M2 M3 ← middle
B1 B2 B3 ← bottom
圖形的顯示規則如下:
接著從設計稿匯出完整形狀的圖形。每一格都是一個獨立的標籤:
<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決定
地基建立好了,接下來就是決定「每一格什麼時候要出現」。把前面整理的規則轉換成程式,可以發現就是把數字拆成十位數和個位數:
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 格逐一寫出來。是因為希望每一格的位置一眼就看得出來,之後需要微調某一格,直接改那一行就好。
最後把規則和 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!
template 和規則本身都不用動,引用的地方也不用動。
把 SVG 當成標籤來思考,先找出圖案的規則,再封裝成元件,圖案就從一張張圖檔,變成了一段邏輯。
.
.
.
這幾天陸續用 CSS 、HTML 來操作 SVG 圖形,是時候輪到 JavaScript 了。介紹如何用 JavaScript 直接操作 <path> 本身,讓圖形能動態改變形狀。我們明天見!