第 1~12 天,我們一步步把「飲料點單」施法到能跑、能管理、能共享。
但真正上線的系統,錯的輸入就像走錯陣法:會讓資料歪掉、流程卡住,甚至害你加班(啊!)。
今天我們召喚兩件強力法器:
這類驗證工具的價值:避免工程師每個表單都「重新造輪子」。
規則用 Schema 集中描述、錯誤由元件統一管理,一致、可測、可擴充,你就不會在不同頁面複製貼上同一段 if/else 了。
今天我們只改 OrderForm.vue
,API 與其他元件維持不變。
主軸目標:
npm i vee-validate yup
思路:
useForm
/useField
綁定欄位與錯誤狀態handleSubmit
成功才 emit,並清空表單<script setup>
import { computed } from 'vue'
import OptionGroup from './OptionGroup.vue'
import { useForm, useField } from 'vee-validate'
import * as yup from 'yup'
const emit = defineEmits(['submit'])
const props = defineProps({
disabled: { type: Boolean, default: false },
drinks: { type: Array, default: () => [] },
sweetnessOptions: { type: Array, default: () => [] },
iceOptions: { type: Array, default: () => [] },
// 例:
// menuRules = {
// '紅茶': { allowedSweetness: ['正常甜','去糖'], allowedIce: ['正常冰','去冰'] },
// '綠茶': { allowedSweetness: ['正常甜','去糖'], allowedIce: ['正常冰','去冰'] },
// '巧克力': { allowedSweetness: ['正常甜','少糖'], allowedIce: ['熱飲'] } // 只能熱
// }
menuRules: { type: Object, default: () => ({}) },
})
/** Yup:表單規則(可重用) */
const schema = computed(() =>
yup.object({
name: yup.string().required('姓名必填').min(2, '至少 2 個字').max(20, '最多 20 個字'),
note: yup.string().max(50, '備註最多 50 個字').optional(),
drink: yup.string().required('請選擇飲料'),
sweetness: yup.string().required('請選擇甜度'),
ice: yup.string().required('請選擇冰量'),
}).test('menu-rule', '選項與菜單規則不符', (values) => {
const rule = props.menuRules?.[values.drink]
if (!rule) return true
const okSweet = !rule.allowedSweetness?.length || rule.allowedSweetness.includes(values.sweetness)
const okIce = !rule.allowedIce?.length || rule.allowedIce.includes(values.ice)
return okSweet && okIce
})
)
/** VeeValidate:表單/欄位 */
const { handleSubmit, errors, isSubmitting } = useForm({ validationSchema: schema })
const { value: name } = useField('name')
const { value: note } = useField('note')
const { value: drink } = useField('drink')
const { value: sweetness } = useField('sweetness')
const { value: ice } = useField('ice')
/** 依飲料動態產生可選甜度/冰量(顯示層面) */
const optSweetness = computed(() => {
const rule = props.menuRules?.[drink.value]
return rule?.allowedSweetness?.length ? rule.allowedSweetness : props.sweetnessOptions
})
const optIce = computed(() => {
const rule = props.menuRules?.[drink.value]
return rule?.allowedIce?.length ? rule.allowedIce : props.iceOptions
})
/** 送出 */
const onSubmit = handleSubmit((v) => {
if (props.disabled) return
emit('submit', v) // 對外契約不變:仍然送出乾淨 payload
name.value = ''
note.value = ''
drink.value = ''
sweetness.value = ''
ice.value = ''
})
</script>
<template>
<!-- 姓名 -->
<div :class="['block', name ? 'complete' : 'invalid']">
<label>姓名(必填)
<input type="text" v-model.trim="name" placeholder="請輸入你的名字" />
</label>
<p class="hint" v-if="errors.name">{{ errors.name }}</p>
</div>
<!-- 備註 -->
<div class="block">
<label>備註(選填)
<textarea v-model.trim="note" placeholder="例如:三點拿、少冰"></textarea>
</label>
<p class="hint" v-if="errors.note">{{ errors.note }}</p>
</div>
<!-- 飲料 / 甜度 / 冰量 -->
<OptionGroup label="步驟 1:選擇飲料" :options="props.drinks" v-model="drink" required />
<p class="hint" v-if="errors.drink">{{ errors.drink }}</p>
<OptionGroup v-if="drink"
label="步驟 2:選擇甜度"
:options="optSweetness"
v-model="sweetness"
required />
<p class="hint" v-if="errors.sweetness">{{ errors.sweetness }}</p>
<OptionGroup v-if="drink && sweetness"
label="步驟 3:選擇冰量"
:options="optIce"
v-model="ice"
required />
<p class="hint" v-if="errors.ice">{{ errors.ice }}</p>
<button :disabled="props.disabled || isSubmitting"
class="submit enabled"
@click="onSubmit">
送出
</button>
<!-- 全表單級錯誤(如跨欄位規則) -->
<p class="hint" v-if="errors['']">{{ errors[''] }}</p>
</template>
欄位 | 規則 | 範例說明 |
---|---|---|
name | required、min(2)、max(20) | 姓名必填,2~20 字 |
note | max(50) | 備註最多 50 字 |
drink | required | 必選飲料 |
sweetness | required | 必選甜度 |
ice | required | 必選冰量 |
cross rule | test() 跨欄位:依 menuRules |
例:巧克力只能熱飲 |
跨欄位驗證的關鍵:
yup.object({ /* fields... */ }).test('menu-rule', '選項與菜單規則不符', (values) => {
const rule = props.menuRules?.[values.drink]
if (!rule) return true
const okSweet = !rule.allowedSweetness?.length || rule.allowedSweetness.includes(values.sweetness)
const okIce = !rule.allowedIce?.length || rule.allowedIce.includes(values.ice)
return okSweet && okIce
})
errors / isSubmitting
等通通幫你管
v-if
、選單)只負責「引導」,真正的資料保證交給 Yup。test()
:像「巧克力只能熱飲」這種關聯規則,很適合集中在 schema。OrderForm
專心組裝 UI 與發出 submit
,規則/錯誤交由工具處理。今天把「輸入正確性」交給成熟法器,
你只要專注在需求與體驗,法陣就會守住資料品質。
需求至上的 Vue 魔法之旅,就是用對工具、寫對地方,
讓你和使用者都安心:每一筆訂單,都是合乎規則的好咒語。
我們今天算是把一些常用到的技巧跟工具講解完畢了
接下來就要進入到chapter3了~~
我們在一起努力唄~!!!