Skip to content

快速开始

本页用一个最小 Demo 展示完整的 CRUD 流程。你将接触到三个核心概念:

  1. CrudAdapter — 负责 list / create / update / remove
  2. Fields / Columns — 描述搜索 / 表单 / 表格的 schema
  3. AutoCrud — 把以上能力组合为可用的 UI

1) 定义行类型

先约定数据结构与查询结构:

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
  }
}

2) 实现 CrudAdapter

CrudAdapter 是数据层的唯一接口。你只需要实现 list(必选),以及 create / update / remove(按需):

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
  }
}

TIP

在实际项目中,adapter 通常封装你的 HTTP 客户端(如 axios / ofetch)。这里用内存数组演示,方便你直接运行。

3) 定义 fields 与 columns

defineFields 描述搜索 / 表单字段,defineColumns 描述表格列。两者都是纯 schema,不包含 UI 渲染逻辑:

ts
import type { SelectOption } from 'naive-ui'
import type { DemoRow } from './basic-types'
import { cellDateTime, cellEnumTag, cellMoney, defineColumns, defineFields } from '@uozi/vito-naive-ui'

const statusOptions: SelectOption[] = [
  { label: '草稿', value: 'draft' },
  { label: '启用', value: 'enabled' },
  { label: '禁用', value: 'disabled' },
]

export const demoFields = defineFields<DemoRow>([
  {
    key: 'name',
    label: '名称',
    type: 'text',
    required: true,
    visibleIn: { searchForm: true, table: true, editForm: true },
    ui: {
      formControl: { placeholder: '输入名称' },
      overrides: {
        editForm: { formControl: { clearable: true } },
        searchForm: { formControl: { clearable: true } },
      },
    },
  },
  {
    key: 'status',
    label: '状态',
    type: 'select',
    required: true,
    visibleIn: { searchForm: true, table: true, editForm: true },
    ui: {
      options: statusOptions,
      formControl: { options: statusOptions, clearable: true },
    },
  },
  {
    key: 'amount',
    label: '金额',
    type: 'money',
    required: true,
    visibleIn: { searchForm: false, table: true, editForm: true },
    ui: {
      formControl: { min: 0, step: 1, placeholder: '输入金额' },
    },
  },
  {
    key: 'createdAt',
    label: '创建时间',
    type: 'datetime',
    visibleIn: { searchForm: false, table: true, editForm: false },
  },
])

export const demoColumns = defineColumns<DemoRow>([
  { key: 'name', label: '名称', sortable: true, width: 220 },
  {
    key: 'status',
    label: '状态',
    width: 120,
    render: cellEnumTag({
      options: statusOptions,
      typeMap: { draft: 'warning', enabled: 'success', disabled: 'error' },
    }),
  },
  {
    key: 'amount',
    label: '金额',
    width: 140,
    render: cellMoney({ currency: 'CNY' }),
  },
  {
    key: 'createdAt',
    label: '创建时间',
    width: 200,
    render: cellDateTime(),
  },
])
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
  }
}

关键属性

属性作用
key字段 key,对应 Row 的属性名
label显示名称
type字段类型(text / select / number / money / date / switch 等)
required表单必填(自动生成验证规则)
visibleIn控制字段在 searchForm / table / editForm 中是否可见
uiNaive UI 扩展配置(控件 props、formItem props、options 等)

4) 使用 AutoCrud 组件

adapterfieldscolumns 传给 AutoCrud 即可:

vue
<script setup lang="ts">
import { AutoCrud } from '@uozi/vito-naive-ui'
import { NAlert, NText } from 'naive-ui'
import { ref } from 'vue'
import { createBasicAdapter } from './basic-adapter'
import { demoColumns, demoFields } from './basic-schema'

const { adapter } = createBasicAdapter()
const crudRef = ref<InstanceType<typeof AutoCrud> | null>(null)
</script>

<template>
  <div style="display: flex; flex-direction: column; gap: 12px">
    <NAlert
      :bordered="false"
      type="info"
    >
      <div>
        这是一个最小的 <NText code>
          AutoCrud
        </NText> 示例:搜索(query.search)、分页、排序、表单(Modal)、新增/编辑/删除。
      </div>
    </NAlert>

    <AutoCrud
      ref="crudRef"
      :adapter="adapter"
      :fields="demoFields"
      :columns="demoColumns"
      search-query-key="search"
      form-mode="modal"
      show-selection
      :show-actions-column="true"
    />
  </div>
</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
import type { SelectOption } from 'naive-ui'
import type { DemoRow } from './basic-types'
import { cellDateTime, cellEnumTag, cellMoney, defineColumns, defineFields } from '@uozi/vito-naive-ui'

const statusOptions: SelectOption[] = [
  { label: '草稿', value: 'draft' },
  { label: '启用', value: 'enabled' },
  { label: '禁用', value: 'disabled' },
]

export const demoFields = defineFields<DemoRow>([
  {
    key: 'name',
    label: '名称',
    type: 'text',
    required: true,
    visibleIn: { searchForm: true, table: true, editForm: true },
    ui: {
      formControl: { placeholder: '输入名称' },
      overrides: {
        editForm: { formControl: { clearable: true } },
        searchForm: { formControl: { clearable: true } },
      },
    },
  },
  {
    key: 'status',
    label: '状态',
    type: 'select',
    required: true,
    visibleIn: { searchForm: true, table: true, editForm: true },
    ui: {
      options: statusOptions,
      formControl: { options: statusOptions, clearable: true },
    },
  },
  {
    key: 'amount',
    label: '金额',
    type: 'money',
    required: true,
    visibleIn: { searchForm: false, table: true, editForm: true },
    ui: {
      formControl: { min: 0, step: 1, placeholder: '输入金额' },
    },
  },
  {
    key: 'createdAt',
    label: '创建时间',
    type: 'datetime',
    visibleIn: { searchForm: false, table: true, editForm: false },
  },
])

export const demoColumns = defineColumns<DemoRow>([
  { key: 'name', label: '名称', sortable: true, width: 220 },
  {
    key: 'status',
    label: '状态',
    width: 120,
    render: cellEnumTag({
      options: statusOptions,
      typeMap: { draft: 'warning', enabled: 'success', disabled: 'error' },
    }),
  },
  {
    key: 'amount',
    label: '金额',
    width: 140,
    render: cellMoney({ currency: 'CNY' }),
  },
  {
    key: 'createdAt',
    label: '创建时间',
    width: 200,
    render: cellDateTime(),
  },
])
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
  }
}

交互预览

loading

这个组件会自动处理:

  • 搜索表单渲染 + 查询联动
  • 表格分页 + 排序
  • 新增 / 编辑表单(Modal)
  • 根据 adapter 能力自动生成操作按钮

下一步

Made with VitePress