昨天我們把 姓名(必填)/ 備註(選填) 加入點單,並用 v-model
打通「輸入 ↔ 狀態」;
同時沿用 Day 1 的流程控制(v-if / :disabled
)與 Day 2 的事件驅動(@click → addOrder
)。
今天 Day 4 要解決的是:狀態要「看得見」——當某步驟未完成、完成、或錯誤時,介面要即時反映。這也是需求驅動功能拉~
這正是 資料綁定 → 樣式 的用途:以 :class(v-bind:class)
把「狀態」直接綁到「CSS 類別」,讓 UI 和邏輯保持一致。
為什麼會事先講v-model在講v-bind呢?
其實我只是要帶出一樣是需求
後的產物
基本上你的點餐服務思考到這個階段
前面的事可以點餐可統計的基本功能
但隨者使用者變多,系統需要更完美UIUX
要更華麗的視覺介面根更良好的系統體驗設計
我們就會需要用到CSS
這時候剛好就可以帶到bind的功能介紹喔!!
我們可以思考,有了基本功能後
應該還需要一些顏色或是提醒文字來告訴使用者現在的輸入階段
這時候我們的css就派上用場了
這時候老闆一定是希望
請提示使用者:尚未輸入名稱
跟飲料-> 甜度-> 冰量
的請幫我一個一個步驟來
並且給紅色的顏色
跟提示字
告知使用者填到哪個階段,還有缺少什麼?
我們可以把使用者的角色跟他的故事(操作動作) 一 一 列下來後方便你去列需求表格跟後續的程式設計
角色 | 故事 | 驗收條件 |
---|---|---|
點餐者 | 作為使用者,我希望系統能以更好的樣式與使用者體驗提示每一步是否完成,例如未完成的欄位顯示紅框與提示文字,完成後顯示綠框;送出後在訂單清單中能一眼看出甜度與冰量。 | 1. 未完成的欄位自動帶有 invalid 樣式。2. 完成的欄位自動帶有 complete 樣式。3. 訂單清單中不同甜度或冰量顯示對應顏色的標籤。 |
如同前幾天的步驟
我們可以把
面向 | 需求描述 |
---|---|
功能 | 讓表單的每一個步驟(姓名、飲料、甜度、冰量)依完成狀態自動切換樣式,例如未完成顯示紅框、完成顯示綠框。 |
使用者 | 點餐者。 |
資料 | 狀態:hasDrink 、hasSweetness 、hasIce 、canSubmit ,以及訂單清單 orders 。 |
互動 | 使用者輸入或選擇時,即時更新狀態;送出後自動清空並刷新 UI。 |
UI / UX | 以顏色 與標籤明 確顯示「哪一步已完成/未完成」,並在訂單清單中對不同屬性加上辨識標籤(甜度、冰量)。 |
驗收 | 1. 任一必填步驟未完成時該欄位自動帶有 invalid 樣式。2. 完成後切換為 complete 樣式。3. 清單能顯示含有不同屬性標籤的訂單。 |
上述都列完後可以畫時序圖
這樣就可以清楚的知道我們今天update的內容是蝦咪喽~
我們今天著重在
class的設計部分也就是completed是否已完成根invalid顏色改變的部分
我的設計思路
設計師會設計上方如果尚未填寫我就是紅色
必填已填的就會是綠色
功能:在模板中把 HTML 屬性(class、style、src、value、id、disabled…)綁定到 JavaScript 變數或運算結果。
語法糖:: 是 v-bind 的縮寫。
效果:當對應的資料改變時,Vue 自動更新屬性或樣式,不需手動操作 DOM。
他也可以使用縮寫的寫法
<!-- 完整寫法 -->
<div v-bind:class="{ active: isActive }"></div>
<!-- 縮寫 -->
<div :class="{ active: isActive }"></div>
簡單的範例
透過前面學到的@click我們嘗試把class顯示啟動跟未啟動的class
順便把value綁上去
<!-- VBindDemo.vue -->
<template>
<h2>v-bind:class 最小範例</h2>
<button :class="{ active: isActive }" @click="toggle">
{{ isActive ? '✅ 已啟動' : '❌ 未啟動' }}
</button>
<p>目前狀態:{{ isActive ? '啟動中' : '關閉中' }}</p>
</template>
<script setup>
import { ref } from 'vue'
// 狀態變數
const isActive = ref(false)
// 切換狀態
function toggle() {
isActive.value = !isActive.value
}
</script>
<style scoped>
button {
padding: 8px 16px;
border-radius: 6px;
border: 1px solid #aaa;
cursor: pointer;
}
/* 只有當 isActive 為 true 時才會被套用 */
.active {
background-color: #4caf50;
color: #fff;
border-color: #4caf50;
}
</style>
需求 | Vue 技術與重點 |
---|---|
步驟完成與否切換樣式 | :class (v-bind:class):<div :class="{ invalid: !name, complete: !!name }"> ,依據狀態動態套用 CSS 類別。 |
對不同甜度/冰量加上標籤色 | :class + 條件判斷:<span :class="sweetness === '去糖' ? 'is-nosugar' : 'is-sugar'"> 。 |
判斷每步是否完成 | computed 屬性:hasDrink 、hasSweetness 、hasIce ,讓模板判斷更乾淨。 |
送出按鈕啟用與樣式 | :disabled="!canSubmit" 搭配 :class="canSubmit ? 'enabled' : 'disabled'" 。 |
清單動態渲染 | v-for 迭代 orders 陣列,並結合 :class 呈現不同屬性標籤。 |
流程圖可以參照我們前面畫的需求部分flow chart
再根據決定的色系來實作程式碼
完成後會是這樣
<!-- DrinkOrdersDay4.vue -->
<template>
<h2>飲料點單(Day 4:狀態→樣式 :class)</h2>
<!-- Day 3:姓名/備註 -->
<div :class="['block', name ? 'complete' : 'invalid']">
<label>姓名(必填)
<input type="text" v-model.trim="name" placeholder="請輸入你的名字" />
</label>
<p class="hint" v-if="!name">尚未填寫姓名</p>
</div>
<div class="block">
<label>備註(選填)
<textarea v-model.trim="note" placeholder="例如:三點拿、少冰"></textarea>
</label>
</div>
<!-- 步驟 1:飲料 -->
<fieldset :class="['block', hasDrink ? 'complete' : 'invalid']">
<legend>步驟 1:選擇飲料</legend>
<label><input type="radio" name="drink" value="紅茶" @change="onDrinkChange('紅茶')" /> 紅茶</label>
<label><input type="radio" name="drink" value="綠茶" @change="onDrinkChange('綠茶')" /> 綠茶</label>
<p class="hint" v-if="!hasDrink">尚未選取飲料</p>
</fieldset>
<!-- 步驟 2:甜度(僅在選好飲料後顯示) -->
<fieldset v-if="hasDrink" :class="['block', hasSweetness ? 'complete' : 'invalid']">
<legend>步驟 2:選擇甜度</legend>
<label><input type="radio" name="sweetness" value="正常甜" @change="onSweetnessChange('正常甜')" /> 正常甜</label>
<label><input type="radio" name="sweetness" value="去糖" @change="onSweetnessChange('去糖')" /> 去糖</label>
<p class="hint" v-if="!hasSweetness">尚未選擇甜度</p>
</fieldset>
<!-- 步驟 3:冰量(僅在選好甜度後顯示) -->
<fieldset v-if="hasDrink && hasSweetness" :class="['block', hasIce ? 'complete' : 'invalid']">
<legend>步驟 3:選擇冰量</legend>
<label><input type="radio" name="ice" value="正常冰" @change="onIceChange('正常冰')" /> 正常冰</label>
<label><input type="radio" name="ice" value="去冰" @change="onIceChange('去冰')" /> 去冰</label>
<p class="hint" v-if="!hasIce">尚未選擇冰量</p>
</fieldset>
<!-- 送出 -->
<button :disabled="!canSubmit" @click="addOrder" :class="['submit', canSubmit ? 'enabled' : 'disabled']">
{{ canSubmit ? '送出' : '請完成所有必填' }}
</button>
<!-- 清單:用 :class 呈現屬性標籤樣式 -->
<section v-if="orders.length" class="list">
<h3>目前已送出的訂單</h3>
<ul>
<li v-for="(o, i) in orders" :key="i" class="order">
<span class="idx">{{ i + 1 }}.</span>
<span class="name">{{ o.name }}</span>
<span class="pill">{{ o.drink }}</span>
<span class="pill" :class="o.ice === '去冰' ? 'is-noice' : 'is-ice'">{{ o.ice }}</span>
<span class="pill" :class="o.sweetness === '去糖' ? 'is-nosugar' : 'is-sugar'">{{ o.sweetness }}</span>
<span v-if="o.note" class="note">備註:{{ o.note }}</span>
</li>
</ul>
</section>
</template>
<script setup>
import { ref, computed } from 'vue'
/* Day 3:輸入欄位 */
const name = ref('')
const note = ref('')
/* Day 1/2:三個選項(維持 @change) */
const drink = ref('')
const sweetness = ref('')
const ice = ref('')
/* 清單 */
const orders = ref([])
/* 事件 */
function onDrinkChange(v) { drink.value = v }
function onSweetnessChange(v) { sweetness.value = v }
function onIceChange(v) { ice.value = v }
/* Day 4:步驟完成flag(讓 :class 好讀) */
const hasDrink = computed(() => !!drink.value)
const hasSweetness = computed(() => !!sweetness.value)
const hasIce = computed(() => !!ice.value)
/* 送出條件:姓名 + 三選 */
const canSubmit = computed(() => !!(name.value && hasDrink.value && hasSweetness.value && hasIce.value))
/* 送出 */
function addOrder() {
if (!canSubmit.value) return
orders.value.push({
name: name.value,
note: note.value,
drink: drink.value,
sweetness: sweetness.value,
ice: ice.value
})
// 清空,準備下一筆
name.value = ''
note.value = ''
drink.value = ''
sweetness.value = ''
ice.value = ''
}
</script>
<style scoped>
/* 最小必要樣式:用於展示 :class 的效果 */
.block { padding: 8px; border: 1px solid #ddd; border-radius: 8px; margin: 10px 0; }
.invalid { border-color: #e57373; background: #fff5f5; }
.complete { border-color: #66bb6a; background: #f3fff3; }
.hint { font-size: 12px; color: #c62828; margin-top: 4px; }
.submit { padding: 8px 12px; border-radius: 6px; border: 1px solid #ccc; margin: 8px 0; }
.submit.enabled { background: #1976d2; color: #fff; border-color: #1976d2; }
.submit.disabled { background: #f0f0f0; color: #888; }
.list { margin-top: 14px; }
.order { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 6px 0; }
.idx { width: 24px; text-align: right; margin-right: 4px; color: #666; }
.name { font-weight: 600; margin-right: 6px; }
.pill { padding: 2px 8px; border-radius: 9999px; border: 1px solid #ccc; font-size: 12px; }
.is-ice { background: #e3f2fd; border-color: #90caf9; }
.is-noice { background: #e8f5e9; border-color: #a5d6a7; }
.is-sugar { background: #fff3e0; border-color: #ffcc80; }
.is-nosugar { background: #fce4ec; border-color: #f48fb1; }
.note { color: #555; font-size: 12px; }
</style>
今天我們家了style的CSS樣式
也就是在vue的寫法裡面,除了前幾天介紹的template
、script
多了一個style
至於為什麼要加scope呢?
目的:讓這個元件的 CSS 不會滲漏到其他元件,也不會被外面的 CSS 汙染。
原理(重點理解就好):Vue 會在渲染時自動替你的 HTML 節點加上一個像 data-v-xxxxxx 的屬性;同時把 CSS 選擇器也自動加上同樣的屬性,達到**樣式「只在本元件生效」**的效果。
其實就是以後你程式碼大之後要拆解多個vue的組件元數用的-> 這邊先這樣有個概念即可
可以透過這邊驗證程式有沒有對
需求回顧
v-if
/ :disabled
)、Day 2 的事件驅動(@click
)、Day 3 的雙向資料流(v-model
)基礎上,技術關鍵
:class
(v-bind:class)ref
或 computed
的狀態即時切換 CSS 類別,例如 { invalid: !hasDrink, complete: hasDrink }
。<style scoped>
computed
flaghasDrink
、hasSweetness
、hasIce
,讓模板判斷簡單、可讀。v-for
+ :class完成的功能
invalid
樣式(紅框或提示),完成後即時切換為 complete
(綠框)。<style scoped>
保護,不會影響其他頁面或元件。類別 | 注意事項 |
---|---|
scoped 的作用 | 確保 CSS 只套用在當前元件;若要改子元件內部樣式需用 ::v-deep 。 |
狀態切換 | 使用 ref 和 computed 建立flag,再用 :class 對應 UI;不要直接在模板寫過長條件。 |
樣式必須存在 | :class 只負責套用 class,不會自動產生 CSS,必須在 <style scoped> 中定義。 |
v-for 與 class | 在訂單清單使用 v-for 迭代並加條件 class,可直觀呈現每筆訂單屬性。 |
DOM 更新自動化 | 只要 ref 或 computed 值改變,Vue 會自動刷新畫面,無需手動操作 DOM。 |
<style scoped>
保持元件獨立。