外观
Avue CRUD 组件
2026-04-01
版本: @smallwei/avue 3.8.x 官网: avuejs.com
avue-crud 是 Nebula 前端最核心的低代码组件,通过一份配置对象(option) 同时驱动:
- 列表表格(含分页、排序、工具栏)
- 搜索栏(多条件筛选)
- 新增/编辑表单(弹窗或抽屉,含校验)
- 查看详情
绝大多数管理页面(角色、字典、菜单、机构、员工等)都基于它实现,可以大幅减少重复代码。
注册
main.ts 中全量注册,并配置全局默认值(Nebula 子应用已默认配置):
import Avue from '@smallwei/avue'
import '@smallwei/avue/lib/index.css'
app.use(Avue, {
crudOption: {
size: 'small',
menuFixed: 'right', // 操作列固定右侧
rowKey: 'id',
menuAlign: 'center',
align: 'left',
menuWidth: 220,
addBtn: false, // 默认关闭内置的新增按钮(由 menu-left 插槽自定义)
editBtn: false,
delBtn: false,
viewBtn: false,
searchShow: true, // 默认显示搜索栏
searchMenuSpan: 6,
searchSpan: 6,
labelWidth: 90,
dialogType: 'drawer', // 表单默认用抽屉方式
dialogDirection: 'rtl',
dialogWidth: 800,
dialogClickModal: false, // 点击遮罩不关闭
searchIcon: true,
searchIndex: 3,
showOverflowTooltip: true,
calcHeight: 130,
height: 'auto',
},
})核心用法
模板结构
<template>
<avue-crud
ref="crudRef"
:data="tableData"
:option="option"
:table-loading="loading"
:page="page"
:search="searchForm"
:before-open="beforeOpen"
@search-change="handleSearch"
@search-reset="handleReset"
@refresh-change="loadData"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
@row-save="handleRowSave"
@row-update="handleRowUpdate"
>
<!-- 工具栏左侧:自定义新增按钮 -->
<template #menu-left>
<el-button type="primary" size="small" @click="handleAdd">新增</el-button>
</template>
<!-- 自定义列渲染(如 status 用 el-switch 代替默认渲染) -->
<template #status="{ row }">
<el-switch :model-value="row.status === 0" @change="onStatusChange($event, row)" />
</template>
<!-- 自定义操作列(代替 editBtn / delBtn) -->
<template #menu="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</avue-crud>
</template>option 配置对象
Nebula 项目中,每个页面的 option 抽取为独立的 xxxCrudOption.ts 文件:
// src/pages/role/roleCrudOption.ts
export function getRoleCrudOption() {
return {
border: true,
page: true, // 开启分页
header: true, // 显示表头
menuWidth: 220, // 操作列宽度
addTitle: '添加角色', // 新增弹窗/抽屉标题
editTitle: '编辑角色',
saveBtnText: '完成',
updateBtnText: '完成',
cancelBtnText: '取消',
column: [
{
label: '角色名称',
prop: 'roleName',
minWidth: 160,
search: true, // 出现在搜索栏
rules: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
maxlength: 100,
showWordLimit: true,
},
{
label: '角色编码',
prop: 'roleCode',
minWidth: 160,
search: true,
editDisabled: true, // 编辑时禁用该字段
rules: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
},
{
label: '状态',
prop: 'status',
width: 90,
slot: true, // 使用插槽自定义渲染
type: 'select',
addDisplay: false, // 新增表单中不显示
editDisplay: false, // 编辑表单中不显示
viewDisplay: false,
dicData: [
{ label: '启用', value: 0 },
{ label: '禁用', value: 1 },
],
},
{
label: '备注',
prop: 'remark',
minWidth: 240,
type: 'textarea',
maxlength: 800,
showWordLimit: true,
},
],
}
}column 字段配置详解
常用通用属性
| 属性 | 类型 | 说明 |
|---|---|---|
label | string | 列标题 |
prop | string | 字段名 |
width / minWidth | number | 列宽 / 最小列宽 |
align | string | 对齐:left center right |
search | boolean | 是否出现在搜索栏 |
searchSpan | number | 搜索栏栅格(1~24) |
rules | array | 表单校验规则(同 Element Plus) |
slot | boolean | 是否使用插槽自定义列渲染 |
hide | boolean | 在列表中隐藏(表单中仍显示) |
addDisplay | boolean | 新增表单中是否显示 |
editDisplay | boolean | 编辑表单中是否显示 |
viewDisplay | boolean | 查看详情中是否显示 |
editDisabled | boolean | 编辑时禁用该输入框 |
value | any | 表单字段默认值 |
formatter | function | 列值格式化函数 (row, val, result) => string |
overHidden | boolean | 超长文字省略显示(悬浮 tooltip) |
maxlength | number | 输入框最大字符数 |
showWordLimit | boolean | 显示字数统计 |
placeholder | string | 输入框占位文本 |
字段 type 类型
// 常用 type 值
type: 'input' // 默认,单行文本
type: 'textarea' // 多行文本
type: 'number' // 数字输入
type: 'select' // 下拉选择
type: 'radio' // 单选按钮
type: 'checkbox' // 多选框
type: 'switch' // 开关
type: 'date' // 日期选择
type: 'datetime' // 日期时间选择
type: 'tree' // 树形下拉(用于选择上级节点)
type: 'upload' // 文件上传select / radio 字段的 dicData
// 静态字典(直接写在 column 中)
{
label: '菜单类型',
prop: 'menuType',
type: 'select',
dicData: [
{ label: '目录', value: 0 },
{ label: '菜单', value: 1 },
{ label: '按钮', value: 2 },
],
}
// 动态字典(从接口加载,通过 computed 传入 option)
const option = computed(() => ({
column: [{
label: '机构类型',
prop: 'orgType',
type: 'select',
dicData: orgTypeOptions.value, // 响应式数据
props: { label: 'label', value: 'value' },
}]
}))tree 类型(树形选择)
// 用于选择上级机构 / 上级菜单
{
label: '上级机构',
prop: 'parentId',
type: 'tree',
hide: true, // 列表中不显示,仅在表单中出现
checkStrictly: true, // 父子节点独立勾选(不联动)
clearable: true,
dicData: orgTreeDic, // 树形数组(有 children 字段)
props: {
label: 'orgName', // 显示的文本字段
value: 'id', // 绑定的 value 字段
children: 'children',
},
}树形表格模式
机构管理、菜单管理等层级数据使用树形表格:
// option 中开启树形
const option = {
tree: true, // 开启树形表格
rowParentKey: 'parentId', // 父节点 ID 字段名
lazy: false, // false = 一次性加载全部,true = 按需懒加载
defaultExpandAll: false,
treeProps: {
children: 'children',
hasChildren: 'hasChildren',
},
page: false, // 树形通常不分页
}注意: 搜索后需展开所有节点时,通过 :key 强制重新渲染 avue-crud:
const treeRenderKey = ref(0)
const option = computed(() =>
getOrgCrudOption(orgTreeOptions.value, searchForm.orgName.trim().length > 0)
)
function handleSearch(params: any, done: () => void) {
searchForm.orgName = params.orgName ?? ''
treeRenderKey.value += 1 // 触发重新渲染,使 defaultExpandAll 生效
loadTree(searchForm.orgName)
done()
}<!-- key 变化时强制重渲染 -->
<avue-crud :key="treeRenderKey" :option="option" ... />分页配置
// 分页对象结构(传给 :page 属性)
const page = ref({
total: 0,
currentPage: 1,
pageSize: 20,
})
// 事件处理
function handleCurrentChange(currentPage: number) {
page.value.currentPage = currentPage
loadData()
}
function handleSizeChange(pageSize: number) {
page.value.pageSize = pageSize
page.value.currentPage = 1
loadData()
}事件回调详解
@row-save — 新增保存
// done():关闭弹窗并停止 loading
// hideLoading():仅停止 loading,不关闭弹窗(出错时用)
async function handleRowSave(
row: any,
done: (rowArg?: any) => void,
hideLoading?: () => void
) {
try {
await createRoleApi({
roleName: row.roleName,
roleCode: row.roleCode,
remark: row.remark,
})
ElMessage.success('保存成功')
done() // 关闭弹窗
loadData() // 刷新列表
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
hideLoading?.() // 仅停止 loading,让用户继续修改
}
}@row-update — 编辑保存
async function handleRowUpdate(
row: any,
_index: number,
done: (rowArg?: any) => void,
hideLoading?: () => void
) {
try {
await updateRoleApi({ id: row.id, ...row })
ElMessage.success('保存成功')
done()
loadData()
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
hideLoading?.()
}
}@search-change / @search-reset — 搜索
function handleSearch(params: any, done: () => void) {
// params 是搜索栏各字段的值对象
searchForm.value = {
roleName: params?.roleName ?? '',
roleCode: params?.roleCode ?? '',
}
page.value.currentPage = 1
done() // 必须调用,通知 avue-crud 搜索完成
loadData()
}
function handleReset(done: () => void) {
searchForm.value = {}
page.value.currentPage = 1
done()
loadData()
}before-open — 打开弹窗前预处理
编辑场景下,弹窗打开前需要从接口加载最新详情数据(避免显示旧缓存):
const beforeOpen = (done: (fn?: () => void) => void, type: string) => {
if (type !== 'edit') {
done()
return
}
const crud = crudRef.value as any
const id = crud?.tableForm?.id
if (!id) {
done()
return
}
// 加载详情后再打开弹窗
roleAPI.getRoleById(id)
.then((detail: any) => {
// 将详情数据合并到 tableForm(表单数据源)
Object.assign(crud.tableForm, {
roleName: detail.roleName ?? '',
roleCode: detail.roleCode ?? '',
remark: detail.remark ?? '',
})
})
.catch((e: any) => {
ElMessage.error(e?.message || '加载失败')
})
.finally(() => done()) // 无论成功失败都要调用 done()
}通过 ref 手动控制
const crudRef = ref<any>(null)
// 手动打开新增表单
function handleAdd() {
crudRef.value?.rowAdd?.()
}
// 手动打开编辑表单(传入行数据)
function handleEdit(row: any) {
crudRef.value?.rowEdit?.(row)
}
// 获取当前表单数据
const formData = crudRef.value?.tableForm插槽总览
| 插槽名 | 说明 |
|---|---|
#menu-left | 工具栏左侧(常放新增按钮) |
#menu-right | 工具栏右侧 |
#menu="{ row }" | 自定义操作列(代替内置编辑/删除按钮) |
#[prop]="{ row }" | 自定义某列的表格渲染 |
#[prop]-form="scope" | 自定义某列在表单中的渲染 |
#[prop]-search | 自定义某列在搜索栏的渲染 |
示例:同一列在表格和表单中使用不同插槽
<!-- 表格中显示 Switch -->
<template #status="{ row }">
<el-switch :model-value="row.status === 0" @change="onStatusChange($event, row)" />
</template>
<!-- 编码规则页面:在表单中自定义明细列表 -->
<template #detailList-form="scope">
<div class="rule-detail">
<el-table :data="getFormRow(scope).detailList || []" border size="small">
<!-- 嵌套表格 ... -->
</el-table>
</div>
</template>弹窗 vs 抽屉
// 全局配置(main.ts 中已设置)默认为抽屉
dialogType: 'drawer'
dialogDirection: 'rtl' // 从右侧滑出
dialogWidth: 800 // 抽屉宽度 (px)
// 某个页面需要弹窗,在该页面 option 中覆盖
const option = {
dialogType: 'dialog', // 覆盖为弹窗
dialogWidth: '560px',
}完整页面示例(角色管理)
参考
src/pages/role/index.vue,完整流程:搜索 → 分页 → 新增(弹窗) → 编辑(加载详情) → Switch 状态切换 → 删除。
option(roleCrudOption.ts)
└─ column[]: label, prop, type, search, rules, slot, dicData ...
└─ 全局设置: page, border, menuWidth, addTitle, editTitle ...
<avue-crud>
├─ :data="tableData" ← 表格数据
├─ :option="option" ← 配置对象
├─ :page="page" ← 分页状态
├─ :search="searchForm" ← 搜索栏数据双向绑定
├─ :before-open="beforeOpen"← 编辑前加载详情
├─ @row-save ← 新增保存
├─ @row-update ← 编辑保存
├─ @search-change ← 搜索
├─ @search-reset ← 重置
├─ @current-change ← 翻页
├─ @size-change ← 每页条数
├─ #menu-left ← 自定义新增按钮
├─ #status="{ row }" ← 自定义状态列
└─ #menu="{ row }" ← 自定义操作列(编辑、删除、配置权限)常见问题
| 问题 | 解决方案 |
|---|---|
| 搜索后树形不展开 | 用 :key 绑定变化的 renderKey,触发强制重渲染 |
| 编辑弹窗显示旧数据 | 使用 before-open 钩子从接口加载最新数据并 Object.assign(crud.tableForm, ...) |
done() 不调用导致弹窗卡住 | catch 块中用 hideLoading?.() 代替 done(),或在 finally 中视情况调用 |
| 新增/编辑的字段默认值 | 在 column 中设置 value: xxx(如 value: 0 表示状态默认启用) |
slot: true 后表单也被插槽替换 | 列表插槽 #prop 只控制列表;表单用 #prop-form 单独控制 |
| option 动态修改不生效 | 将 option 改为 computed 返回,或整体替换对象引用 |
