AdSense

網頁

2026/1/22

Vue Nuxt 3 分頁

本篇在Vue Nuxt 3專案實作分頁查詢(Pagination)。


目前進入[出口業務]頁面可查詢[訂單]列表,但是把結果全部列出,接下來會把查詢結果改成分頁形式。


事前要求

參考「Vue Nuxt 3 抽取下拉選單為元件(Components)」。


修改API

原本查詢[訂單API]檔案server/api/orders.get.js是用JavaScript撰寫,現在改用TypeScript,所以附檔名改為ts,也就是server/api/orders.get.ts

增加傳入分頁參數pagepageSize

增加訂單(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,定義OrderOderResponse介面,用於接收[訂單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函式prevPagenextPage,並將分頁查詢字串更新到網址列,這樣重新整理網頁時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>

github



測試

[出口業務]頁面查詢後的分頁效果如下。



沒有留言:

AdSense