昨天我們替採集瓶加上了心情調色盤,讓每次採集前都能選取屬於今天的色彩。但光有顏色還不夠,如果不能把當天發生的那件小事化作文字,時間一久,可能就忘了當初為什麼選了那個顏色。
所以今天我們要替採集瓶加上最重要的「文字留言框」,並認識 Vue 在表單處理上的重點工具:雙向綁定 v-model。
在認識 v-model 之前,我們先回顧一下前面幾天學過的語法:
{{ count }}:把 JS 的資料,單純印到 HTML 畫面上。:class / :style:由 JS 的狀態,決定 HTML 標籤的樣式。你有發現共通點嗎?它們都是單向的(JavaScript ➜ 畫面):只要 JS 裡的資料變了,畫面就會跟著更新。
但輸入框(<input>)不一樣。輸入框是讓「使用者」在畫面上打字的地方。我們不僅希望 JS 能把文字塞進輸入框,更需要使用者在鍵盤敲下文字時,JS 裡的變數能「即時」拿到最新打進去的字。
這種「資料變,畫面跟著變;畫面被使用者修改,資料也即時同步更新」的雙向溝通,就叫做雙向綁定(Two-Way Binding),而 Vue 在這方面給我們的專屬指令就是 v-model 。
v-model 其實不是一個新的語法,而是把我們在 Day 6 學的 v-bind(:) 和 Day 8 學到的 v-on (@)結合在一起,Vue 利用 v-model 來同時做這兩個指令。
範例:
<!-- 原始寫法 -->
<input
:value="todayNote"
@input="todayNote = $event.target.value"
/>
<!-- 透過 v-model 的寫法 -->
<input v-model="todayNote" />
:value="todayNote":負責把變數的值送到輸入框顯示(v-bind 的工作)。@input="...":監聽使用者的打字輸入事件,把最新輸入的字存回變數(v-on 的工作)。Vue 知道每次寫表單都要拆成這兩行太麻煩了,所以發明了 v-model,讓我們用一個指令同時搞定輸入與同步。
打開 src/components/GemJar.vue,依序進行修改。
<script setup>:新增文字變數與採集檢查在原本的 selectedColor 下方,新增變數來存使用者的文字:
// 存放輸入框的文字
const todayNote = ref('')
接著找到原本的 addGem() 函式,在最前面加入「沒打字就跳提示」的檢查,並在採集成功後印出結果並清空:
function addGem() {
// 新增:如果沒打字,就不給放入
if (todayNote.value.trim() === '') {
alert('請先寫下一件今天的美好小事喔!')
return
}
count.value++
isGlowing.value = true
// 新增:在 Console 確認採集到的完整資訊
console.log('採集成功!', {
color: selectedColor.value,
note: todayNote.value
})
// 新增:採集後清空輸入框
todayNote.value = ''
setTimeout(() => {
isGlowing.value = false
}, 1000)
}
<template>:在調色盤下方插入輸入框找到調色盤 </div> 與「放入這道微光」<button> 之間,把這段輸入框群組放進去:
</div> <!-- 原本調色盤的結尾 -->
<!-- 新增:文字輸入區域 -->
<div class="input-group">
<input
v-model="todayNote"
type="text"
maxlength="20"
placeholder="寫下一件微小的好事..."
class="note-input"
/>
<!-- 即時計算剩餘字數 -->
<span class="char-count">{{ 20 - todayNote.length }} / 20</span>
</div>
<button class="btn-collect" ...> <!-- 這是原本的按鈕 -->
因為我希望使用者每天僅需花少少的時間簡單記下今天的美好,所以字數限制為20字。
<style scoped>:輸入框排版原本的瓶子、調色盤樣式完全不用動,直接滑到 CSS 最底部,貼上輸入框的外觀設定:
/* 新增:輸入框外層容器 */
.input-group {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
padding: 6px 12px;
margin-bottom: 16px;
width: 240px;
box-sizing: border-box;
}
/* 新增:輸入框本體 */
.note-input {
background: transparent;
border: none;
outline: none;
color: #ffffff;
font-size: 0.85rem;
width: 100%;
}
.note-input::placeholder {
color: #6e7681;
}
/* 新增:字數提示文字 */
.char-count {
font-size: 0.75rem;
color: #8b949e;
white-space: nowrap;
margin-left: 8px;
}
以上分段敘述新增的功能和程式碼,以下是完整 GemJar.vue 程式碼:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const isGlowing = ref(false)
const selectedColor = ref('#E3B341')
// 1. 存放輸入框文字
const todayNote = ref('')
// 2. 切換顏色的函式,接收使用者點選的顏色
function selectColor(color) {
selectedColor.value = color
}
function addGem() {
// 防呆機制:若未輸入文字則中斷
if (todayNote.value.trim() === '') {
alert('請先寫下一件今天的美好小事喔!')
return
}
count.value++
isGlowing.value = true
// 在 Console 印出採集到的顏色與文字
console.log('採集成功!', {
color: selectedColor.value,
note: todayNote.value
})
// 採集完成後清空輸入框
todayNote.value = ''
setTimeout(() => {
isGlowing.value = false
}, 1000)
}
</script>
<template>
<div class="bottle-wrapper">
<div class="cork"></div>
<div class="bottle" :class="{ 'is-glowing': isGlowing }">
<span v-if="count === 0">瓶子目前是空的</span>
</div>
</div>
<p>已採集 {{ count }} 顆光珠</p>
<!-- 心情調色盤 -->
<div class="palette">
<button
class="color-dot blue"
:class="{ 'is-active': selectedColor === '#64B5F6' }"
@click="selectColor('#64B5F6')"
></button>
<button
class="color-dot gold"
:class="{ 'is-active': selectedColor === '#E3B341' }"
@click="selectColor('#E3B341')"
></button>
<button
class="color-dot pink"
:class="{ 'is-active': selectedColor === '#F48FB1' }"
@click="selectColor('#F48FB1')"
></button>
<button
class="color-dot green"
:class="{ 'is-active': selectedColor === '#81C784' }"
@click="selectColor('#81C784')"
></button>
</div>
<!-- 文字輸入區域(雙向綁定與即時剩餘字數) -->
<div class="input-group">
<input
v-model="todayNote"
type="text"
maxlength="20"
placeholder="寫下一件微小的好事..."
class="note-input"
/>
<span class="char-count">{{ 20 - todayNote.length }} / 20</span>
</div>
<!-- 採集按鈕 -->
<button
class="btn-collect"
:style="{ backgroundColor: selectedColor }"
@click="addGem"
>
放入這道微光
</button>
</template>
<style scoped>
/* 瓶子的樣式 */
.bottle-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin: 16px 0;
}
/* 軟木塞 */
.cork {
width: 40px;
height: 12px;
background-color: #a07855;
border-radius: 3px;
}
/* 瓶身 */
.bottle {
width: 160px;
height: 220px;
border: 2px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.04);
border-radius: 12px 12px 40px 40px;
display: flex;
align-items: center;
justify-content: center;
color: #6e7681;
font-size: 0.85rem;
transition: border-color 1.0s ease, box-shadow 1.0s ease;
}
.bottle.is-glowing {
border-color: rgba(227, 179, 65, 0.95);
box-shadow: 0 0 24px rgba(227, 179, 65, 0.5);
}
/* 調色盤排版 */
.palette {
display: flex;
gap: 12px;
margin: 12px 0 16px 0;
}
/* 心情代表色 */
.color-dot {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s ease;
}
/* 各別顏色定義 */
.color-dot.blue { background-color: #64B5F6; }
.color-dot.gold { background-color: #E3B341; }
.color-dot.pink { background-color: #F48FB1; }
.color-dot.green { background-color: #81C784; }
/* 當前被選中的心情代表色 */
.color-dot.is-active {
border-color: #ffffff;
transform: scale(1.25);
}
/* 輸入框外層容器 */
.input-group {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
padding: 6px 12px;
margin-bottom: 16px;
width: 240px;
box-sizing: border-box;
}
/* 輸入框文字本體 */
.note-input {
background: transparent;
border: none;
outline: none;
color: #ffffff;
font-size: 0.85rem;
width: 100%;
}
.note-input::placeholder {
color: #6e7681;
}
/* 即時字數計數標籤 */
.char-count {
font-size: 0.75rem;
color: #8b949e;
white-space: nowrap;
margin-left: 8px;
}
/* 採集按鈕樣式 */
.btn-collect {
padding: 8px 20px;
border: none;
border-radius: 20px;
color: #1a1a1a;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s ease;
}
.btn-collect:active {
transform: scale(0.96);
}
</style>
存檔後回到瀏覽器測試:
即時打字連動:在輸入框隨意敲幾個字,右側是不是即時計算剩餘字數?
防呆檢查:如果輸入框是空的,直接按「放入這道微光」,會跳出提醒視窗請你輸入內容。
資料打包確認:選好顏色、輸入「今天買了喜歡的積木」,按下按鈕。打開瀏覽器的 Console,會看到成功印出了帶有顏色與文字的完整紀錄,且按鈕按下的瞬間,輸入框也自動被清空了。
v-bind 是單向傳遞(資料 ➜ 畫面);而 v-model 是雙向同步,專門用在表單輸入元素上。v-model 等同於 :value(動態綁定值)加上 @input(監聽打字事件)的合體。count)」、「顏色(selectedColor)」以及「故事(todayNote)」。現在按下按鈕時,Console 已經能印出一筆完整的紀錄了,但我們不能讓紀錄只留在 Console 裡。明天(Day 10),我們將嘗試用「陣列物件」把這些資料封裝成一顆顆真正的光珠資料,讓光珠在瓶子裡可視化!