Appearance
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 需要使用新的值进行覆盖 |
queryKey | QueryKey | 在使用 useInfiniteList 时若不传入 query,会随机生成一组 queryKey,并返回 |
Options
继承 useInfiniteQuery 的所有 options,新增部分如下
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
table | Table | 通过 @ifanrx/uni-mp/io 生成的对象中的 table 部分,如 io.userProfile、io.activity | - |
shouldFetchNextOnReachBottom | boolean | Ref<boolean> | ComputedRef<boolean> | 是否在小程序触底时自动拉取下一页数据 | true |
shouldRefreshOnPullDown | boolean | Ref<boolean> | ComputedRef<boolean> | 是否在小程序触发下拉刷新时重新拉取列表 | false |
enableLoading | boolean | Ref<boolean> | ComputedRef<boolean> | 是否在加载数据时调用 uni.showLoading | true |
loadingOptions | UniApp.ShowLoadingOptions | uni.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 来处理查询条件