在 Vue3 中使用 Pinia 管理状态并实现分页功能是一个很好的实践。下面我将介绍如何在 Pinia 中管理分页状态和数据。
首先创建一个专门用于管理分页数据的 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()
}
}
}
})
<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>
要将 Array.from生成的模拟数据替换为从 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"
}
]
根据你放置 data.json的位置,有两种加载方式:
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([])
}
}
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([])
}
}
<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>
如果你更喜欢使用 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([])
}
}
文件位置:
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或后端服务器上
对于大型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 获取分页数据:
// 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()
}
}
}
})
<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,可以这样实现:
<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>
Vue3 + Pinia 实现分页功能
发布时间:2025-9-14
分类:技术
标签: vue 教程 博客 Pinia