昨天 Day 1 我們已經做出一個可以逐步選擇飲料
、甜度
、冰量
的前端介面,並透過 @change 在使
用者選項改變時即時更新狀態。那個版本的核心是條件判斷與畫面控制:
用 v-if / v-else 分段顯示每一步
用 :disabled 控制按鈕是否可點
@change 的工作就是當值被改變時,把新的 value 寫回狀態。
但是,我們真正的目的是 建立一個飲料點餐統計系統。
所以今天 Day 2 要往送出訂單
邁進:
使用者完成三步選項後,按下送出按鈕,系統要能記錄這筆訂單,並在記憶體中(我們先存在前端,後續章節會教大
家存到後端)保存。
我的寫作思路依舊是:
1.需求
→ 2.User Story
→ 3.流程與架構
→ 4.需要的Vue 技術
→ 5.逐步完成程式碼
。
只要一開始把使用者操作邏輯與系統需求想清楚,再把流程與 UI 畫出來,程式其實就很容易寫。
其實現在AI coding輔助非常厲害,不一定需要一篇一篇把document看完才可以coding。
更考驗的是工程師在思考系統
好了~! 接下來我們就開始吧!
user story的核心概念很簡單
基本上就是定義這個動作的腳色有誰? (點餐人、統計者...等)
動作有什麼? (點選飲料後送出)
那我們可把各種動作拆解成小動作會更好去把程式的小功能(function)完成
角色 | 故事 | 驗收條件 |
---|---|---|
點餐者 | 作為一位使用者,我想在選擇完成後點擊「送出」按鈕,把我的飲料選擇記錄到系統中,以便後續統計。 | 按下送出後:1. 訂單被加到清單;2. 畫面提示成功;3. 表單清空。 |
其實很簡單,我們昨天已經先把飲料、甜度、冰量的資料定義了。
接下來我們可以想像自己跟自己或是跟朋友討論接下來是有什麼需求?
當然我們點完餐就是要把它送入清單中就像是foodpanda或是ubereat這樣的模式。
所以我們勢必須要把三種data送入list(陣列中存放),同時送出的功能也獲透過@click功能觸發。
我們可以把需求面像裂成表格,更好去思考後面的圖~
面向 | 需求描述 |
---|---|
功能 | 使用者完成飲料、甜度、冰量選擇後,能夠按下「送出」並把該筆資料存入訂單清單。 |
使用者 | 點餐者(今天仍以單一使用者為主,未分統計者角色)。 |
資料 | 每筆訂單包含 { drink, sweetness, ice } 。 |
狀態 | 需有一個 orders 陣列存放已送出的所有訂單。 |
行為 | 按下按鈕時:1️⃣ 驗證選項完整 → 2️⃣ 新增到 orders → 3️⃣ 重置輸入狀態。 |
畫面 | 送出後顯示「送出成功」訊息與目前的訂單清單。 |
可以把大系統拆解成這樣的時序圖(接續day1的內容延伸今天的事件觸發功能)
有了上面的情境我們就可以思考需要的動作
有哪些這時候我們可以詢問google、stackoverflow、或AI
基本上這四種需求可以延伸出我們會使用到的功能
需求 | Vue 技術 |
---|---|
點擊按鈕觸發事件 | @click (v-on:click 的簡寫) |
新增一筆訂單到清單 | ref([]) 建立 orders 陣列,方法 addOrder() 處理新增 |
重置輸入狀態 | 將 drink.value 、sweetness.value 、ice.value 設為空字串 |
顯示所有已送出的訂單 | v-for 迭代 orders ,動態生成清單 |
這邊我們認識了兩位新朋友@click跟v-for
@ 與 v-on 是什麼?
v-on
是 Vue 綁定 DOM 事件的指令,用來監聽元素的各種事件(click、change、input、keyup...)。
@ 是 v-on
的縮寫,兩者完全等價:
<button v-on:click="doSomething">送出</button>
<!-- 等同於 -->
<button @click="doSomething">送出</button>
我們官方最常使用的範例也是用這個方式demo
<template>
<button @click="count++">點我加一</button>
<p>目前數字:{{ count }}</p>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
v-for 是什麼?
v-for
是 Vue 用來迭代(loop)陣列或物件的指令。
會依據資料數量動態產生多個元素,不用手動一個個寫 。
常用情境:
顯示清單(商品列表、訂單紀錄、留言…)
基本的語法是
<li v-for="(item, index) in items" :key="index">
{{ index + 1 }}. {{ item }}
</li>
舉個範例今天的資料長這樣,很貼近我們的飲料點餐樣貌
<!-- VForOrdersDemo.vue -->
<template>
<h2>v-for 範例:飲料訂單清單</h2>
<ul>
<li v-for="(o, i) in orders" :key="i">
{{ i + 1 }}. 飲料:{{ o.drink }} / 甜度:{{ o.sweetness }} / 冰量:{{ o.ice }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
/* 這裡準備一組示範資料 */
const orders = ref([
{ drink: '紅茶', sweetness: '正常甜', ice: '正常冰' },
{ drink: '綠茶', sweetness: '去糖', ice: '去冰' },
{ drink: '紅茶', sweetness: '去糖', ice: '正常冰' }
])
/*
實際渲染結果 (畫面會自動產生以下 HTML):
<ul>
<li>1. 飲料:紅茶 / 甜度:正常甜 / 冰量:正常冰</li>
<li>2. 飲料:綠茶 / 甜度:去糖 / 冰量:去冰</li>
<li>3. 飲料:紅茶 / 甜度:去糖 / 冰量:正常冰</li>
</ul>
*/
</script>
OK會了這個騷操作之後我們就可以開始著手修改我們的程式摟~!!
今天的程式flowchart會長這樣
有了這個思路之後
我們預期長出來的UI會是這樣
我們接下來就可以把程式碼長出來了~
其實有這個思路可以直接詠唱給AI或是把昨天程式碼改過就好了
<!-- DrinkOrdersDay2.vue -->
<template>
<h2>飲料點單(Day 2:事件觸發 + v-for)</h2>
<!-- 步驟 1:飲料 -->
<fieldset>
<legend>步驟 1:選擇飲料</legend>
<label><input type="radio" name="drink" value="紅茶" @change="onDrinkChange('紅茶')"> 紅茶</label>
<label><input type="radio" name="drink" value="綠茶" @change="onDrinkChange('綠茶')"> 綠茶</label>
<p v-if="!drink">⚠️ 尚未選取飲料</p>
<p v-else>✅ 已選:{{ drink }}</p>
</fieldset>
<!-- 步驟 2:甜度(選到飲料後才出現) -->
<fieldset v-if="drink">
<legend>步驟 2:選擇甜度</legend>
<label><input type="radio" name="sweetness" value="正常甜" @change="onSweetnessChange('正常甜')"> 正常甜</label>
<label><input type="radio" name="sweetness" value="去糖" @change="onSweetnessChange('去糖')"> 去糖</label>
<p v-if="!sweetness">⚠️ 尚未選擇甜度</p>
<p v-else>✅ 已選:{{ sweetness }}</p>
</fieldset>
<!-- 步驟 3:冰量(選到甜度後才出現) -->
<fieldset v-if="drink && sweetness">
<legend>步驟 3:選擇冰量</legend>
<label><input type="radio" name="ice" value="正常冰" @change="onIceChange('正常冰')"> 正常冰</label>
<label><input type="radio" name="ice" value="去冰" @change="onIceChange('去冰')"> 去冰</label>
<p v-if="!ice">⚠️ 尚未選擇冰量</p>
<p v-else>✅ 已選:{{ ice }}</p>
</fieldset>
<!-- 送出按鈕:完成三項才可按 -->
<button :disabled="!canSubmit" @click="addOrder">送出</button>
<!-- v-for:顯示已送出的訂單清單 -->
<section v-if="orders.length">
<h3>目前已送出的訂單</h3>
<ul>
<li v-for="(o, i) in orders" :key="i">
{{ i + 1 }}. 飲料:{{ o.drink }} / 甜度:{{ o.sweetness }} / 冰量:{{ o.ice }}
</li>
</ul>
</section>
</template>
<script setup>
import { ref, computed } from 'vue'
// 當前表單狀態
const drink = ref('') // '紅茶' | '綠茶'
const sweetness = ref('') // '正常甜' | '去糖'
const ice = ref('') // '正常冰' | '去冰'
// 訂單清單:v-for 的資料來源
const orders = ref([])
// 事件處理(用 @change)
function onDrinkChange(v) { drink.value = v }
function onSweetnessChange(v) { sweetness.value = v }
function onIceChange(v) { ice.value = v }
// 三者皆選才允許送出
const canSubmit = computed(() => drink.value && sweetness.value && ice.value)
// @click:加入一筆訂單並清空表單,v-for 會自動重渲染
function addOrder() {
if (!canSubmit.value) return
orders.value.push({
drink: drink.value,
sweetness: sweetness.value,
ice: ice.value
})
// 清空,準備下一筆
drink.value = ''
sweetness.value = ''
ice.value = ''
}
/*
// 如果你想先預覽 v-for 的渲染效果,可先塞入示例資料:
orders.value = [
{ drink: '紅茶', sweetness: '正常甜', ice: '正常冰' },
{ drink: '綠茶', sweetness: '去糖', ice: '去冰' },
{ drink: '紅茶', sweetness: '去糖', ice: '正常冰' },
]
*/
</script>
我們可以透過這邊去驗證程式碼對不對
今天有幾個小tips要提醒大家
在 中,若是用 ref() 宣告的變數(例如 drink、sweetness、ice、orders),
存取或修改都要加 .value:
drink.value = '紅茶' // ✅ 正確
console.log(drink.value) // ✅ 正確
如果直接寫 drink = '紅茶',那只是把整個 ref 變數替換成字串,Vue 無法追蹤變化,畫面也不會更新。
2.新增資料時記得 push .value
orders 是一個 ref 包著陣列,所以在新增資料時要使用:
orders.value.push({
drink: drink.value,
sweetness: sweetness.value,
ice: ice.value
})
3.清空狀態:直接把 .value 設為空字串
送出後需要準備下一筆訂單,必須將三個狀態都清回空字串:
drink.value = ''
sweetness.value = ''
ice.value = ''
這樣 裡的 v-if 條件會重新判斷,回到初始畫面流程不然的話,暫存清單沒清空下一個user無法使用喔!!。
4.v-for 會自動偵測新增與清空
v-for 綁定在 orders 上,只要 orders.value 有任何變化(新增、刪除),Vue 會自動重新渲染畫面:
<li v-for="(o, i) in orders" :key="i">
{{ i + 1 }}. 飲料:{{ o.drink }} / 甜度:{{ o.sweetness }} / 冰量:{{ o.ice }}
</li>
讀值或寫值 → state.value
新增或刪除陣列元素 → orders.value.push() / splice()
清空狀態 → state.value = ''
只要遵守 「任何來自 ref 的值都要用 .value」 這個原則,就能避免資料不更新或畫面不重繪的問題。