Appearance
useCrudList
列表数据管理:分页、排序、查询状态、自动拉取、去重、Abort。
基本用法
ts
import { useCrudList } from '@uozi/vito-core'
const list = useCrudList<MyRow, MyQuery>({
adapter,
initialQuery: { search: {} },
initialPageSize: 20,
})
// 获取数据
list.rows.value // MyRow[]
list.total.value // number
list.loading.value // boolean
// 操作
list.setQuery({ search: { name: 'foo' } })
list.setPage(2)
list.setSort({ field: 'createdAt', order: 'descend' })
list.refresh()
list.reset()交互示例
loading
Options
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
adapter | CrudAdapter<Row, Query> | — | 数据适配器(必填) |
initialQuery | Query | {} | 初始查询对象 |
initialPage | number | 1 | 初始页码 |
initialPageSize | number | 20 | 初始每页条数 |
autoFetch | boolean | true | 初始化拉取一次;且 query/sort/page/pageSize 变化时自动拉取 |
debounceMs | number | 0 | 自动拉取的防抖延迟(ms) |
dedupe | boolean | true | 相同参数的请求是否去重 |
onError | (error: unknown) => void | — | 请求失败回调 |
返回值
State
| 属性 | 类型 | 说明 |
|---|---|---|
rows | Ref<Row[]> | 当前页数据 |
total | Ref<number> | 总条数 |
loading | Ref<boolean> | 是否加载中 |
error | Ref<unknown> | 最近一次错误 |
page | Ref<number> | 当前页码 |
pageSize | Ref<number> | 每页条数 |
query | Ref<Query> | 查询对象 |
sort | Ref<CrudSort | null> | 排序配置 |
Actions
| 方法 | 签名 | 说明 |
|---|---|---|
refresh | () => Promise<void> | 强制重新拉取(忽略 dedupe) |
setQuery | (partial: Partial<Query>, options?: SetQueryOptions) => void | 更新查询条件并重置到第 1 页 |
setPage | (page: number) => void | 设置页码 |
setPageSize | (size: number) => void | 设置每页条数并重置到第 1 页 |
setSort | (sort: CrudSort | null) => void | 设置排序并重置到第 1 页 |
reset | () => void | 回到 initialQuery/initialPage/initialPageSize 并强制刷新 |
行为说明
autoFetch
默认开启。初始化会调度一次 list 请求;当 query / sort / page / pageSize 任一变化时,也会自动调度。配合 debounceMs 可以在高频变化时降频。
dedupe(去重)
默认开启。会根据 { page, pageSize, query, sort } 计算请求 key:
- 如果新 key 与上次完成的请求 key 相同,跳过
- 如果新 key 与正在进行的请求 key 相同,跳过
refresh()始终忽略去重
Abort
发起新请求前会 abort() 上一个未完成的请求(如果环境支持 AbortController)。list 的 signal 参数来自这个 controller。
最后一次请求胜出
内部用递增序列号 (requestSeq) 标记每次请求。当响应返回时,如果序列号不是最新的,直接丢弃结果。这确保快速切换页面 / 排序时,只有最后一次请求的结果生效。
setQuery 行为
setQuery 默认做 浅合并(mode: 'merge')并将 page 重置为 1。\n+\n+你也可以传入 options:\n+- mode: 'replace' 完全替换 query\n+- clearKeys 先删除指定 key(常用于扁平搜索条件清空)\n+- pruneEmpty 深度裁剪空值(undefined/null/''/[]/{}),保留 0/false
完整示例
vue
<script setup lang="ts">
import type { CrudAdapter } from '@uozi/vito-core'
import type { DemoQuery, DemoRow } from './basic-types'
import { useCrudForm, useCrudList, useCrudSelection } from '@uozi/vito-core'
import { CrudForm, CrudSearch, CrudTable, defineColumns, defineFields } from '@uozi/vito-naive-ui'
import { NButton, NCard, NSpace } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { createBasicAdapter } from './basic-adapter'
const { adapter } = createBasicAdapter()
const fields = defineFields<DemoRow>([
{ key: 'name', label: '名称', type: 'text', required: true, visibleIn: { searchForm: true, table: true, editForm: true } },
{ key: 'status', label: '状态', type: 'select', visibleIn: { searchForm: true, table: true, editForm: true } },
{ key: 'amount', label: '金额', type: 'number', visibleIn: { searchForm: false, table: true, editForm: true } },
])
const columns = defineColumns<DemoRow>([
{ key: 'name', label: '名称', sortable: true, width: 220 },
{ key: 'status', label: '状态', width: 120 },
{ key: 'amount', label: '金额', sortable: true, width: 120 },
])
const list = useCrudList<DemoRow, DemoQuery>({
adapter: adapter as CrudAdapter<DemoRow, DemoQuery>,
initialQuery: { search: {} },
})
const selection = useCrudSelection<DemoRow>({
rows: list.rows,
getId: adapter.getId,
})
const form = useCrudForm<DemoRow>({
fields: fields as any,
})
const visible = ref(false)
const editing = ref<DemoRow | null>(null)
function openCreate() {
editing.value = null
form.setMode('create')
form.reset()
visible.value = true
}
function openEdit(row: DemoRow) {
editing.value = row
form.setMode('edit')
form.reset(row)
visible.value = true
}
async function handleSubmit(data: Partial<DemoRow>) {
if (form.mode.value === 'create')
await adapter.create?.(data as any)
else if (editing.value)
await adapter.update?.(editing.value.id, data as any)
visible.value = false
await list.refresh()
}
onMounted(() => {
void list.refresh()
})
</script>
<template>
<NCard title="手动组合:CrudSearch + CrudTable + CrudForm">
<template #header-extra>
<NSpace>
<NButton
type="primary"
@click="openCreate"
>
新增
</NButton>
</NSpace>
</template>
<CrudSearch
:list="list"
:fields="(fields as any)"
query-key="search"
/>
<CrudTable
:list="list"
:columns="columns"
:selection="selection"
:row-key="(row: DemoRow) => row.id"
show-actions-column
>
<template #row-actions="{ row }">
<NSpace>
<NButton
size="small"
@click="openEdit(row)"
>
编辑
</NButton>
</NSpace>
</template>
</CrudTable>
<CrudForm
v-model:visible="visible"
:form="form"
:fields="(fields as any)"
display-mode="modal"
@submit="handleSubmit"
/>
</NCard>
</template>ts
import type { CrudAdapter, CrudSort, ListResult } from '@uozi/vito-core'
import type { DemoQuery, DemoRow } from './basic-types'
function compare(a: unknown, b: unknown): number {
if (a === b)
return 0
if (a === null || a === undefined)
return -1
if (b === null || b === undefined)
return 1
if (typeof a === 'number' && typeof b === 'number')
return a - b
return String(a).localeCompare(String(b))
}
function sortRows(rows: DemoRow[], sort?: CrudSort | null): DemoRow[] {
if (!sort)
return rows
const dir = sort.order === 'descend' ? -1 : 1
return rows.slice().sort((ra, rb) => dir * compare((ra as any)[sort.field], (rb as any)[sort.field]))
}
function filterRows(rows: DemoRow[], query: DemoQuery): DemoRow[] {
const s = query.search ?? {}
const name = s.name?.trim() ?? null
const status = s.status ?? null
return rows.filter((r) => {
if (name && !r.name.toLowerCase().includes(name.toLowerCase()))
return false
if (status && r.status !== status)
return false
return true
})
}
function createSeed(): DemoRow[] {
const now = Date.now()
return [
{ id: 1, name: '示例 1', status: 'enabled', amount: 12.3, createdAt: now - 3600_000 },
{ id: 2, name: '示例 2', status: 'draft', amount: 45.6, createdAt: now - 7200_000 },
{ id: 3, name: '示例 3', status: 'disabled', amount: 78.9, createdAt: now - 10800_000 },
]
}
export function createBasicAdapter(initial?: DemoRow[]): {
adapter: CrudAdapter<DemoRow, DemoQuery>
reset: (next?: DemoRow[]) => void
getAll: () => DemoRow[]
} {
let db = (initial ?? createSeed()).slice()
let idSeq = db.reduce((m, r) => Math.max(m, r.id), 0) + 1
function getAll() {
return db.slice()
}
function reset(next?: DemoRow[]) {
db = (next ?? createSeed()).slice()
idSeq = db.reduce((m, r) => Math.max(m, r.id), 0) + 1
}
const adapter: CrudAdapter<DemoRow, DemoQuery> = {
getId(row) {
return row.id
},
async list(params): Promise<ListResult<DemoRow>> {
const filtered = filterRows(db, params.query)
const sorted = sortRows(filtered, params.sort)
const page = Math.max(1, params.page)
const pageSize = Math.max(1, params.pageSize)
const start = (page - 1) * pageSize
return {
items: sorted.slice(start, start + pageSize),
total: sorted.length,
}
},
async create(data): Promise<DemoRow> {
const row: DemoRow = {
id: idSeq++,
name: String((data as any)?.name ?? ''),
status: ((data as any)?.status ?? 'draft') as DemoRow['status'],
amount: Number((data as any)?.amount ?? 0),
createdAt: Date.now(),
}
db = [row, ...db]
return row
},
async update(id, data): Promise<DemoRow> {
const idx = db.findIndex(r => r.id === id)
if (idx < 0)
throw new Error(`记录不存在:${id}`)
const next: DemoRow = { ...db[idx], ...(data as any), id: db[idx].id }
db = db.slice()
db[idx] = next
return next
},
async remove(id): Promise<void> {
db = db.filter(r => r.id !== id)
},
}
return { adapter, reset, getAll }
}ts
export interface DemoRow {
id: number
name: string
status: 'draft' | 'enabled' | 'disabled'
amount: number
createdAt: number
}
export interface DemoQuery {
search?: {
name?: string | null
status?: DemoRow['status'] | null
}
}