Giao diện
Lỗi 422 và form
Hai tầng kiểm tra dữ liệu, cả hai đều cần:
| Tầng | Ai kiểm | Ví dụ | Hiện ở đâu |
|---|---|---|---|
| Ràng buộc nhập liệu | rules của CForm, chạy ở trình duyệt | Bắt buộc, độ dài, định dạng email | Ngay khi rời ô nhập |
| Nghiệp vụ | Backend, trả 422 | Mã trùng, vượt hạn mức, khách hàng ngừng hoạt động | Dưới đúng ô nhập, sau khi gửi |
Framework cố tình không hiện toast cho 422: lỗi phải nằm cạnh ô nhập gây ra nó, không phải ở góc màn hình.
Thân lỗi 422
json
{
"message": "Dữ liệu không hợp lệ",
"errors": {
"customer": ["Khách hàng không tồn tại"],
"amount": ["Giá trị phải lớn hơn 0", "Vượt hạn mức tín dụng"]
}
}errors (hoặc fields) phải ở gốc body. Backend lồng vào chỗ khác thì quy đổi trong fetcher trước khi ném ra.
Đọc lỗi ra
ts
import { getFieldErrors, isValidation } from '@tasco/utils'
const serverErrors = ref<Record<string, string[]>>({})
try {
await createOrder(model)
} catch (caught) {
if (isValidation(caught)) {
serverErrors.value = getFieldErrors(caught) // { customer: [...], amount: [...] }
} else {
$tascoError(caught)
}
}getFieldErrors luôn trả object (rỗng nếu không có gì đọc được) và luôn chuẩn hoá giá trị thành mảng chuỗi, kể cả khi backend trả một chuỗi đơn.
Gắn vào ô nhập
vue
<CFormItem
label="Khách hàng"
name="customer"
:validate-status="serverErrors.customer ? 'error' : undefined"
:help="serverErrors.customer?.[0]"
>
<CInput v-model:value="model.customer" />
</CFormItem>Bấm nút trong ví dụ dưới để xem lỗi 422 hiện lên đúng ô nhập, và biến mất khi người dùng sửa lại:
Nhiều lỗi trên một field thì hiện tất cả:
vue
:help="serverErrors.amount?.join('. ')"Ví dụ đầy đủ
vue
<script setup lang="ts">
import { CFormItem, CInput, CTextarea, message } from '@tasco/ui'
import { getFieldErrors, isValidation } from '@tasco/utils'
import type { OrderInput } from '~/composables/orders.types'
const { createOrder } = useOrdersApi()
const { $tascoError } = useNuxtApp()
const model = reactive<OrderInput>({ customer: '', amount: 0, note: '' })
const submitting = ref(false)
const serverErrors = ref<Record<string, string[]>>({})
const rules = {
customer: [{ required: true, message: 'Nhập tên khách hàng' }],
amount: [{ required: true, message: 'Nhập giá trị đơn hàng' }],
}
/** Người dùng sửa ô nào thì xoá lỗi server của ô đó — tránh lỗi cũ nằm lại. */
function clearServerError(field: keyof OrderInput) {
if (serverErrors.value[field]) {
const { [field]: _removed, ...rest } = serverErrors.value
serverErrors.value = rest
}
}
// `finish` chỉ phát khi rules phía client đã qua nên đây là chỗ gọi API. Form cần tự quyết
// thời điểm validate thì dùng ref: xem [Trang form](/guide/patterns/form).
async function onFinish() {
submitting.value = true
serverErrors.value = {}
try {
const order = await createOrder({ ...model })
message.success('Đã tạo đơn hàng.')
await navigateTo(`/orders/${order.id}`)
} catch (caught) {
if (isValidation(caught)) serverErrors.value = getFieldErrors(caught)
else $tascoError(caught)
} finally {
submitting.value = false
}
}
</script>
<template>
<CForm :model="model" :rules="rules" style="max-width: 520px" @finish="onFinish">
<CFormItem
label="Khách hàng"
name="customer"
:validate-status="serverErrors.customer ? 'error' : undefined"
:help="serverErrors.customer?.[0]"
>
<CInput v-model:value="model.customer" @change="clearServerError('customer')" />
</CFormItem>
<CFormItem
label="Giá trị"
name="amount"
:validate-status="serverErrors.amount ? 'error' : undefined"
:help="serverErrors.amount?.[0]"
>
<CInputCurrency v-model:value="model.amount" @change="clearServerError('amount')" />
</CFormItem>
<CFormItem label="Ghi chú" name="note">
<CTextarea v-model:value="model.note" :rows="3" />
</CFormItem>
<CFormItem>
<CButton variant="primary" :loading="submitting" html-type="submit">Lưu</CButton>
</CFormItem>
</CForm>
</template>Lỗi không thuộc về field nào
Backend có thể trả 422 với message chung mà không có errors ("Đơn hàng đã bị khoá bởi người khác"). Hiện nó ở đầu form:
vue
<CAlert
v-if="formError"
type="error"
:message="formError"
show-icon
style="margin-bottom: 16px"
/>ts
catch (caught) {
if (isValidation(caught)) {
const error = toAppError(caught)
serverErrors.value = getFieldErrors(error)
formError.value = Object.keys(serverErrors.value).length ? '' : error.message
}
}CAlert là primitive antdv re-export — import { CAlert } from '@tasco/ui'.
Bẫy thường gặp
- Không xoá lỗi cũ trước khi gửi lại — người dùng sửa xong vẫn thấy lỗi cũ nằm đó.
- Tên field không khớp giữa backend và
model:order_amountở backend sẽ không gắn vào ôamount. Thống nhất tên, hoặc map lại trong fetcher. - Dùng
createError({ data })trong route Nitro của dự án — h3 lồng thêm một tầngdatakhiếngetFieldErrorskhông thấy gì. DùngsetResponseStatus(event, 422)rồi trả body thẳng, xem Hợp đồng dữ liệu. - Backend trả 400 thay vì 422 cho lỗi nhập liệu — người dùng sẽ thấy toast thay vì lỗi dưới ô nhập. Thống nhất mã trạng thái từ đầu.
Liên quan
- Trang form — mẫu trang thêm/sửa đầy đủ.
- Hiển thị lỗi — các loại lỗi còn lại.
- Hợp đồng dữ liệu — định dạng 422 chuẩn.