Vue3使用Fetch读取json数据并在Pinia分页

下面是一个完整的实现方案,使用 Vue3 的 fetch API 读取 public 目录下的 data.json 文件,并在 Pinia 存储中使用组合式 API 实现分页功能。

1. 创建 Pinia 存储

首先,创建一个 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
  }
})

2. 在组件中使用

在 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>

3. 准备 data.json 文件

确保你的 public 目录下有一个 data.json 文件,格式类似于:

[
  {"id": 1, "name": "项目1", "description": "描述1"},
  {"id": 2, "name": "项目2", "description": "描述2"},
  // 更多数据...
]

4. 主文件配置

确保你的 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 存储和组件逻辑。下面是完整的实现方案:

1. 修改 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
  }
})

2. 修改组件实现无限滚动

<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>

3. 可选优化:使用 Intersection Observer API

为了更精确地检测滚动到底部,可以使用 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>

4. 添加加载动画

可以添加一个漂亮的加载动画来提升用户体验:

<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 中的数据量足够大以展示无限滚动效果

对于真实项目,可能需要添加防抖或节流来优化滚动事件处理

这个实现提供了流畅的无限滚动体验,同时保持了代码的简洁和可维护性。