Skip to content

useInfiniteList

功能

  • 基于 @tanstack 的 useInfiniteQuery 进行封装
  • 仅支持与 @ifanrx/uni-mp/io 生成的 io 对象配合使用,会根据 io[table].find 获取分页数据,当拉取下一页数据时,会自动根据上一次请求的参数继续请求下一分页
    • 本方法会将 useInfiniteQuery 返回的 data 格式化,只输出列表内容。同时增加 getQueryData 方法可供获取原始的 useInfiniteQuery data
    • 可通过修改 InfiniteListOptions.select 来格式化 data 以满足更多场景
  • 支持触底加载下一页、上拉刷新列表、重置分页信息

API

ts
interface InfiniteListOptions extends UseInfiniteQueryOptions {
  // io 内的 table 对象,如 io.userprofile/io.activity 等,当需要使用其他方式来请求时,需传入 queryFn 参数,具体看下方 example
  table: Table
  /** 是否在小程序触底时自动拉取下一页数据,默认 true */
  shouldFetchNextOnReachBottom: boolean | Ref<boolean> | ComputedRef<boolean>
  /** 是否在小程序触发下拉刷新时重新拉取列表,默认 false */
  shouldRefreshOnPullDown: boolean | Ref<boolean> | ComputedRef<boolean>
  /** 是否在加载数据时调用 showLoading,默认 true */
  enableLoading: boolean | Ref<boolean> | ComputedRef<boolean>
  /** 默认值:{title: '', mask: false} */
  loadingOptions: UniApp.ShowLoadingOptions
}

interface InfiniteListResult
  extends Omit<UseInfiniteQueryReturnType<unknown, unknown>, 'data'> {
  data: Ref<any[]>
  queryKey: QueryKey
  refresh: (params: ServiceParams) => any
  getQueryData: () => {pages: any[]; pageParams: any[]}
  setQueryData: (updater) => void
}
export default function useInfiniteList(
  options: InfiniteListOptions
): InfiniteListResult

Result

继承 useInfiniteQuery 的所有 result,新增部分如下

参数类型说明
getQueryData() => ({pages: any[], pageParams: any[]})获取 useInfiniteQuery 的原始 data
setQueryData(updater) => any基于 queryClient.setQueryData 封装,可用于直接修改 data
refresh(pageParam) => void重置分页信息并以 pageParam 作为第一页的请求参数刷新列表,若不传入 pageParam,会取 initialPageParam 作为第一页的请求参数。使用 refresh 时,params 会自动拼接上 initialPageParam 里面的参数,如果不再需要 initialPageParam 需要使用新的值进行覆盖
queryKeyQueryKey在使用 useInfiniteList 时若不传入 query,会随机生成一组 queryKey,并返回

Options

继承 useInfiniteQuery 的所有 options,新增部分如下

参数类型说明默认值
tableTable通过 @ifanrx/uni-mp/io 生成的对象中的 table 部分,如 io.userProfile、io.activity-
shouldFetchNextOnReachBottomboolean | Ref<boolean> | ComputedRef<boolean>是否在小程序触底时自动拉取下一页数据true
shouldRefreshOnPullDownboolean | Ref<boolean> | ComputedRef<boolean>是否在小程序触发下拉刷新时重新拉取列表false
enableLoadingboolean | Ref<boolean> | ComputedRef<boolean>是否在加载数据时调用 uni.showLoadingtrue
loadingOptionsUniApp.ShowLoadingOptionsuni.showLoading 配置{title: '', mask: false}
select(list: any[]) => any[]与 useInfiniteQueryOptions.select 不同,useInfiniteListOptions.select 的参数是抹平后的数据列表,与 useInfiniteListResult.data 保持一致的结构

Example

js
const {data, getQueryData, refresh} = useInfiniteList({
  table: io.activity,
  // 这里可以传请求参数,包括 query
  initialPageParam: {
    offset: 0,
    limit: 20,
    query: io.query.compare('status', '=', 'xxxx'),
  },
})

console.log(getQueryData()) // 获取原始 useInfiniteQuery data

// 重置查询条件和分页信息
function onParamsChange(params) {
  refresh({
    offset: 0,
    limit: 20,
    query: io.query.compare(name, '=', params.name),
  })
}

手动加载首页数据

js
const enabled = ref(false)

const {refresh} = useInfiniteList({
  table: io.activity,
  enabled,
})

function onSearch(params) {
  enabled.value = true
  const query = io.query.compare('name', '=', params.name)
  refresh({query})
}

<Button @click="onSearch">搜索</Button>

在加载数据时启用 uni.showLoading

js
const {data} = useInfiniteList({
  table: io.activity,
  enableLoading: true,
  loadingOptions: {title: '加载中', mask: true},
})

开启触底刷新、下拉刷新

js
const {data} = useInfiniteList({
  table: io.activity,
  shouldFetchNextOnReachBottom: true,
  shouldRefreshOnPullDown: true,
})

自定义 queryFn

当使用场景不是通过 io.xxx.find 来拉取列表数据时,可以通过自定义 queryFn 来修改请求行为

需要保证 queryFn 返回的对象与知晓云的数据列表结构一致,即 queryFn 返回的结构要与以下结构一致:

  • {objects: array, meta: object} 点我查看相关资料
    • objects 是列表数据
    • meta 用于判断是否存在下一页数据

example

js
import {useInfiniteList} from '@ifanrx/uni-mp'

import io from '@/io'

const {
  data: records,
  isFetching,
  isFetched,
} = useInfiniteList({
  queryFn: ({pageParam}) => {
    return io.faas.getFollowers(pageParam)
  },
  initialPageParam: {limit: 20, offset: 0},
})

常见场景示例(欢迎随时补充)

TIP

一般来说,视图层对列表的渲染取决于 useInfiniteList 返回的 data:

当 data 为 undefined 时表示正在请求中,一般用于显示骨架屏(非必选)

当 data.length 为 0 时,表示数据列表为空,一般用于显示 empty-placeholder(非必选)

当 data.length 不为 0 时,正常渲染列表

如果需要显示错误信息,可通过 watch isError(非必选)

vue
<script setup>
import {watch} from 'vue'
import {isUndef} from 'licia'
import {useInfiniteList, message} from '@ifanrx/uni-mp'

const {data, isError} = useInfiniteList({
  table: io.activity,
})

watch(isError, error => {
  if (error) {
    message.showToast('数据获取失败')
  }
})
</script>

<template>
  <skeleton v-if="isUndef(data)" />
  <empty-placeholder v-else-if="data.length === 0" />
  <data-list v-else />
</template>

普通列表

vue
<script setup>
import {useInfiniteList} from '@ifanrx/uni-mp'

import io from '@/io'

import NoticeCard from './notice-card.vue'

const {data: notices} = useInfiniteList({
  table: io.notice,
  initialPageParam: {
    expand: 'created_by',
  },
})
</script>

<template>
  <mp-nav-bar
    background-color="transparent"
    enable-scroll-change-background
    :scroll-change-background-directly="false"
    :scroll-threshold="44"
    title="我的消息"
  />

  <empty-placeholder v-if="notices?.length === 0" class="empty" />
  <view v-else-if="notices?.length > 0">
    <notice-card
      v-for="notice in notices"
      :key="notice.id"
      class="notice-card"
      :notice="notice"
    />
  </view>
</template>

<style lang="scss" scoped>
:global(page) {
  background-color: #f7f7f7;
}

.notice-card :deep(.notice) {
  margin-bottom: 30rpx;
}

.notice-card:first-child :deep(.notice) {
  margin-top: 10rpx;
}

.notice-card:last-child :deep(.notice) {
  margin-bottom: 0;
}
</style>

包含 tab 切换的列表

vue
<script setup>
import {useInfiniteList} from '@ifanrx/uni-mp'
import {ref} from 'vue'

import io from '@/io'

import NoticeCard from './notice-card.vue'

const NOTICE_SOURCE = {
  SYSTEM: 'system',
  USER: 'user',
}

const NOTICE_SOURCE_TEXT_MAP = {
  [NOTICE_SOURCE.SYSTEM]: '系统消息',
  [NOTICE_SOURCE.USER]: '其他用户消息',
}

const TABS = Object.entries(NOTICE_SOURCE_TEXT_MAP).map(([key, label]) => ({
  key,
  label,
}))
const activeTabIndex = ref(0)

function onTabItemClick(e) {
  activeTabIndex.value = e.currentIndex
  refresh({
    query: io.queryAnd(
      noticeQuery,
      io.query.compare('source', '=', TABS[e.currentIndex].key)
    ),
  })
}

// 用于复杂查询
const noticeQuery = io.query.exists('created_by')

const {data: notices, refresh} = useInfiniteList({
  table: io.notice,
  initialPageParam: {
    expand: 'created_by',
    orderBy: 'created_at',
    query: io.queryAnd(
      noticeQuery,
      io.query.compare('source', '=', TABS[activeTabIndex.value].key)
    ),
  },
})
</script>

<template>
  <mp-nav-bar
    background-color="transparent"
    enable-scroll-change-background
    :scroll-change-background-directly="false"
    :scroll-threshold="44"
    title="我的消息"
  />

  <uni-segmented-control
    :current="activeTabIndex"
    style-type="text"
    :values="TABS.map(({label}) => label)"
    @click-item="onTabItemClick"
  />

  <empty-placeholder v-if="notices?.length === 0" class="empty" />
  <view v-else-if="notices?.length > 0">
    <notice-card
      v-for="notice in notices"
      :key="notice.id"
      class="notice-card"
      :notice="notice"
    />
  </view>
</template>

<style lang="scss" scoped>
:global(page) {
  background: #f7f7f7;
}

.notice-card :deep(.notice) {
  margin-bottom: 30rpx;
}

.notice-card:first-child :deep(.notice) {
  margin-top: 30rpx;
}

.notice-card:last-child :deep(.notice) {
  margin-bottom: 0;
}
</style>

包含搜索条件的列表

vue
<script setup>
import {useInfiniteList} from '@ifanrx/uni-mp'
import {reactive} from 'vue'

import io from '@/io'

import NoticeCard from './notice-card.vue'

const NOTICE_SOURCE = {
  SYSTEM: 'system',
  USER: 'user',
}

const NOTICE_SOURCE_TEXT_MAP = {
  [NOTICE_SOURCE.SYSTEM]: '系统消息',
  [NOTICE_SOURCE.USER]: '其他用户消息',
}

const SOURCE_RANGE = Object.entries(NOTICE_SOURCE_TEXT_MAP).map(
  ([value, text]) => ({value, text})
)

const form = reactive({
  keywords: undefined,
  source: undefined,
})

// 用于复杂查询
const noticeQuery = io.query.exists('created_by')

const {data: notices, refresh} = useInfiniteList({
  table: io.notice,
  initialPageParam: {
    expand: 'created_by',
    orderBy: 'created_at',
    query: noticeQuery,
  },
})

function search() {
  const {query} = io
  if (form.keywords) {
    query.contains('message', form.keywords)
  }
  if (form.source) {
    query.compare('source', '=', form.source)
  }
  refresh({
    query: io.queryAnd(noticeQuery, query),
  })
}
</script>

<template>
  <mp-nav-bar
    background-color="transparent"
    enable-scroll-change-background
    :scroll-change-background-directly="false"
    :scroll-threshold="44"
    title="我的消息"
  />

  <view class="form">
    <uni-easyinput
      v-model="form.keywords"
      class="input"
      placeholder="请输入内容"
    />
    <uni-data-select
      v-model="form.source"
      class="select"
      :localdata="SOURCE_RANGE"
    />
    <view class="search" @click="search">搜索</view>
  </view>

  <empty-placeholder v-if="notices?.length === 0" class="empty" />
  <view v-else-if="notices?.length > 0">
    <notice-card
      v-for="notice in notices"
      :key="notice.id"
      class="notice-card"
      :notice="notice"
    />
  </view>
</template>

<style lang="scss" scoped>
:global(page) {
  background: #f7f7f7;
}

.notice-card :deep(.notice) {
  margin-bottom: 30rpx;
}

.notice-card:first-child :deep(.notice) {
  margin-top: 30rpx;
}

.notice-card:last-child :deep(.notice) {
  margin-bottom: 0;
}

.form {
  padding: 0 20rpx;
  display: flex;
  justify-content: space-between;
}

.input {
  flex: 0 0 40%;
}

.select {
  flex: 0 0 40%;
  padding: 0 16rpx;
}

.search {
  flex: 0 0 15%;
  font-size: 28rpx;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

自定义列表 queryFn

vue
<script setup>
import {useInfiniteList} from '@ifanrx/uni-mp'

import io from '@/io'

const {data: notices} = useInfiniteList({
  queryFn: ({pageParam}) => {
    /** 云函数/接口返回的结构应该满足 {objects: array, meta: object} */
    return io.faas.getFollowers(pageParam)
  },
  initialPageParam: {
    offset: 0,
    limit: 20,
    expand: 'created_by',
  },
})
</script>

<template>
  <mp-nav-bar
    background-color="transparent"
    enable-scroll-change-background
    :scroll-change-background-directly="false"
    :scroll-threshold="44"
    title="我的消息"
  />

  <empty-placeholder v-if="notices?.length === 0" class="empty" />
  <view v-else-if="notices?.length > 0">
    <notice-card
      v-for="notice in notices"
      :key="notice.id"
      class="notice-card"
      :notice="notice"
    />
  </view>
</template>

<style lang="scss" scoped>
:global(page) {
  background: #f7f7f7;
}

.notice-card :deep(.notice) {
  margin-bottom: 30rpx;
}

.notice-card:first-child :deep(.notice) {
  margin-top: 10rpx;
}

.notice-card:last-child :deep(.notice) {
  margin-bottom: 0;
}
</style>

更新数据源

可以通过 setQueryData 手动更新当前数据源:

js
// case 1:清空数据
const {
  ...
  setQueryData,
} = useInfiniteList(...)

// 可直接传递
setQueryData({
  pageParams: [],
  pages: [],
})

// case 2:修改某一项
const {
  ...
  setQueryData,
} = useInfiniteList(...)

// 可通过回调函数
setQueryData(data => {
  return {
    ...data,
    pages: data.pages.map(row => {
      if (row.id === 'xxx') {
        return {...row, value: 'new value'}
      }

      return row
    }),
  }
})

注意事项

正常情况来说,列表的查询参数都是可以通过 useInfiniteList 的 initialPageParam 或者通过 refresh({query: xxx}) 来传入,即在外部计算得出 query 实例后,再传给 useInfiniteList,而不是直接修改 queryFn 来处理查询条件

源码

use-infinite-list/index.js