el-plus-crud
Version:
采用Vue3 + TS,封装的element-plus数据驱动表单、列表组件
348 lines (288 loc) • 10.2 kB
Markdown
name: el-plus-crud-patterns
description: Proven patterns and recipes for common CRUD scenarios — complete CRUD page, search-reset workflow, master-detail, custom components, and permission control
version: 1.0.0
origin: el-plus-crud
tags: [vue3, element-plus, patterns, recipes, crud-workflow]
# Best Practices — Common CRUD Patterns & Recipes
Practical patterns for building real-world CRUD applications with el-plus-crud. Each pattern provides a complete, copy-pasteable solution with configuration and code.
## When to Activate
- Building a complete CRUD page (list + add + edit + delete)
- Implementing master-detail or one-to-many editing
- Integrating custom form components
- Solving cross-component coordination problems
- Asking "how to implement X with el-plus-crud"
## Pattern 1: Standard CRUD Page
The most common pattern: `ElPlusTable` (list) + `ElPlusFormDialog` (add/edit) + toolbar search.
```html
<template>
<div class="crud-page">
<ElPlusTable ref="tableRef" :tableConfig="tableConfig" />
<ElPlusFormDialog
v-model:show="showDialog"
v-model="formData"
:formDesc="formDesc"
:requestFn="api.create"
:updateFn="api.update"
:tableRef="tableRef"
:title="formData.id ? '编辑' : '新增'"
/>
</div>
</template>
```
**Key points:**
- Same `formDesc` for add and edit
- `formData.id` presence determines `requestFn` vs `updateFn`
- `tableRef` enables automatic table reload after submit
## Pattern 2: Grouped Form Dialog (formGroup mode)
Use `ElPlusFormDialog` with `formGroup` prop for multi-section form dialogs. Same dialog lifecycle as formDesc mode, but organizes fields into titled groups.
```html
<template>
<ElPlusTable ref="tableRef" :tableConfig="tableConfig" />
<ElPlusFormDialog
v-model:show="showDialog"
v-model="formData"
:formGroup="formGroupConfig"
title="编辑用户信息"
:tableRef="tableRef"
/>
</template>
<script setup lang="ts">
import type { IFormGroupConfig } from 'el-plus-crud'
const formGroupConfig: IFormGroupConfig = {
column: 2,
requestFn: api.saveUser,
successTip: '保存成功!',
group: [
{
title: '基本信息',
formDesc: {
name: { type: 'input', label: '姓名', required: true },
phone: { type: 'input', label: '手机号', rules: 'phone' }
}
},
{
title: '地址信息',
formDesc: {
province: { type: 'area', label: '地区', required: true },
address: { type: 'input', label: '详细地址' }
}
}
]
}
</script>
```
**Key points:**
- Pass `formGroup` instead of `formDesc` to switch to grouped mode
- `tableRef` auto-reload works identically to formDesc mode
- Success callback priority: `props.success` → `formGroup.success` → default (reload + close)
- Same dialog configuration (width, draggable, etc.) applies
## Pattern 3: Search-Reset-Pagination Workflow
```typescript
const tableConfig: ITableConfig = {
fetch: api.getList,
toolbar: {
collapsible: true,
formConfig: {
formDesc: {
keyword: { type: 'input', label: '关键词', width: '180px' },
status: { type: 'select', label: '状态', width: '150px', options: [{ l: '启用', v: 1 }, { l: '禁用', v: 0 }] },
dateRange: { type: 'daterange', label: '时间范围', width: '280px', startTimeKey: 'startDate', endTimeKey: 'endDate' }
},
column: 3,
collapsedRows: 1,
beforeRequest: (data) => {
// Transform search params
if (data.keyword) { data.name = data.keyword; delete data.keyword }
return data
}
},
btns: [{ label: '新增', type: 'primary', icon: 'Plus', on: { click: handleAdd } }]
},
column: [...]
}
```
**Key points:**
- `collapsible: true` + `collapsedRows: 1` shows first row of search fields with expand button
- `beforeRequest` transforms search params before API call
- `daterange` auto-splits into `startDate`/`endDate`
- Set fixed `width` on each search field for consistent layout
## Pattern 4: Delete with Confirmation
```typescript
// In column btns array:
{
type: 'btns', label: '操作', width: '200', fixed: 'right',
btns: [
{
label: '编辑', type: 'primary', text: true,
on: { click: (row: any) => { formData.value = { ...row }; showDialog.value = true } }
},
{
label: '删除', type: 'danger', text: true,
confirm: '确定删除该记录?',
on: { click: async (row: any) => {
await api.deleteById(row.id)
tableRef.value.reload()
}}
}
]
}
```
**Key point:** `confirm` property shows confirmation dialog before executing `on.click`.
## Pattern 5: Batch Selection + Batch Operations
```html
<ElPlusTable
type="selection"
ref="tableRef"
:tableConfig="tableConfig"
:selectList="selectedRows"
@selection="onSelectionChange"
/>
```
```typescript
const selectedRows = ref([])
const onSelectionChange = (rows: any[]) => {
selectedRows.value = rows
}
const handleBatchDelete = async () => {
if (!selectedRows.value.length) return ElMessage.warning('请先选择记录')
const ids = selectedRows.value.map(r => r.id)
await api.batchDelete({ ids })
ElMessage.success('删除成功')
tableRef.value.reload()
tableRef.value.resetSelect()
}
```
**Key point:** Cross-page selection is maintained automatically in `allSelectRowList`.
## Pattern 6: Form Field Linking
Field A changes → Field B's options/visibility updates.
```typescript
const formDesc: IFormDesc = {
province: {
type: 'select', label: '省份', required: true,
options: provinceList,
on: {
change: (val: any, formData: any) => {
// Clear city and load new options
formData.city = null
loadCityOptions(val)
}
}
},
city: {
type: 'select', label: '城市', required: true,
options: () => cityOptions.value,
vif: (formData) => !!formData.province
}
}
```
**Key points:**
- Use `on.change` to respond to field value changes
- Set `vif` as function to show/hide dependent fields
- Use function-form `options` for dynamically loaded data
## Pattern 7: Custom Form Component
```typescript
// 1. Register in ICRUDConfig
app.use(elPlusCrud, {
form: { comList: ['mapselector'] } // registers el-plus-form-mapselector
})
// 2. Create component file with standard interface
// ElPlusFormMapselector.vue
<script setup lang="ts">
const props = defineProps<{
modelValue?: any
field?: string
desc?: any
formData?: any
disabled?: boolean
}>()
const emit = defineEmits(['update:modelValue'])
</script>
// 3. Use in formDesc
{ type: 'mapselector', label: '选择位置' }
```
**Key points:**
- Component name must be `ElPlusForm{TypeName}` (PascalCase)
- Must accept standard props: `modelValue`, `field`, `desc`, `formData`, `disabled`
- Must emit `update:modelValue`
## Pattern 8: Permission Control
```typescript
// 1. Configure auth function
app.use(elPlusCrud, {
auth: (perm: string) => useUserStore().permissions.includes(perm)
})
// 2. Use auth on form fields (hides field if no permission)
const formDesc: IFormDesc = {
name: { type: 'input', label: '姓名', auth: 'user:name' },
salary: { type: 'nbinput', label: '薪资', auth: 'user:salary' }
}
// 3. Use auth on table columns (hides column if no permission)
const columns: Array<IColumnItem> = [
{ prop: 'name', label: '姓名', auth: 'user:name' },
{ prop: 'salary', label: '薪资', auth: 'user:salary' }
]
// 4. Use auth on action buttons
btns: [
{ label: '编辑', auth: 'user:edit', on: { click: handleEdit } },
{ label: '删除', auth: 'user:delete', on: { click: handleDelete } }
]
```
## Pattern 9: Extract Config to Separate Files
```
views/user/
├── UserPage.vue # Component template only
├── config/
│ ├── formDesc.ts # Form field descriptors
│ ├── tableConfig.ts # Table configuration
│ └── api.ts # API functions
└── composables/
└── useUserCrud.ts # Composable for CRUD logic
```
```typescript
// composables/useUserCrud.ts
export function useUserCrud() {
const tableRef = ref()
const showDialog = ref(false)
const formData = ref({})
const handleAdd = () => { formData.value = {}; showDialog.value = true }
const handleEdit = (row: any) => { formData.value = { ...row }; showDialog.value = true }
const handleDelete = async (id: string) => {
await api.deleteById(id)
tableRef.value.reload()
}
return { tableRef, showDialog, formData, handleAdd, handleEdit, handleDelete }
}
```
## Anti-Patterns
### FAIL: Rebuild formDesc on every render
```typescript
// FAIL: Creating new object every render triggers unnecessary re-computation
const getFormDesc = () => ({ name: { type: 'input', label: '名称' } })
```
### PASS: Define once, use reactive properties for dynamics
```typescript
const formDesc: IFormDesc = {
name: { type: 'input', label: '名称', disabled: () => isEdit.value }
}
```
### FAIL: Put complex logic in template
```html
<!-- FAIL: Template should be declarative -->
<ElPlusForm v-if="mode === 'add' && !loading && hasPermission" ... />
```
### PASS: Control via configuration properties
```typescript
formDesc.name.vif = (formData) => mode.value === 'add' && hasPermission.value
```
## Best Practices
1. Extract `formDesc` / `tableConfig` into independent `.ts` files, import in component
2. Use composable functions (`useCrud.ts`) to encapsulate standard CRUD logic
3. Complex forms → use `ElPlusFormGroup` for sections; simple forms → use `ElPlusForm` directly
4. Decouple search from listing: `toolbar.formConfig` handles query, `tableConfig.fetch` handles request
5. Configure `ICRUDConfig` globally once in `main.ts` — never repeat upload/auth/format config per component
6. Use `tbName` on all business tables for column visibility persistence
## Related Skills
- `el-plus-crud-form` — all patterns involve formDesc configuration (indirect)
- `el-plus-crud-table` — all patterns involve tableConfig configuration (indirect)
- `el-plus-crud-config` — global configuration is prerequisite for all patterns (indirect)