昨天我們在 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);
}
由下圖可以看出瓶子中間那行提示:「瓶子目前是空的」沒有置中,所以需要將程式碼再調整一下。
看到.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 才能正確地把它單獨置中。
存檔後回到瀏覽器,測試看看:
如果這三點都正常,代表 gems 陣列、v-for 渲染、:key 身分識別都正確運作了。
小提示
我們有在程式碼中寫一句:console.log('目前所有光珠:', gems.value),你可以打開開發人員工具,切換到 Console 看看資料是否有正確寫入。
count 升級成 gems,讓資料不只是「數量」,還完整保留了每一次採集的內容。v-for 列表渲染:學會用 v-for="item in list" 把陣列裡的每一筆資料,重複畫成對應的畫面元素。:key 的重要性:理解為什麼每一筆資料都需要一個獨一無二的身分證,讓 Vue 在畫面更新時不會認錯資料。現在瓶子真的會累積光珠了,不過目前所有光珠都只是裡面排排站,明天(Day 11),我們要讓新加入的光珠有動畫效果:帶有一點重量感,從瓶口緩緩落下、停在瓶底。