Skip to content

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

参数类型默认值说明
adapterCrudAdapter<Row, Query>数据适配器(必填)
initialQueryQuery{}初始查询对象
initialPagenumber1初始页码
initialPageSizenumber20初始每页条数
autoFetchbooleantrue初始化拉取一次;且 query/sort/page/pageSize 变化时自动拉取
debounceMsnumber0自动拉取的防抖延迟(ms)
dedupebooleantrue相同参数的请求是否去重
onError(error: unknown) => void请求失败回调

返回值

State

属性类型说明
rowsRef<Row[]>当前页数据
totalRef<number>总条数
loadingRef<boolean>是否加载中
errorRef<unknown>最近一次错误
pageRef<number>当前页码
pageSizeRef<number>每页条数
queryRef<Query>查询对象
sortRef<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)。listsignal 参数来自这个 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
  }
}

Made with VitePress