RequestList 列表组件系统
强大的 React 列表组件库,支持无限滚动、虚拟滚动、分页、选择、分组、排序等功能。
导入
// 核心组件
import { InfiniteList, PaginatedList, RequestList } from '@xcloud/ui-mobile'
// 增强组件
import {
VirtualInfiniteList,
SelectableList,
GroupedList,
SortableList,
PullToRefresh,
SwipeableListItem,
} from '@xcloud/ui-mobile'
// Hooks
import {
useInfiniteList,
usePaginatedList,
useListSelection,
useListFilters,
useListMutations,
} from '@xcloud/ui-mobile'核心组件
InfiniteList - 无限滚动列表
基础的无限滚动列表组件,自动触底加载更多数据:
import { InfiniteList } from '@xcloud/ui-mobile'
<InfiniteList
queryKey={['products']}
queryFn={async ({ pageParam = 1 }) => {
const res = await fetch(`/api/products?page=${pageParam}`)
return res.json()
}}
renderItem={(product) => <ProductCard product={product} />}
emptyText="暂无商品"
/>PaginatedList - 分页列表
传统的页码分页列表:
<PaginatedList
queryKey={['orders']}
queryFn={fetchOrders}
pageSize={20}
renderItem={(order) => <OrderItem order={order} />}
showPagination
/>增强组件
VirtualInfiniteList - 虚拟滚动列表
使用虚拟滚动技术,高性能渲染大量数据。基于 @tanstack/react-virtual 实现:
import { VirtualInfiniteList } from '@xcloud/ui-mobile'
<VirtualInfiniteList
queryKey={['products']}
queryFn={fetchProducts}
height={600}
estimateSize={72}
overscan={5}
renderItem={(product) => <ProductCard product={product} />}
/>主要特性:
- 🚀 只渲染可见区域,支持数千条数据流畅滚动
- 📏 自动测量每个项目的实际高度
- 🔄 结合无限滚动,自动加载更多数据
SelectableList - 可选择列表
支持单选/多选、全选、批量操作的列表组件:
import { SelectableList } from '@xcloud/ui-mobile'
<SelectableList
queryKey={['products']}
queryFn={fetchProducts}
selectionOptions={{
getItemId: (item) => item.id,
multiple: true,
maxSelection: 20,
}}
batchActions={[
{
key: 'delete',
label: '删除',
icon: '🗑️',
danger: true,
onAction: async (items) => {
await deleteProducts(items.map(i => i.id))
},
},
]}
renderItem={(product, index, { isSelected, onToggle }) => (
<div onClick={onToggle}>
<input type="checkbox" checked={isSelected} />
<span>{product.name}</span>
</div>
)}
onSelectionChange={(ids, items) => {
console.log('选中:', ids)
}}
/>主要特性:
- ☑️ 支持单选和多选模式,可设置最大选择数量
- ✅ 全选功能,显示选中数量和半选状态
- 🎯 自定义批量操作,如删除、导出等
GroupedList - 分组列表
支持按规则分组显示数据,支持吸顶标题和可折叠分组:
import { GroupedList } from '@xcloud/ui-mobile'
<GroupedList
queryKey={['contacts']}
queryFn={fetchContacts}
groupBy={(contact) => contact.name[0].toUpperCase()}
renderGroupHeader={(groupKey, items) => (
<div>{groupKey} ({items.length})</div>
)}
renderItem={(contact) => <ContactCard contact={contact} />}
stickyHeader
collapsible
defaultExpandedGroups={['A', 'B', 'C']}
/>主要特性:
- 📋 通过 groupBy 函数自动将数据分组显示
- 📌 滚动时分组标题自动吸附在顶部
- 🎯 支持折叠/展开分组,节省空间
使用场景: 联系人列表(按首字母)、订单列表(按日期)、商品列表(按分类)等
SortableList - 可排序列表
支持拖拽排序的列表组件,基于 @dnd-kit 实现:
import { SortableList } from '@xcloud/ui-mobile'
const [tasks, setTasks] = useState([...])
<SortableList
items={tasks}
getItemId={(task) => task.id}
onReorder={(newTasks) => {
setTasks(newTasks)
}}
showDragHandle
disabled={false}
renderItem={(task) => (
<div>
<input type="checkbox" checked={task.completed} />
<span>{task.title}</span>
</div>
)}
/>主要特性:
- 🎯 基于 @dnd-kit,支持鼠标和触摸拖拽
- ☰ 可选的拖拽手柄,避免误触
- 🔒 支持动态启用/禁用拖拽功能
辅助组件
PullToRefresh - 下拉刷新
移动端下拉刷新组件,支持触摸手势:
import { PullToRefresh, InfiniteList } from '@xcloud/ui-mobile'
import { useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
<PullToRefresh
onRefresh={async () => {
await queryClient.invalidateQueries({ queryKey: ['products'] })
}}
threshold={80}
maxPullDistance={150}
>
<InfiniteList
queryKey={['products']}
queryFn={fetchProducts}
renderItem={(item) => <ProductCard item={item} />}
/>
</PullToRefresh>主要特性:
- 📱 原生触摸事件支持,流畅的手势体验
- 🎨 平滑的阻尼动画,符合物理直觉
- ⚙️ 自定义触发阈值、文本和指示器
SwipeableListItem - 滑动操作列表项
iOS/Android 风格的左右滑动操作:
import { SwipeableListItem, InfiniteList } from '@xcloud/ui-mobile'
<InfiniteList
queryKey={['emails']}
queryFn={fetchEmails}
renderItem={(email) => (
<SwipeableListItem
leftActions={[
{
key: 'star',
label: '收藏',
icon: '⭐',
color: '#ff9800',
onAction: () => handleStar(email.id),
},
]}
rightActions={[
{
key: 'delete',
label: '删除',
icon: '🗑️',
color: '#f44336',
onAction: () => handleDelete(email.id),
},
]}
threshold={50}
>
<div>{email.subject}</div>
</SwipeableListItem>
)}
/>主要特性:
- ⬅️➡️ 支持左滑和右滑,可配置不同操作
- 🎨 自定义按钮颜色、图标和文字
- 📱 流畅的触摸交互,符合原生应用习惯
使用场景: 邮件列表、聊天消息、待办事项、购物车等
使用 Hooks
useInfiniteList
无限滚动列表的底层 Hook:
import { useInfiniteList } from '@xcloud/ui-mobile'
const { data, loadMore, hasMore, isLoading, refresh } = useInfiniteList({
queryKey: ['products'],
queryFn: fetchProducts,
enabled: true,
})useListSelection
列表选择状态管理 Hook:
import { useListSelection } from '@xcloud/ui-mobile'
const {
selectedIds,
selectedCount,
isSelected,
toggleSelect,
selectAll,
clearSelection,
isAllSelected,
isIndeterminate,
} = useListSelection({
getItemId: (item) => item.id,
multiple: true,
maxSelection: 100,
onSelectionChange: (ids) => {
console.log('选中的 IDs:', ids)
},
})useListFilters
列表筛选和搜索 Hook:
import { useListFilters } from '@xcloud/ui-mobile'
const {
search,
setSearch,
sort,
setSort,
filters,
setFilters,
reset,
} = useListFilters({
searchKeys: ['name', 'description'],
defaultSort: { field: 'createdAt', order: 'desc' },
})useListMutations
列表 CRUD 操作 Hook:
import { useListMutations } from '@xcloud/ui-mobile'
const {
create,
update,
remove,
isCreating,
isUpdating,
isDeleting,
} = useListMutations({
queryKey: ['products'],
createFn: async (data) => {
await fetch('/api/products', { method: 'POST', body: JSON.stringify(data) })
},
updateFn: async (id, data) => {
await fetch(`/api/products/${id}`, { method: 'PUT', body: JSON.stringify(data) })
},
deleteFn: async (id) => {
await fetch(`/api/products/${id}`, { method: 'DELETE' })
},
onSuccess: () => console.log('操作成功'),
})与 @xcloud/request 集成
RequestList 组件提供了与 @xcloud/request 包的无缝集成,通过适配器自动处理分页、搜索和过滤。
安装
pnpm add @xcloud/request @xcloud/ui-mobile设置 RequestProvider
首先,在应用根组件中设置 RequestProvider:
import { RequestProvider } from '@xcloud/request'
import { createClient } from '@xcloud/request'
// 创建全局请求客户端实例
const requestClient = createClient({
baseURL: '/api',
// 其他配置...
})
function App() {
return (
<RequestProvider client={requestClient}>
{/* 你的应用组件 */}
</RequestProvider>
)
}使用 Hook 版本适配器(推荐)
使用 useRequestAdapter 或 useSearchableRequestAdapter 自动从 RequestProvider 获取 client:
import { useRequestAdapter } from '@xcloud/ui-mobile'
import { RequestList } from '@xcloud/ui-mobile'
interface User {
id: string
name: string
email: string
}
function UserList() {
// 自动从 RequestProvider 获取 client
const queryFn = useRequestAdapter<User>({
url: '/users',
method: 'GET', // 可选,默认 GET
})
return (
<RequestList
queryKey={['users']}
queryFn={queryFn}
mode="pagination"
renderItem={(user) => (
<div className="p-4 border-b">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
/>
)
}使用可搜索适配器(Hook 版本)
import { useSearchableRequestAdapter } from '@xcloud/ui-mobile'
import { RequestList } from '@xcloud/ui-mobile'
import { useState } from 'react'
function UserList() {
const [search, setSearch] = useState('')
const [role, setRole] = useState<string>()
// 自动从 RequestProvider 获取 client
const queryFn = useSearchableRequestAdapter<User>({
url: '/users',
searchFields: ['name', 'email'], // 搜索字段
filterFields: ['role', 'status'], // 过滤字段
})
return (
<>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索用户..."
/>
<RequestList
queryKey={['users', { search, role }]}
queryFn={queryFn}
mode="pagination"
renderItem={(user) => <UserCard user={user} />}
/>
</>
)
}使用 useRequestClient Hook(工厂函数版本)
在组件中使用 useRequestClient 获取客户端实例:
import { useRequestClient } from '@xcloud/request'
import { RequestList, createRequestAdapter } from '@xcloud/ui-mobile'
interface User {
id: string
name: string
email: string
}
function UserList() {
// 从 Context 获取客户端实例
const client = useRequestClient()
// 创建适配器
const queryFn = createRequestAdapter<User>({
url: '/users',
client, // 使用 Context 中的客户端
method: 'GET',
})
return (
<RequestList
queryKey={['users']}
queryFn={queryFn}
mode="pagination"
renderItem={(user) => (
<div className="p-4 border-b">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
/>
)
}基础用法
不使用 useRequestClient 时,可以直接传入客户端实例:
import { RequestList, createRequestAdapter } from '@xcloud/ui-mobile'
import { createClient } from '@xcloud/request'
// 创建请求客户端实例
const client = createClient({
baseURL: '/api',
})
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
function UserList() {
// 创建适配器
const queryFn = createRequestAdapter<User>({
url: '/users',
client,
method: 'GET',
})
return (
<RequestList
queryKey={['users']}
queryFn={queryFn}
mode="pagination"
renderItem={(user) => (
<div className="p-4 border-b">
<h3>{user.name}</h3>
<p>{user.email}</p>
<span>{user.role}</span>
</div>
)}
/>
)
}搜索和过滤
使用 createSearchableRequestAdapter 支持搜索和多字段过滤:
import { useState } from 'react'
import { RequestList, createSearchableRequestAdapter } from '@xcloud/ui-mobile'
import { createClient } from '@xcloud/request'
const client = createClient()
function UserListWithSearch() {
const [search, setSearch] = useState('')
const [role, setRole] = useState<string | undefined>()
const queryFn = createSearchableRequestAdapter<User>({
url: '/users',
client,
searchFields: ['name', 'email'], // 搜索这些字段
filterFields: ['role', 'status'], // 过滤这些字段
})
return (
<div>
{/* 搜索框 */}
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索用户..."
/>
{/* 过滤器 */}
<select value={role} onChange={(e) => setRole(e.target.value)}>
<option value="">全部角色</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
</select>
{/* 列表 - queryKey 包含搜索和过滤参数 */}
<RequestList
queryKey={['users', { search, role }]}
queryFn={queryFn}
mode="pagination"
renderItem={(user) => (
<div>{user.name}</div>
)}
/>
</div>
)
}自定义参数转换
如果你的 API 使用不同的参数格式:
import { createRequestAdapter, extractQueryParams } from '@xcloud/ui-mobile'
const queryFn = createRequestAdapter<User>({
url: '/users',
client,
// 自定义参数格式
transformParams: (context) => {
const params = extractQueryParams(context.queryKey)
return {
pageNumber: context.pageParam, // API 使用 pageNumber
limit: 20, // API 使用 limit
keyword: params.search, // API 使用 keyword
...params,
}
},
// 如果响应格式不同,也可以自定义转换
transformResponse: (response) => ({
data: response.list,
hasMore: response.hasMore,
total: response.total,
currentPage: response.page,
pageSize: response.pageSize,
nextPage: response.hasMore ? response.page + 1 : undefined,
}),
})POST 请求
默认使用 GET,也可以使用 POST:
const queryFn = createRequestAdapter<User>({
url: '/users/search',
client,
method: 'POST', // 使用 POST 方法
})额外参数
添加固定的请求参数:
const queryFn = createRequestAdapter<Product>({
url: '/products',
client,
extraParams: {
category: 'electronics',
inStock: true,
},
})无限滚动
配合无限滚动使用:
const queryFn = createRequestAdapter<User>({
url: '/users',
client,
})
<RequestList
queryKey={['users']}
queryFn={queryFn}
mode="infinite" // 无限滚动模式
renderItem={(user) => <div>{user.name}</div>}
/>CRUD 操作
结合 useListMutations 实现增删改查:
import { RequestList, createRequestAdapter, useListMutations } from '@xcloud/ui-mobile'
function UserManagement() {
const queryFn = createRequestAdapter<User>({
url: '/users',
client,
})
const mutations = useListMutations<User>({
queryKey: ['users'],
createFn: async (data) => await client.post('/users', data),
updateFn: async (id, data) => await client.put(`/users/${id}`, data),
deleteFn: async (id) => await client.delete(`/users/${id}`),
})
return (
<RequestList
queryKey={['users']}
queryFn={queryFn}
mode="pagination"
renderItem={(user) => (
<div>
<span>{user.name}</span>
<button onClick={() => mutations.update(user.id, { name: 'New Name' })}>
编辑
</button>
<button onClick={() => mutations.delete(user.id)}>
删除
</button>
</div>
)}
headerActions={
<button onClick={() => mutations.create({ name: 'New User', email: 'new@example.com' })}>
添加用户
</button>
}
/>
)
}完整示例
import { useState } from 'react'
import { RequestList, createSearchableRequestAdapter } from '@xcloud/ui-mobile'
import { createClient } from '@xcloud/request'
const client = createClient({
baseURL: 'https://api.example.com',
})
interface Article {
id: string
title: string
content: string
author: string
category: string
createdAt: string
}
export function ArticleList() {
const [search, setSearch] = useState('')
const [category, setCategory] = useState('')
const queryFn = createSearchableRequestAdapter<Article>({
url: '/articles',
client,
searchFields: ['title', 'content', 'author'],
filterFields: ['category'],
extraParams: {
published: true,
},
})
return (
<div className="article-list">
{/* 搜索和过滤 */}
<div className="filters">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索文章..."
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="">全部分类</option>
<option value="tech">技术</option>
<option value="business">商业</option>
<option value="lifestyle">生活</option>
</select>
</div>
{/* 文章列表 */}
<RequestList
queryKey={['articles', { search, category }]}
queryFn={queryFn}
mode="infinite"
renderItem={(article) => (
<article className="article-card">
<h2>{article.title}</h2>
<p className="author">作者: {article.author}</p>
<p className="content">{article.content.substring(0, 200)}...</p>
<time>{new Date(article.createdAt).toLocaleDateString()}</time>
</article>
)}
emptyMessage="暂无文章"
errorMessage={(error) => `加载失败: ${error.message}`}
/>
</div>
)
}适配器 API
createRequestAdapter
创建标准的请求适配器。
function createRequestAdapter<TData>(
options: RequestAdapterOptions<TData>
): (context: QueryFunctionContext) => Promise<PagedResponse<TData>>选项:
| 属性 | 类型 | 描述 |
|---|---|---|
url | string | API 端点 URL |
client | RequestClient | 请求客户端实例(可选) |
method | 'GET' | 'POST' | HTTP 方法,默认 GET |
transformParams | (context) => Record<string, any> | 参数转换函数 |
transformResponse | (response) => PagedResponse<TData> | 响应转换函数 |
extraParams | Record<string, any> | 额外的请求参数 |
createSearchableRequestAdapter
创建支持搜索和过滤的适配器。
function createSearchableRequestAdapter<TData>(
options: RequestAdapterOptions<TData> & {
searchFields?: string[]
filterFields?: string[]
}
): (context: QueryFunctionContext) => Promise<PagedResponse<TData>>额外选项:
| 属性 | 类型 | 描述 |
|---|---|---|
searchFields | string[] | 搜索字段列表 |
filterFields | string[] | 过滤字段列表 |
响应数据格式
@xcloud/request 的标准分页响应格式:
interface RequestPaginationResponse<T> {
list: T[] // 数据列表
total: number // 总数
page: number // 当前页
pageSize: number // 每页数量
totalPages: number // 总页数
hasMore: boolean // 是否有下一页
}适配器会自动将其转换为 RequestList 需要的 PagedResponse 格式。
最佳实践
- 共享客户端实例: 在应用中创建一个全局的
RequestClient实例,避免重复创建 - 类型安全: 始终为数据定义 TypeScript 类型
- 查询键管理: 使用一致的查询键结构,便于缓存管理
- 错误处理: 利用
@xcloud/request的内置错误处理和重试机制 - 性能优化: 使用
staleTime和cacheTime优化缓存策略
API 文档
InfiniteList Props
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
queryKey | QueryKey | - | React Query 查询键 |
queryFn | (context) => Promise<PagedResponse> | - | 查询函数 |
renderItem | (item, index) => ReactNode | - | 渲染列表项 |
emptyText | string | '暂无数据' | 空状态文本 |
errorTitle | string | '加载失败' | 错误标题 |
showLoadMoreButton | boolean | false | 显示加载更多按钮 |
skeletonCount | number | 3 | 骨架屏数量 |
renderSkeleton | () => ReactNode | - | 自定义骨架屏 |
renderEmpty | () => ReactNode | - | 自定义空状态 |
renderError | (error, retry) => ReactNode | - | 自定义错误状态 |
VirtualInfiniteList Props
继承 InfiniteList 所有属性,额外支持:
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
height | number | string | '100vh' | 虚拟滚动容器高度 |
estimateSize | number | 50 | 预估每项高度(px) |
overscan | number | 5 | 预渲染项目数量 |
SelectableList Props
继承 InfiniteList 所有属性,额外支持:
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
selectionOptions | UseListSelectionOptions | - | 选择配置 |
batchActions | BatchAction[] | [] | 批量操作配置 |
showSelectAll | boolean | true | 是否显示全选按钮 |
selectAllText | string | '全选' | 全选按钮文本 |
stickyBatchBar | boolean | true | 批量操作栏是否吸顶 |
renderBatchBar | (params) => ReactNode | - | 自定义批量操作栏 |
onSelectionChange | (ids, items) => void | - | 选中状态改变回调 |
GroupedList Props
继承 InfiniteList 所有属性,额外支持:
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
groupBy | (item) => string | - | 分组函数 |
renderGroupHeader | (key, items) => ReactNode | - | 分组标题渲染函数 |
stickyHeader | boolean | true | 分组标题是否吸顶 |
collapsible | boolean | false | 分组是否可折叠 |
defaultExpandedGroups | string[] | - | 默认展开的分组 |
expandedGroups | string[] | - | 受控模式:展开的分组 |
onGroupToggle | (key, expanded) => void | - | 分组展开/折叠回调 |
sortGroups | (a, b) => number | - | 分组排序函数 |
SortableList Props
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
items | TData[] | - | 列表数据 |
getItemId | (item) => string | number | (item) => item.id | 获取项目 ID |
onReorder | (items) => void | - | 排序改变回调 |
renderItem | (item, index) => ReactNode | - | 列表项渲染函数 |
showDragHandle | boolean | true | 是否显示拖拽手柄 |
dragHandleIcon | ReactNode | '☰' | 拖拽手柄图标 |
disabled | boolean | false | 是否禁用拖拽 |
PullToRefresh Props
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | 子组件 |
onRefresh | () => Promise<void> | - | 刷新回调 |
threshold | number | 80 | 触发刷新的下拉距离(px) |
maxPullDistance | number | 150 | 最大下拉距离(px) |
indicatorHeight | number | 60 | 刷新指示器高度(px) |
disabled | boolean | false | 是否禁用下拉刷新 |
renderIndicator | (params) => ReactNode | - | 自定义刷新指示器 |
SwipeableListItem Props
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | 列表项内容 |
leftActions | SwipeAction[] | [] | 左侧操作配置 |
rightActions | SwipeAction[] | [] | 右侧操作配置 |
threshold | number | 50 | 触发操作的滑动距离(px) |
disabled | boolean | false | 是否禁用滑动 |
类型定义
PagedResponse
interface PagedResponse<TData> {
data: TData[]
total?: number
page?: number
pageSize?: number
hasMore?: boolean
nextPage?: number
}BatchAction
interface BatchAction<TData> {
key: string
label: string
icon?: ReactNode
danger?: boolean
disabled?: boolean
onAction: (items: TData[]) => Promise<void> | void
}SwipeAction
interface SwipeAction {
key: string
label: string
icon?: ReactNode
color?: string
disabled?: boolean
onAction: () => void | Promise<void>
}组合使用示例
虚拟滚动 + 下拉刷新
import { PullToRefresh, VirtualInfiniteList } from '@xcloud/ui-mobile'
<PullToRefresh
onRefresh={async () => {
await queryClient.invalidateQueries({ queryKey: ['products'] })
}}
>
<VirtualInfiniteList
queryKey={['products']}
queryFn={fetchProducts}
renderItem={(product) => <ProductCard product={product} />}
height={600}
estimateSize={100}
/>
</PullToRefresh>分组 + 可选择
import { GroupedList, useListSelection } from '@xcloud/ui-mobile'
const selection = useListSelection({ multiple: true })
<GroupedList
queryKey={['contacts']}
queryFn={fetchContacts}
groupBy={(contact) => contact.name[0].toUpperCase()}
renderGroupHeader={(key) => <div>{key}</div>}
renderItem={(contact) => (
<div onClick={() => selection.toggleSelect(contact)}>
<input type="checkbox" checked={selection.isSelected(contact)} />
<span>{contact.name}</span>
</div>
)}
stickyHeader
/>主要特性
🚀 高性能
- 虚拟滚动支持数千条数据流畅渲染
- 基于 Intersection Observer 的自动触底加载
- 优化的 DOM 操作和渲染性能
💾 智能缓存
- 基于 TanStack React Query
- 自动缓存和失效管理
- 支持乐观更新
🎯 丰富功能
- 无限滚动、分页、虚拟滚动
- 单选/多选、批量操作
- 分组显示、拖拽排序
- 下拉刷新、滑动操作
🎨 灵活定制
- 自定义加载、空状态、错误状态
- 支持骨架屏
- 完整的 TypeScript 类型支持
- 可自定义样式
注意事项
- 必须提供 QueryClientProvider: 确保在应用根组件中设置了 QueryClientProvider
- 查询键唯一性: 每个列表的 queryKey 应该是唯一的
- 数据格式规范: queryFn 必须返回符合 PagedResponse 接口的数据
- 移动端支持: PullToRefresh 和 SwipeableListItem 需要在移动端或模拟器中测试