本篇在Vue Nuxt 3專案實作分頁查詢(Pagination)。
目前進入[出口業務]頁面可查詢[訂單]列表,但是把結果全部列出,接下來會把查詢結果改成分頁形式。
事前要求
參考「Vue Nuxt 3 抽取下拉選單為元件(Components)」。
修改API
原本查詢[訂單API]檔案server/api/orders.get.js是用JavaScript撰寫,現在改用TypeScript,所以附檔名改為ts,也就是server/api/orders.get.ts。
增加傳入分頁參數page和pageSize。
增加訂單(orders)中的資料數量,並依分頁參數取得分頁結果。
修改回應參數,增加分頁參數total(總筆數)、totalPages(總頁數)、page(目前頁號)和pageSize(分頁大小),並將分頁過的訂單資料放在data。
server/api/orders.get.ts
export default defineEventHandler(async (event) => {
const { status, country, page = "1", pageSize = "5" } = getQuery(event); // 取得查詢參數
await new Promise((r) => setTimeout(r, 2000)); // 模擬延遲
const orders = [
{ id: 10001, status: "pending", country: "US" },
{ id: 10002, status: "done", country: "JP" },
{ id: 10003, status: "pending", country: "CN" },
{ id: 10004, status: "done", country: "KR" },
{ id: 10005, status: "pending", country: "VN" },
{ id: 10006, status: "done", country: "US" },
{ id: 10007, status: "pending", country: "JP" },
{ id: 10008, status: "done", country: "CN" },
{ id: 10009, status: "pending", country: "KR" },
{ id: 10010, status: "done", country: "VN" },
];
let result = orders;
// 根據查詢參數篩選訂單
if (status) {
result = result.filter((o) => o.status === status);
}
if (country) {
result = result.filter((o) => o.country === country);
}
// 分頁
const pageNum = parseInt(page as string, 10);
const size = parseInt(pageSize as string, 10);
const start = (pageNum - 1) * size;
const pagedData = result.slice(start, start + size);
// 計算總頁數
const totalPages = Math.ceil((result.length || 0) / size)
return {
total: result.length,
totalPages: totalPages,
page: pageNum,
pageSize: size,
data: pagedData,
};
});
新增介面
新增types/api/orders.ts,定義Order和OderResponse介面,用於接收[訂單API]的回應資料。
types/api/orders.ts
export interface Order {
id: number
status: string
country: string
}
export interface OrdersResponse {
data: Order[]
total: number
totalPages: number
page: number
pageSize: number
}
修改組合函式
修改composables/useCountries.ts,把[國家狀態]值的資料型態改為<Record<string, string> | null>,因為之後[出口業務]頁面pages/home/export/index.vue會改用TypeScript。
composables/useCountries.ts
export function useCountries() {
const countryMap = useState<Record<string, string> | null>(
"countryMap",
() => null
);
const pending = useState("countryMapPending", () => false);
const error = useState<Error | null>("countryMapError", () => null);
const loadCountries = async () => {
if (countryMap.value) return; // 已有資料就不打API,直接回傳
pending.value = true;
error.value = null;
try {
countryMap.value = await $fetch<Record<string, string>>("/api/countries");
} catch (err) {
error.value = err as Error;
} finally {
pending.value = false;
}
};
return {
countryMap,
pending,
error,
loadCountries,
};
}
修改頁面
修改[出口業務]頁面pages/home/export/index.vue,<script>改為TypeScript及相應變數的資料型態調整。
增加分頁資訊與[上一頁]與[下一頁]按鈕。
修改調用[訂單API] /api/orders,增加傳入分頁及接收分頁參數。
增加[上一頁]與[下一頁]按鈕的click函式prevPage和nextPage,並將分頁查詢字串更新到網址列,這樣重新整理網頁時SSR/hydration後結果也依然相同。
pages/home/export/index.vue
<template>
<div>
<h1>出口業務</h1>
<label>
訂單狀態:
<input v-model="status" placeholder="pending/done">
</label>
<br>
<label>
出口國家:
<CountrySelect v-model="country" />
</label>
<br>
<button @click="search">查詢</button>
<p v-if="orderPending">資料載入中...</p>
<p v-else-if="error" class="error">資料載入失敗</p>
<p v-else-if="result.data.length === 0">沒有訂單資料</p>
<ul v-else>
<li v-for="order in result?.data" :key="order.id">
<NuxtLink :to="`/home/export/${order.id}`">
訂單 #{{ order.id }},
狀態: {{ order.status }},
出口國家:{{ countryMap?.[order.country] ?? order.country }}
</NuxtLink>
</li>
</ul>
<!-- 分頁按鈕 -->
<button @click="prevPage" :disabled="orderPending || currentPage <= 1">上一頁</button>
<span>第 {{ currentPage }} 頁 / 共 {{ totalPages }} 頁</span>
<button @click="nextPage" :disabled="orderPending || currentPage >= totalPages">下一頁</button>
</div>
</template>
<script setup lang="ts">
definePageMeta({
requireAuth: true,
roles: ['admin', 'user']
})
import type { OrdersResponse } from "~/types/api/orders"
const route = useRoute()
const router = useRouter()
const status = ref('')
const country = ref('')
const queryStatus = ref(route.query.status)
const queryCountry = ref(route.query.country)
// 分頁
const currentPage = ref(1)
const pageSize = 5
const queryPage = ref(route.query.page)
const queryPageSize = ref(route.query.pageSize)
const totalPages = ref(0)
const { data: result, pending: orderPending, error, refresh } = useFetch<OrdersResponse>('/api/orders', {
query: {
status: queryStatus,
country: queryCountry,
page: queryPage,
pageSize: queryPageSize
},
default: () => ({ data: [], total: 0, totalPages: 0, page: 1, pageSize }),
})
// 當 result 改變時,把 totalPages 同步到 ref
watch(result, (newVal) => {
if (newVal) {
totalPages.value = newVal.totalPages
}
}, { immediate: true })
watch(() => route.query, (query) => {
status.value = (query.status as string) || ''
country.value = (query.country as string) || ''
currentPage.value = Number(query.page || 1)
}, { immediate: true })
const { countryMap, loadCountries } = useCountries()
loadCountries()
function search() {
queryStatus.value = status.value
queryCountry.value = country.value
currentPage.value = 1
queryPage.value = String(currentPage.value)
queryPageSize.value = String(pageSize)
pushQueryAndRefresh()
}
function prevPage() {
if (currentPage.value <= 1) return
currentPage.value--
updatePageQueryAndRefresh()
}
function nextPage() {
if (currentPage.value >= totalPages.value) return
currentPage.value++
updatePageQueryAndRefresh()
}
function updatePageQueryAndRefresh() {
queryPage.value = String(currentPage.value)
queryPageSize.value = String(pageSize)
pushQueryAndRefresh()
}
function pushQueryAndRefresh() {
const query: Record<string, string> = {}
if (status.value) query.status = String(status.value)
if (country.value) query.country = String(country.value)
if (currentPage.value) query.page = String(currentPage.value)
if (pageSize) query.pageSize = String(pageSize)
router.push({ query })
refresh()
}
</script>
<style scoped>
.error {
color: red;
}
</style>
測試
[出口業務]頁面查詢後的分頁效果如下。
沒有留言:
張貼留言