Appearance
AutoCrud
一体化 CRUD 组件:Search + Table + Form + Actions + Selection + Route Sync。
Props
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
adapter | CrudAdapter<Row, Query> | — | 数据适配器(必填) |
fields | CrudField<Row>[] | — | 字段定义(必填) |
columns | CrudColumn<Row>[] | 由 fields 生成 | 表格列定义 |
searchFields | CrudField<Row>[] | 使用 fields | 搜索字段(可独立于 fields 定义搜索表单) |
searchQueryKey | string | — | 搜索条件写入 query[key](不设置则扁平写入 query 根) |
actions | CrudAction<Row>[] | 自动生成 | 自定义 actions(与默认 actions 合并,id 相同则覆盖) |
formMode | 'modal' | 'drawer' | 'modal' | 表单显示模式 |
showSelection | boolean | false | 是否开启行勾选 |
showActionsColumn | boolean | true | 是否显示操作列 |
disableCreate | boolean | false | 禁用新增按钮 |
disableEdit | boolean | false | 禁用编辑按钮 |
disableDelete | boolean | false | 禁用删除按钮 |
disableExport | boolean | false | 禁用导出按钮 |
title | string | '列表' | 卡片标题 |
paginationProps | PaginationProps | — | 透传到 NPagination |
routeSync | boolean | false | 开启 URL query 同步 |
routeQueryKey | string | 'q' | URL query 参数名 |
默认 Actions 行为
当未传入 actions prop 时,AutoCrud 会根据 adapter 能力自动生成:
| 条件 | 生成的 Action |
|---|---|
adapter.create 存在且 disableCreate 为 false | presetActions.create (toolbar) |
adapter.update 存在且 disableEdit 为 false | presetActions.edit (row) |
adapter.remove 存在且 disableDelete 为 false | presetActions.delete (row) |
adapter.export 存在且 disableExport 为 false | presetActions.export (toolbar) |
Emits
| 事件 | 参数 | 说明 |
|---|---|---|
submit | { mode: 'create' | 'edit', data: Partial<Row> } | 表单提交前触发 |
success | { mode: 'create' | 'edit', data: Row } | 表单提交成功 |
error | error: unknown | 任何错误(list 请求、表单提交等) |
Expose
通过 ref 访问内部实例:
ts
const crudRef = ref<InstanceType<typeof AutoCrud>>()
crudRef.value?.list // UseCrudListReturn
crudRef.value?.selection // UseCrudSelectionReturn
crudRef.value?.form // UseCrudFormReturn
crudRef.value?.refresh() // 刷新列表
crudRef.value?.openCreate() // 打开新增表单
crudRef.value?.openEdit(row) // 打开编辑表单| 属性/方法 | 类型 | 说明 |
|---|---|---|
list | UseCrudListReturn | 列表状态 |
selection | UseCrudSelectionReturn | 选择状态 |
form | UseCrudFormReturn | 表单状态 |
refresh | () => Promise<void> | 刷新列表 |
openCreate | () => void | 打开新增表单 |
openEdit | (row: Row) => void | 打开编辑表单 |
Slots
字段级 Slots
| Slot | Props | 说明 |
|---|---|---|
cell-${key} | { row, value, rowIndex } | 自定义表格单元格 |
field-${key} | { field, model, mode, value } | 自定义表单字段 |
search-${key} | { field, model } | 自定义搜索字段 |
布局 Slots
| Slot | Props | 说明 |
|---|---|---|
toolbar | { list, selection, openCreate } | 工具栏区域 |
before-table | { list, selection } | 表格前区域 |
batch-actions | { list, selection, actions, renderActionButton } | 批量操作区 |
row-actions | { row, rowIndex, openEdit, actions, defaultButtons } | 行操作区 |
search | { list, fields } | 替换整个搜索组件 |
table | { list, columns, selection, rowKey, ... } | 替换整个表格组件 |
form | { form, fields, visible, setVisible, mode, editingRow, submit } | 替换整个表单组件 |
row-actions 的 defaultButtons
row-actions slot 提供 defaultButtons 对象,包含预设按钮的函数式组件:
vue
<template #row-actions="{ row, defaultButtons }">
<NSpace>
<component
:is="defaultButtons.Edit"
:row="row"
/>
<component
:is="defaultButtons.Delete"
:row="row"
/>
<NButton
size="small"
@click="doSomething(row)"
>
自定义
</NButton>
</NSpace>
</template>| 按钮 | 说明 |
|---|---|
defaultButtons.Edit | 编辑按钮(对应 presetActions.edit) |
defaultButtons.Delete | 删除按钮(对应 presetActions.delete) |
defaultButtons.View | 查看按钮(对应 presetActions.view,如果存在) |
示例
交互示例
loading
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
}
}