Giao diện
Trang chi tiết
Xem một bản ghi: thông tin chính, vài hành động, và thường có bảng con hoặc tab. Khác trang danh sách ở chỗ dữ liệu tải một lần theo id trên URL.
Bộ khung
vue
<!-- app/pages/orders/[id].vue -->
<script setup lang="ts">
import { CDescriptions, CDescriptionsItem } from '@tasco/ui'
import type { Order } from '~/composables/orders.types'
definePageMeta({ permissions: ['/orders/search'] })
const route = useRoute()
const { fetchOrder } = useOrdersApi()
const { data: order, status, error, refresh } = await useAsyncData<Order>(
() => `order-${route.params.id}`,
() => fetchOrder(String(route.params.id)),
)
const currency = new Intl.NumberFormat('vi-VN')
</script>
<template>
<CPageHeader
:title="order?.code ?? 'Đơn hàng'"
:sub-title="order?.customer"
:breadcrumb="[{ title: 'Đơn hàng', to: '/orders' }, { title: order?.code ?? '' }]"
@navigate="navigateTo"
>
<template #extra>
<CButton v-can="'/orders/update'" variant="outline" @click="navigateTo(`/orders/${order?.id}/edit`)">
Sửa
</CButton>
<CButton v-can="'/orders/delete'" variant="primary" danger @click="remove">
Xoá
</CButton>
</template>
</CPageHeader>
<CCard v-if="status === 'pending'">Đang tải…</CCard>
<CEmpty
v-else-if="error"
:description="error.message"
bordered
>
<CButton variant="primary" @click="refresh">Thử lại</CButton>
</CEmpty>
<CEmpty v-else-if="!order" description="Không tìm thấy đơn hàng." bordered />
<CCard v-else title="Thông tin chung">
<CDescriptions :column="2" bordered>
<CDescriptionsItem label="Mã đơn">{{ order.code }}</CDescriptionsItem>
<CDescriptionsItem label="Khách hàng">{{ order.customer }}</CDescriptionsItem>
<CDescriptionsItem label="Giá trị">{{ currency.format(order.amount) }} ₫</CDescriptionsItem>
<CDescriptionsItem label="Trạng thái">
<CTag :color="ORDER_STATUS[order.status].color" dot>
{{ ORDER_STATUS[order.status].label }}
</CTag>
</CDescriptionsItem>
</CDescriptions>
</CCard>
</template>Ba trạng thái phải có
| Trạng thái | Hiển thị |
|---|---|
| Đang tải | Khung chờ hoặc dòng "Đang tải…" — không để trang trắng |
| Lỗi | Thông báo kèm nút thử lại (refresh) |
| Không có dữ liệu | CEmpty với lời giải thích; backend trả null chứ không phải 404 |
Bỏ qua một trong ba là nguồn gốc của "trang trắng không biết vì sao".
Khoá của useAsyncData
ts
// Sai — đổi sang đơn khác vẫn thấy dữ liệu đơn cũ
useAsyncData('order', () => fetchOrder(String(route.params.id)))
// Đúng — khoá đổi theo tham số
useAsyncData(() => `order-${route.params.id}`, () => fetchOrder(String(route.params.id)))Khoá phải chứa mọi tham số ảnh hưởng tới kết quả.
Hành động trên bản ghi
ts
import { message, useConfirm } from '@tasco/ui'
const confirm = useConfirm()
const { removeOrder } = useOrdersApi()
const { $tascoError } = useNuxtApp()
async function remove() {
const ok = await confirm({
title: 'Xoá đơn hàng?',
content: `Đơn ${order.value?.code} sẽ bị xoá vĩnh viễn.`,
okText: 'Xoá',
danger: true,
})
if (!ok) return
try {
await removeOrder(String(route.params.id))
message.success('Đã xoá đơn hàng.')
await navigateTo('/orders')
} catch (caught) {
$tascoError(caught)
}
}Hành động không thể hoàn tác (xoá, huỷ, duyệt) luôn hỏi lại. Nêu rõ đối tượng bị tác động trong câu hỏi, đừng chỉ viết "Bạn có chắc không?".
Sau hành động làm đổi dữ liệu đang xem, gọi refresh() thay vì tự sửa state tại chỗ — tránh lệch với backend.
Bảng con
Danh sách con (dòng hàng của đơn, lịch sử thao tác) dùng lại useTable với fetcher nhận id:
ts
const { fetchOrderItems } = useOrdersApi()
// Đặt bí danh khi trang có nhiều bảng — tránh trùng tên với bảng chính.
const {
dataSource: itemRows,
loading: itemsLoading,
pagination: itemsPagination,
onChange: onItemsChange,
} = useTable<OrderItem>(
(query) => fetchOrderItems(String(route.params.id), query),
{ pageSize: 10, settingsKey: 'orders:items' },
)vue
<CTable
title="Mặt hàng"
row-key="id"
:columns="itemColumns"
:data-source="itemRows"
:loading="itemsLoading"
:pagination="itemsPagination"
@change="onItemsChange"
/>settingsKey của bảng con phải khác bảng chính, nếu không hai bảng ghi đè thiết lập của nhau.
Tab
vue
<script setup lang="ts">
import { CTabPane, CTabs } from '@tasco/ui'
const tab = ref('info')
</script>
<template>
<CTabs v-model:active-key="tab">
<CTabPane key="info" tab="Thông tin">…</CTabPane>
<CTabPane key="items" tab="Mặt hàng">…</CTabPane>
<CTabPane key="history" tab="Lịch sử">…</CTabPane>
</CTabs>
</template>Tab nặng (bảng con, biểu đồ) chỉ nên tải khi được mở lần đầu: đặt immediate: false cho useTable rồi gọi load() trong watch của tab.
Muốn giữ tab qua lần tải lại trang thì đồng bộ với query string (?tab=items) thay vì lưu vào state cục bộ.
Danh sách rà soát
- [ ] Khoá
useAsyncDatachứa tham số route. - [ ] Đủ ba trạng thái: đang tải, lỗi, không có dữ liệu.
- [ ] Breadcrumb dẫn được về danh sách (
@navigate="navigateTo"). - [ ] Hành động phá huỷ có xác nhận và có
v-can. - [ ] Sau hành động thì
refresh(), không tự sửa dữ liệu tại chỗ. - [ ] Bảng con dùng
settingsKeyriêng.
Liên quan
- Trang danh sách — nơi dẫn tới trang này.
- Trang form — màn hình sửa.
- Xử lý lỗi — hiển thị lỗi tải dữ liệu.