下面是一个完整的实现方案,使用 Vue3 的 fetch API 读取 public 目录下的 data.json 文件,并在 Pinia 存储中使用组合式 API 实现分页功能。
首先,创建一个 Pinia 存储来处理数据获取和分页逻辑:
// stores/dataStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useDataStore = defineStore('data', () => {
// 状态
const data = ref([])
const loading = ref(false)
const error = ref(null)
const currentPage = ref(1)
const itemsPerPage = ref(10)
// 计算属性
const totalItems = computed(() => data.value.length)
const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage.value))
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage.value
const end = start + itemsPerPage.value
return data.value.slice(start, end)
})
// 操作
const fetchData = async () => {
try {
loading.value = true
error.value = null
const response = await fetch('/data.json')
if (!response.ok) throw new Error('Network response was not ok')
data.value = await response.json()
} catch (err) {
error.value = err.message
console.error('Error fetching data:', err)
} finally {
loading.value = false
}
}
const setPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
const setItemsPerPage = (count) => {
itemsPerPage.value = count
currentPage.value = 1 // 重置到第一页
}
return {
data,
loading,
error,
currentPage,
itemsPerPage,
totalItems,
totalPages,
paginatedData,
fetchData,
setPage,
setItemsPerPage
}
})
在 Vue 组件中使用这个存储:
<template>
<div>
<h1>数据分页展示</h1>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<div v-else>
<!-- 分页控件 -->
<div class="pagination-controls">
<button @click="setPage(currentPage - 1)" :disabled="currentPage === 1">
上一页
</button>
<span>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
<button @click="setPage(currentPage + 1)" :disabled="currentPage === totalPages">
下一页
</button>
<select v-model="itemsPerPage" @change="handleItemsPerPageChange">
<option value="5">每页 5 条</option>
<option value="10">每页 10 条</option>
<option value="20">每页 20 条</option>
<option value="50">每页 50 条</option>
</select>
</div>
<!-- 数据表格 -->
<table>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<!-- 根据你的实际数据添加更多列 -->
</tr>
</thead>
<tbody>
<tr v-for="item in paginatedData" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<!-- 根据你的实际数据添加更多列 -->
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { useDataStore } from '@/stores/dataStore'
import { storeToRefs } from 'pinia'
import { onMounted } from 'vue'
const dataStore = useDataStore()
// 从 store 中解构需要的状态和方法
const {
loading,
error,
currentPage,
itemsPerPage,
totalPages,
paginatedData,
fetchData,
setPage,
setItemsPerPage
} = storeToRefs(dataStore)
// 处理每页显示数量变化
const handleItemsPerPageChange = () => {
dataStore.setItemsPerPage(Number(itemsPerPage.value))
}
// 组件挂载时获取数据
onMounted(() => {
if (dataStore.data.length === 0) {
dataStore.fetchData()
}
})
</script>
<style scoped>
.pagination-controls {
margin: 20px 0;
display: flex;
align-items: center;
gap: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
确保你的 public 目录下有一个 data.json 文件,格式类似于:
[
{"id": 1, "name": "项目1", "description": "描述1"},
{"id": 2, "name": "项目2", "description": "描述2"},
// 更多数据...
]
确保你的 main.js 或 main.ts 正确配置了 Pinia:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
如果需要更复杂的功能,可以考虑:
搜索过滤:在存储中添加搜索功能,过滤后再分页
排序:添加排序功能
无限滚动:替代传统分页
缓存:添加数据缓存机制避免重复请求
错误重试:添加错误重试逻辑
这个实现使用了 Vue3 的组合式 API 和 Pinia 的状态管理,提供了响应式的分页功能,并且通过 fetch 从 public 目录获取 JSON 数据。
使用无限滚动替代传统分页的实现方案
要将上面的传统分页改为无限滚动,我们需要修改 Pinia 存储和组件逻辑。下面是完整的实现方案:
// stores/dataStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useDataStore = defineStore('data', () => {
// 状态
const data = ref([])
const loading = ref(false)
const error = ref(null)
const visibleItems = ref([])
const itemsPerLoad = ref(10) // 每次加载的条目数
const loadedItemsCount = ref(0) // 已加载的条目数
// 计算属性
const hasMore = computed(() => loadedItemsCount.value < data.value.length)
// 操作
const fetchData = async () => {
try {
loading.value = true
error.value = null
const response = await fetch('/data.json')
if (!response.ok) throw new Error('Network response was not ok')
data.value = await response.json()
loadMore() // 初始加载
} catch (err) {
error.value = err.message
console.error('Error fetching data:', err)
} finally {
loading.value = false
}
}
const loadMore = () => {
if (!hasMore.value) return
const start = loadedItemsCount.value
const end = start + itemsPerLoad.value
const newItems = data.value.slice(start, end)
visibleItems.value = [...visibleItems.value, ...newItems]
loadedItemsCount.value = end
}
const reset = () => {
visibleItems.value = []
loadedItemsCount.value = 0
}
return {
data,
loading,
error,
visibleItems,
itemsPerLoad,
hasMore,
fetchData,
loadMore,
reset
}
})
<template>
<div>
<h1>无限滚动数据展示</h1>
<div v-if="loading && loadedItemsCount === 0">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<div v-else>
<!-- 数据列表 -->
<div class="data-list">
<div v-for="item in visibleItems" :key="item.id" class="data-item">
<div>ID: {{ item.id }}</div>
<div>名称: {{ item.name }}</div>
<!-- 根据你的实际数据添加更多字段 -->
</div>
</div>
<!-- 加载更多指示器 -->
<div v-if="loading" class="loading-more">
加载更多数据...
</div>
<div v-else-if="!hasMore" class="no-more">
没有更多数据了
</div>
</div>
</div>
</template>
<script setup>
import { useDataStore } from '@/stores/dataStore'
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted } from 'vue'
const dataStore = useDataStore()
// 从 store 中解构需要的状态和方法
const {
loading,
error,
visibleItems,
hasMore,
fetchData,
loadMore
} = storeToRefs(dataStore)
// 滚动事件处理
const handleScroll = () => {
const { scrollTop, clientHeight, scrollHeight } = document.documentElement
const scrollThreshold = 100 // 距离底部多少像素时触发加载
if (scrollTop + clientHeight >= scrollHeight - scrollThreshold && hasMore.value && !loading.value) {
dataStore.loadMore()
}
}
// 组件挂载时获取数据并添加滚动监听
onMounted(() => {
if (dataStore.data.length === 0) {
dataStore.fetchData()
}
window.addEventListener('scroll', handleScroll)
})
// 组件卸载时移除滚动监听
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>
<style scoped>
.data-list {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
}
.data-item {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background: #f9f9f9;
}
.loading-more, .no-more {
padding: 15px;
text-align: center;
color: #666;
}
.loading-more {
font-weight: bold;
}
</style>
为了更精确地检测滚动到底部,可以使用 Intersection Observer API:
<template>
<!-- 其他内容不变 -->
<div ref="loader" class="loader"></div>
</template>
<script setup>
import { ref } from 'vue'
const loader = ref(null)
// 替换原来的 handleScroll 方法
const setupInfiniteScroll = () => {
const observer = new IntersectionObserver((entries) => {
const target = entries[0]
if (target.isIntersecting && hasMore.value && !loading.value) {
dataStore.loadMore()
}
}, {
root: null,
rootMargin: '20px',
threshold: 0.1
})
if (loader.value) {
observer.observe(loader.value)
}
onUnmounted(() => {
if (loader.value) {
observer.unobserve(loader.value)
}
})
}
onMounted(() => {
if (dataStore.data.length === 0) {
dataStore.fetchData()
}
setupInfiniteScroll()
})
</script>
<style scoped>
.loader {
height: 1px;
}
</style>
可以添加一个漂亮的加载动画来提升用户体验:
<template>
<!-- 替换原来的加载更多指示器 -->
<div v-if="loading" class="loading-spinner">
<div class="spinner"></div>
</div>
</template>
<style scoped>
.loading-spinner {
display: flex;
justify-content: center;
padding: 20px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
核心逻辑变化:
移除了传统分页的页码概念
改为跟踪已加载的项目数量
每次滚动到底部时加载更多项目
只渲染可见的项目(虽然在这个实现中我们仍然保留了所有已加载项目)
可以考虑使用虚拟滚动技术处理大量数据
用户体验优化:
添加加载状态指示器
防止重复加载
提供没有更多数据的提示
确保 data.json 中的数据量足够大以展示无限滚动效果
对于真实项目,可能需要添加防抖或节流来优化滚动事件处理
这个实现提供了流畅的无限滚动体验,同时保持了代码的简洁和可维护性。
Vue3使用Fetch读取json数据并在Pinia分页
发布时间:2025-9-14
分类:技术
标签: vue 教程 博客