Vue3 + Pinia 实现分页功能

在 Vue3 中使用 Pinia 管理状态并实现分页功能是一个很好的实践。下面我将介绍如何在 Pinia 中管理分页状态和数据。

基本实现步骤

1. 创建 Pinia store

首先创建一个专门用于管理分页数据的 store:

// stores/paginationStore.js
import { defineStore } from 'pinia'

export const usePaginationStore = defineStore('pagination', {
  state: () => ({
    allItems: [],      // 所有数据
    currentItems: [],   // 当前页数据
    currentPage: 1,    // 当前页码
    pageSize: 10,      // 每页条数
    totalItems: 0      // 总条数
  }),
  getters: {
    totalPages: (state) => Math.ceil(state.totalItems / state.pageSize)
  },
  actions: {
    // 设置所有数据
    setAllItems(items) {
      this.allItems = items
      this.totalItems = items.length
      this.updateCurrentItems()
    },

    // 更新当前页数据
    updateCurrentItems() {
      const start = (this.currentPage - 1) * this.pageSize
      const end = start + this.pageSize
      this.currentItems = this.allItems.slice(start, end)
    },

    // 改变页码
    setPage(page) {
      if (page >= 1 && page <= this.totalPages) {
        this.currentPage = page
        this.updateCurrentItems()
      }
    },

    // 改变每页条数
    setPageSize(size) {
      this.pageSize = size
      this.currentPage = 1 // 重置到第一页
      this.updateCurrentItems()
    },

    // 下一页
    nextPage() {
      if (this.currentPage < this.totalPages) {
        this.currentPage++
        this.updateCurrentItems()
      }
    },

    // 上一页
    prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--
        this.updateCurrentItems()
      }
    }
  }
})

2. 在组件中使用

<template>
  <div>
    <!-- 显示当前页数据 -->
    <ul>
      <li v-for="item in pagination.currentItems" :key="item.id">
        {{ item.name }}
      </li>
    </ul>

    <!-- 分页控件 -->
    <div class="pagination-controls">
      <button @click="pagination.prevPage" :disabled="pagination.currentPage === 1">
        上一页
      </button>

      <span v-for="page in pagination.totalPages" :key="page">
        <button 
          @click="pagination.setPage(page)"
          :class="{ active: pagination.currentPage === page }">
          {{ page }}
        </button>
      </span>

      <button @click="pagination.nextPage" :disabled="pagination.currentPage === pagination.totalPages">
        下一页
      </button>

      <select v-model="pageSize" @change="handlePageSizeChange">
        <option value="5">5 条/页</option>
        <option value="10">10 条/页</option>
        <option value="20">20 条/页</option>
        <option value="50">50 条/页</option>
      </select>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { usePaginationStore } from '@/stores/paginationStore'

const pagination = usePaginationStore()
const pageSize = ref(pagination.pageSize)

// 模拟获取数据
const fetchData = async () => {
  // 实际项目中这里可能是 API 调用
  const mockData = Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `Item ${i + 1}`,
    description: `This is item number ${i + 1}`
  }))

  pagination.setAllItems(mockData)
}

const handlePageSizeChange = (e) => {
  const size = parseInt(e.target.value)
  pagination.setPageSize(size)
}

onMounted(() => {
  fetchData()
})
</script>

<style>
.pagination-controls {
  margin-top: 20px;
  display: flex;
  gap: 10px;
  align-items: center;
}

.pagination-controls button {
  padding: 5px 10px;
  cursor: pointer;
}

.pagination-controls button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.pagination-controls button.active {
  font-weight: bold;
  background-color: #eee;
}

select {
  padding: 5px;
}
</style>

将模拟数据替换为 data.json 文件

要将 Array.from生成的模拟数据替换为从 data.json文件加载的真实数据,你需要以下几个步骤:

1. 创建 data.json 文件

首先,在项目的 public或 src/assets目录下创建 data.json文件:

// public/data.json 或 src/assets/data.json
[
  {
    "id": 1,
    "name": "Item 1",
    "description": "This is item number 1"
  },
  {
    "id": 2,
    "name": "Item 2",
    "description": "This is item number 2"
  },
  // ... 更多数据项
  {
    "id": 100,
    "name": "Item 100",
    "description": "This is item number 100"
  }
]

2. 修改 fetchData 函数

根据你放置 data.json的位置,有两种加载方式:

方式一:放在 public 目录下

const fetchData = async () => {
  try {
    // 从public目录加载
    const response = await fetch('/data.json')
    if (!response.ok) {
      throw new Error('Failed to fetch data')
    }
    const jsonData = await response.json()
    pagination.setAllItems(jsonData)
  } catch (error) {
    console.error('Error loading data:', error)
    // 可以设置错误状态或回退到空数组
    pagination.setAllItems([])
  }
}

方式二:放在 src/assets 目录下(推荐)

const fetchData = async () => {
  try {
    // 从assets目录加载(需要Vite/webpack等打包工具支持)
    const jsonData = await import('@/assets/data.json')
    pagination.setAllItems(jsonData.default || jsonData)
  } catch (error) {
    console.error('Error loading data:', error)
    // 可以设置错误状态或回退到空数组
    pagination.setAllItems([])
  }
}

3. 完整组件示例

<template>
  <div>
    <ul>
      <li v-for="item in pagination.currentItems" :key="item.id">
        {{ item.name }} - {{ item.description }}
      </li>
    </ul>

    <div class="pagination-controls">
      <button @click="pagination.prevPage" :disabled="pagination.currentPage === 1">
        上一页
      </button>
      <span>第 {{ pagination.currentPage }} 页 / 共 {{ pagination.totalPages }} 页</span>
      <button @click="pagination.nextPage" :disabled="pagination.currentPage === pagination.totalPages">
        下一页
      </button>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { usePaginationStore } from '@/stores/paginationStore'

const pagination = usePaginationStore()

const fetchData = async () => {
  try {
    // 方法1:从public目录加载
    // const response = await fetch('/data.json')
    // if (!response.ok) throw new Error('Failed to fetch data')
    // const jsonData = await response.json()

    // 方法2:从assets目录加载(推荐)
    const jsonData = await import('@/assets/data.json')

    pagination.setAllItems(jsonData.default || jsonData)
  } catch (error) {
    console.error('Error loading data:', error)
    pagination.setAllItems([])
  }
}

onMounted(() => {
  fetchData()
})
</script>

4. 使用 axios 替代 fetch

如果你更喜欢使用 axios:

import axios from 'axios'

const fetchData = async () => {
  try {
    // 从public目录加载
    // const response = await axios.get('/data.json')

    // 从assets目录加载
    const response = await axios.get('/src/assets/data.json')

    pagination.setAllItems(response.data)
  } catch (error) {
    console.error('Error loading data:', error)
    pagination.setAllItems([])
  }
}

5. 注意事项

​​文件位置​​:

public/data.json:文件会被直接复制到构建目录,适合较大的数据文件
src/assets/data.json:文件会被打包工具处理,适合较小的数据文件

​​热更新​​:

修改 public/data.json可能需要手动刷新页面
修改 src/assets/data.json会触发Vite/webpack的热更新

​​类型提示(TypeScript)​​:

如果你使用TypeScript,可以创建一个类型定义:

// types/data.d.ts
declare module '*.json' {
  const value: Array<{
    id: number
    name: string
    description: string
  }>
  export default value
}

​​生产环境​​:

在生产环境中,建议使用真正的API端点而不是静态JSON文件
可以将JSON文件放在CDN或后端服务器上

6. 替代方案:动态导入

对于大型JSON文件,可以考虑使用动态导入:

const fetchData = async () => {
  try {
    // 动态导入,会被代码分割
    const jsonModule = await import('@/assets/data.json?raw')
    const jsonData = JSON.parse(jsonModule.default)
    pagination.setAllItems(jsonData)
  } catch (error) {
    console.error('Error loading data:', error)
    pagination.setAllItems([])
  }
}

这样配置后,你就可以完全移除 Array.from生成模拟数据的代码,转而使用真实的JSON数据文件了。

高级实现:结合 API 分页

对于实际项目,通常需要从后端 API 获取分页数据:

1. 更新 Pinia store

// stores/paginationStore.js
import { defineStore } from 'pinia'
import axios from 'axios'

export const usePaginationStore = defineStore('pagination', {
  state: () => ({
    currentItems: [],   // 当前页数据
    currentPage: 1,    // 当前页码
    pageSize: 10,      // 每页条数
    totalItems: 0,     // 总条数
    isLoading: false,  // 加载状态
    error: null        // 错误信息
  }),
  getters: {
    totalPages: (state) => Math.ceil(state.totalItems / state.pageSize)
  },
  actions: {
    // 从API获取数据
    async fetchItems() {
      this.isLoading = true
      this.error = null

      try {
        const response = await axios.get('/api/items', {
          params: {
            page: this.currentPage,
            size: this.pageSize
          }
        })

        this.currentItems = response.data.items
        this.totalItems = response.data.total
      } catch (err) {
        this.error = err.message || 'Failed to fetch items'
      } finally {
        this.isLoading = false
      }
    },

    // 改变页码
    async setPage(page) {
      if (page >= 1 && page <= this.totalPages) {
        this.currentPage = page
        await this.fetchItems()
      }
    },

    // 改变每页条数
    async setPageSize(size) {
      this.pageSize = size
      this.currentPage = 1
      await this.fetchItems()
    },

    // 下一页
    async nextPage() {
      if (this.currentPage < this.totalPages) {
        this.currentPage++
        await this.fetchItems()
      }
    },

    // 上一页
    async prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--
        await this.fetchItems()
      }
    }
  }
})

2. 更新组件

<template>
  <div>
    <!-- 加载状态 -->
    <div v-if="pagination.isLoading" class="loading">加载中...</div>

    <!-- 错误信息 -->
    <div v-if="pagination.error" class="error">{{ pagination.error }}</div>

    <!-- 数据列表 -->
    <ul v-if="!pagination.isLoading && !pagination.error">
      <li v-for="item in pagination.currentItems" :key="item.id">
        {{ item.name }}
      </li>
    </ul>

    <!-- 分页控件 -->
    <div class="pagination-controls">
      <button @click="pagination.prevPage" :disabled="pagination.currentPage === 1 || pagination.isLoading">
        上一页
      </button>

      <span>第 {{ pagination.currentPage }} 页 / 共 {{ pagination.totalPages }} 页</span>

      <button @click="pagination.nextPage" :disabled="pagination.currentPage === pagination.totalPages || pagination.isLoading">
        下一页
      </button>

      <select v-model="pageSize" @change="handlePageSizeChange" :disabled="pagination.isLoading">
        <option value="5">5 条/页</option>
        <option value="10">10 条/页</option>
        <option value="20">20 条/页</option>
        <option value="50">50 条/页</option>
      </select>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { usePaginationStore } from '@/stores/paginationStore'

const pagination = usePaginationStore()
const pageSize = ref(pagination.pageSize)

const handlePageSizeChange = (e) => {
  const size = parseInt(e.target.value)
  pagination.setPageSize(size)
}

onMounted(() => {
  pagination.fetchItems()
})
</script>

<style>
.loading, .error {
  padding: 10px;
  margin: 10px 0;
}

.error {
  color: red;
}

.pagination-controls {
  margin-top: 20px;
  display: flex;
  gap: 10px;
  align-items: center;
}

.pagination-controls button {
  padding: 5px 10px;
  cursor: pointer;
}

.pagination-controls button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

select {
  padding: 5px;
}
</style>

使用 Element Plus 分页组件

如果你使用 Element Plus,可以这样实现:

<template>
  <div>
    <el-table :data="pagination.currentItems" v-loading="pagination.isLoading">
      <el-table-column prop="name" label="名称" />
      <el-table-column prop="description" label="描述" />
    </el-table>

    <el-pagination
      @size-change="pagination.setPageSize"
      @current-change="pagination.setPage"
      :current-page="pagination.currentPage"
      :page-sizes="[5, 10, 20, 50]"
      :page-size="pagination.pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="pagination.totalItems"
      :disabled="pagination.isLoading"
    />
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { usePaginationStore } from '@/stores/paginationStore'

const pagination = usePaginationStore()

onMounted(() => {
  pagination.fetchItems()
})
</script>

总结

  • ​Pinia 管理状态​​:将分页相关状态(当前页、每页条数、总条数等)和逻辑(翻页、改变条数等)集中在 store 中
  • ​组件只负责展示​​:组件只需调用 store 的方法和获取 store 的状态
  • ​支持前端和后端分页​​:根据项目需求选择合适的分页方式
  • ​良好的用户体验​​:添加加载状态和错误处理
  • ​可复用性​​:分页逻辑可以在多个组件中复用
  • 这种方法使得分页逻辑与组件解耦,提高了代码的可维护性和复用性。