iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Modern Web

微光採集瓶:用 Vue 3 打造每日美好記錄網頁系列 第 10 篇

【Day 10】列表渲染:v-for 與光珠可視化

  • 分享至 

  • xImage
  •  

昨天我們在 Console 印出了一筆完整的紀錄,但那筆資料只是被印在 Console 裡,畫面上完全沒有反映,瓶子還是空的,數字也只是單純計數。

今天,我們要讓資料真正呈現在畫面上:把 count 升級成存放光珠資料的陣列,讓瓶子裡真的冒出一顆顆對應顏色的光珠。

一、資料結構升級:從 count 到 gems

先回顧一下,目前 GemJar.vue 裡負責計數的變數:

const count = ref(0)

count 只是一個數字,它只能告訴我們「採集了幾次」,沒辦法告訴我們「每一次採集的內容是什麼」,哪一天選了什麼顏色、寫了什麼話,這些細節全部都不會留存。

今天我們要把它換成一個陣列,陣列裡的每一筆資料,都是一個完整的「光珠物件」:

const gems = ref([])

gems 現在是一個空陣列,之後每採集一次,我們就會往裡面放進一個新的物件,長得像這樣:

{
  id: 1,
  color: '#E3B341',
  note: '今天買了喜歡的積木'
}
  • id:這顆光珠的專屬身分證(等一下會解釋為什麼需要它)
  • color:這顆光珠的顏色(對應到我們 Day8 做的心情調色盤)
  • note:這顆光珠裝著的那句話(對應到 Day9 的文字輸入框)

二、修改 addGem:把資料存進陣列

打開 src/components/GemJar.vue,把 addGem 函式改成這樣:

function addGem() {
  if (todayNote.value.trim() === '') {
    alert('請先寫下一件今天的美好小事喔!')
    return
  }

  // 1. 組成一顆新的光珠物件
  const newGem = {
    id: Date.now(),
    color: selectedColor.value,
    note: todayNote.value
  }

  // 2. push 進陣列裡
  gems.value.push(newGem)
  // 3. 確認資料有寫入陣列
  console.log('目前所有光珠:', gems.value)

  isGlowing.value = true

  todayNote.value = ''

  setTimeout(() => {
    isGlowing.value = false
  }, 1000)
}

這裡有兩個新東西:

1. id: Date.now()

Date.now() 會回傳「現在的時間」轉換成的一串數字。因為每次呼叫的當下時間不可能一樣,這串數字幾乎不會重複,剛好可以拿來當作每顆光珠獨一無二的身分證。

2. gems.value.push(newGem)

push() 是 JavaScript 陣列原生的方法,作用是「把新的東西加到陣列最後面」。因為 gems 是用 ref([]) 包起來的響應式陣列,Vue 會自動偵測到陣列內容變了,畫面也會跟著自動更新。

記得把原本頂部的 const count = ref(0) 刪除,並在 addGem() 函式裡拿掉原本的 count.value++,因為現在光珠數量直接讀取 gems.value.length 即可。

三、v-for:把每一顆光珠畫出來

現在資料有了,接下來要讓畫面知道「怎麼把這些資料一個一個畫出來」。這就是今天的主角——v-for。

v-for 是 Vue 用來做列表渲染的指令,官方定義是:「基於一個陣列(或物件),依序渲染一個列表」。
找到瓶身的部分,改成這樣:

<div class="bottle" :class="{ 'is-glowing': isGlowing }">
  <span v-if="gems.length === 0">瓶子目前是空的</span>

  <div class="gem-pile">
    <div
      v-for="gem in gems"
      :key="gem.id"
      class="gem-dot"
      :style="{ backgroundColor: gem.color }"
    ></div>
  </div>
</div>
  • v-for="gem in gems":意思是「把 gems 這個陣列裡的每一筆資料,取出來、輪流叫做 gem,重複畫出下面這段 HTML」。有幾顆光珠,畫面上就會出現幾個 .gem-dot。
  • :key="gem.id":這是 v-for 的必要搭檔,等一下會詳細解釋。

同時,把原本的空瓶提示,從判斷 count === 0 改成判斷 gems.length === 0,因為現在真正代表「有沒有光珠」的資料,是 gems 這個陣列的長度,不再是 count 這個數字了。

四、為什麼一定要加 :key?

你可能會好奇,:key="gem.id" 這行到底在做什麼,拿掉會怎樣?

想像一下,v-for 每次重新渲染畫面時,Vue 都需要知道:「這次的第一顆光珠,跟上一次的第一顆光珠,是不是同一顆?」如果不給 Vue 一個明確的依據,Vue 只能用「順序位置」去猜(例如「反正都是排在第一個,就當作是同一顆」)。

這在資料只會往後新增的情況下,通常還算安全,但只要之後如果有「刪除某一顆光珠」或「重新排序」的需求,用位置去猜就會出錯。Vue 可能會誤判,把 B 光珠的資料,錯誤地套用到 A 光珠原本的畫面位置上,導致顏色、內容全部對不起來。

:key 就是解決這個問題的方法:給每一顆光珠一張獨一無二的身分證(也就是我們剛剛在 addGem 裡準備好的 id)。這樣不管陣列裡的順序怎麼變化,Vue 都能透過這張身分證,精準地認出「這一顆,就是原本的那一顆」,不會認錯人。

這也是為什麼,:key 不能隨便拿陣列的索引值來充當。因為索引值本身就是「順序位置」,用它當身分證,等於完全沒有解決問題。一定要用資料裡真正獨一無二的欄位,例如我們這裡的 id。

五、更新採集數量的顯示

最後,把原本讀取 count 的地方,改成讀取 gems.length(陣列的長度,也就是目前有幾顆光珠):

<p>已採集 {{ gems.length }} 顆光珠</p>

六、補上光珠的樣式

在 <style scoped> 最下面,加上光珠本身的外觀,還有裝著光珠的容器排版:

/* 裝著所有光珠的容器 */
.gem-pile {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 12px;
  align-items: flex-end;
  justify-content: center;
}

/* 單顆光珠 */
.gem-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  box-shadow: 0 0 6px rgba(255, 255, 255, 0.3);
}

七、問題修正

由下圖可以看出瓶子中間那行提示:「瓶子目前是空的」沒有置中,所以需要將程式碼再調整一下。
https://ithelp.ithome.com.tw/upload/images/20260924/20178067S5l9ZBsHoo.png

問題出在哪裡?

看到.bottle的部分:

<div class="bottle" :class="{ 'is-glowing': isGlowing }">
  <span v-if="gems.length === 0">瓶子目前是空的</span>

  <div class="gem-pile">
    <div v-for="gem in gems" ... ></div>
  </div>
</div>

.bottle 本身是 display: flex; justify-content: center;(預設 flex-direction: row,也就是水平排列)。現在瓶子裡同時存在兩個子元素:<span>(提示文字)跟 <div class="gem-pile">(裝光珠的容器)。

即使目前 gems 是空陣列、gem-pile 裡面沒有任何光珠,這個 <div class="gem-pile"> 本身依然存在(因為它沒有被 v-if 控制),而且我一開始設了 padding: 12px,所以它雖然看起來是空的,實際上還是佔了一點空間。

justify-content: center 這個屬性,作用是「把所有子元素當成一整排,置中對齊」——不是「把每個子元素個別置中」。所以現在的狀況是:<span> 文字 + 那個空的 gem-pile 方塊,兩個東西被當作一組,一起置中,文字自然就被推偏到左邊去了。

修正方式:讓空瓶提示跟光珠容器「互斥」,同一時間只存在一個

在 gem-pile 也加上一個條件判斷,讓它只在「有光珠」的時候才出現:

<div class="bottle" :class="{ 'is-glowing': isGlowing }">
  <span v-if="gems.length === 0">瓶子目前是空的</span>

  <div v-else class="gem-pile">
    <div
      v-for="gem in gems"
      :key="gem.id"
      class="gem-dot"
      :style="{ backgroundColor: gem.color }"
    ></div>
  </div>
</div>
  • 改動只有一個地方:把 <div class="gem-pile"> 前面加上 v-else。

為什麼這樣就能解決

v-if 跟 v-else 是一組的,效果是「兩者只會有一個真正被畫進 DOM 裡」,當 gems.length === 0 時,只有 <span> 存在,gem-pile 完全不會被渲染;當開始有光珠之後,<span> 消失,換成 gem-pile 出現。這樣同一時間,.bottle 裡永遠只有一個子元素,justify-content: center 才能正確地把它單獨置中。


八、成果驗證

存檔後回到瀏覽器,測試看看:

  1. 選一個顏色、寫一句話,按下「放入這道微光」,確認瓶子裡是不是真的出現了一顆對應顏色的小圓點。
    https://ithelp.ithome.com.tw/upload/images/20260924/20178067QWKeevUca2.png
  2. 多按幾次(記得每次都要重新打字,不然會被防呆擋下來),確認瓶子裡的光珠是不是一顆一顆累積增加,而不是互相覆蓋掉。
    https://ithelp.ithome.com.tw/upload/images/20260924/20178067vL91DLx2e3.png
  3. 確認「已採集 X 顆光珠」的數字,是不是跟畫面上實際看到的光珠數量一致。

如果這三點都正常,代表 gems 陣列、v-for 渲染、:key 身分識別都正確運作了。

小提示
我們有在程式碼中寫一句:console.log('目前所有光珠:', gems.value) ,你可以打開開發人員工具,切換到 Console 看看資料是否有正確寫入。

今日小結

  • 陣列取代單一數值:把 count 升級成 gems,讓資料不只是「數量」,還完整保留了每一次採集的內容。
  • v-for 列表渲染:學會用 v-for="item in list" 把陣列裡的每一筆資料,重複畫成對應的畫面元素。
  • :key 的重要性:理解為什麼每一筆資料都需要一個獨一無二的身分證,讓 Vue 在畫面更新時不會認錯資料。

明日預告

現在瓶子真的會累積光珠了,不過目前所有光珠都只是裡面排排站,明天(Day 11),我們要讓新加入的光珠有動畫效果:帶有一點重量感,從瓶口緩緩落下、停在瓶底。


上一篇
【Day 9】寫下今天的心情!v-model 讓輸入框記住你想說的話
下一篇
【Day 11】動態互動:讓光珠從瓶口落下的進場動畫
系列文
微光採集瓶:用 Vue 3 打造每日美好記錄網頁 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言