el-plus-crud
Version:
采用Vue3 + TS,封装的element-plus数据驱动表单、列表组件
501 lines (390 loc) • 16.5 kB
Markdown
---
name: el-plus-crud-form
description: Data-driven form component with 40+ field types, dynamic visibility, multi-column layout, submit lifecycle hooks, and field-level linking
version: 1.0.0
origin: el-plus-crud
tags: [vue3, element-plus, form, data-driven, dynamic-form]
---
# ElPlusForm — Data-Driven Form
ElPlusForm is the core form component. Declare form fields through an `IFormDesc` object, supporting 40+ built-in field types, multi-column layout, dynamic visibility (vif/disabled as functions), field linking, custom component extension, and a complete submit lifecycle.
## When to Activate
- Using `<ElPlusForm>` component
- Writing or modifying `IFormDesc` / `IFormDescItem` objects
- Configuring form field types (input, select, upload, daterange, etc.)
- Implementing field linking (inter-field dependencies, dynamic visibility)
- Calling form expose methods: `submit` / `validate` / `reset` / `clear` / `getData`
## Core Concepts
### IFormDesc Structure
Key = field name (matches backend field), value = `IFormDescItem`.
```typescript
const formDesc: IFormDesc = {
name: { type: 'input', label: '姓名', required: true },
phone: { type: 'input', label: '手机号', rules: 'phone' },
role: { type: 'select', label: '角色', options: roleList }
}
```
### 40+ Built-in Field Types
| Category | Types |
|---|---|
| **Input** | `input`, `number`, `nbinput`, `password`, `textarea`, `quickinput`, `percentinput` |
| **Selection** | `select`, `radio`, `checkbox`, `checkboxbutton`, `switch`, `slider`, `rate`, `transfer` |
| **Date/Time** | `date`, `datetime`, `daterange`, `datetimerange`, `time`, `timerange` |
| **Upload** | `upload`, `image`, `file`, `upbtn` |
| **Tree/Cascade** | `tree`, `tselect` (tree-select), `cascader`, `cascaderpanel` |
| **Display** | `text`, `tag`, `status`, `link`, `rate`, `color` |
| **Advanced** | `area`, `autocomplete`, `lkuser`, `formula` |
| **Action** | `btn`, `btns` |
### Dynamic Properties
These properties accept functions for reactive behavior:
| Property | Function Signature | Description |
|---|---|---|
| `vif` | `(formData, fieldValue) => boolean` | Show/hide field |
| `disabled` | `(formData, fieldValue) => boolean` | Enable/disable field |
| `label` | `(formData, fieldValue) => string` | Dynamic label |
| `attrs` | `(formData, fieldValue) => object` | Dynamic attributes |
| `tip` | `(formData, fieldValue) => string` | Dynamic hint text |
| `style` | `(formData, fieldValue) => object` | Dynamic styles |
| `required` | `(formData) => boolean` | Dynamic mandatory |
When any property is a function, the form automatically enables deep watching on `modelValue` (`isOpenListen = true`).
### Multi-Column Layout
Uses a 24-grid system via `el-row` / `el-col`.
```typescript
// 2-column form
<ElPlusForm :column="2" :formDesc="formDesc" />
// Field spanning 2 columns
{ type: 'textarea', label: '描述', colspan: 2 }
```
### Submit Lifecycle
```
beforeValidate → validate → beforeRequest → requestFn/updateFn → success → requestError → requestEnd
```
- `beforeValidate(data)`: Return false to cancel. Can be async.
- `beforeRequest(data)`: Return false to cancel, return object to replace data. Can be async.
- `requestFn(data)`: Create request. If `updateFn` exists and data has `idKey`, calls `updateFn` instead.
- `success({ response, formData, callBack })`: Success callback. `callBack()` resets loading.
- `requestError(error)`: Error handler.
- `requestEnd()`: Always called (finally block).
These hooks are passed via `v-bind` (attrs), not as props:
```html
<ElPlusForm
:formDesc="formDesc"
v-model="formData"
:requestFn="api.create"
:updateFn="api.update"
:beforeValidate="beforeValidate"
:beforeRequest="beforeRequest"
:success="onSuccess"
/>
```
### Data Transformation — Special Types
The form automatically transforms data for special types during `getData()`:
| Type | Transformation |
|---|---|
| `daterange` | Splits into `startTime`/`endTime` (customizable via `startTimeKey`/`endTimeKey`) |
| `datetimerange` | Same as daterange, preserves time component |
| `timerange` | Splits into `startTime`/`endTime` with `HH:mm:ss` format |
| `area` | Splits into `provinceId`/`cityId`/`zoneId`/`streetId` |
| `checkbox` | Joins array with comma: `[1,2,3]` → `'1,2,3'` |
| `linkuser` | Splits into `userIds`/`deptIds`/`userNames`/`deptNames` |
### Internal Properties (prefixed with `_`)
Do NOT set these manually — they are computed internally:
- `_type`: Resolved component name (e.g., `'el-plus-form-input'`)
- `_vif`: Computed visibility
- `_disabled`: Computed disabled state
- `_attrs`: Merged attributes
- `_label`: Computed label
- `_tip`: Computed hint text
### Expose Methods
```typescript
const formRef = ref()
formRef.value.submit() // Trigger submit lifecycle
formRef.value.validate() // Validate form (returns Promise)
formRef.value.getData() // Get transformed form data
formRef.value.reset() // Reset to initial values
formRef.value.clear() // Clear all data and validation
formRef.value.clearValid() // Clear validation only
formRef.value.refresh() // Re-compute dynamic properties
formRef.value.init() // Init child components
formRef.value.changeValidImg() // Refresh captcha image
```
### Mixins System
Each form sub-component receives these via mixins:
- **getAttrs**: Merges `desc.attrs`, auto-generates `placeholder`, handles `multiple`
- **getEvents**: Wraps events, injects `formData` and `rowIndex` into callbacks
### Display Types — tag
The `tag` type renders an `el-tag` for read-only status display. It does NOT support user input — the value comes from `modelValue`. Two key properties control its behavior:
#### tagType — Controls Tag Color
Determines the `type` attribute of `el-tag` (controls color theme).
| Value Type | Behavior | Example |
|---|---|---|
| **Not set** | Default `'primary'` (blue) | — |
| **Function** `(value, formData, field) => string` | Dynamic color based on value | `tagType: (v) => v === 1 ? 'success' : 'danger'` |
| **String** (format key) | Calls `format[tagType](value, formData, field)` | `tagType: 'statusColor'` |
Supported el-tag types: `'primary'` | `'success'` | `'warning'` | `'danger'` | `'info'`
Rendering rule: if `tagType` is falsy or equals `'--'`, falls back to `'primary'`.
#### format — Controls Display Text
Determines the text shown inside the tag.
| Value Type | Behavior | Example |
|---|---|---|
| **Not set** | Shows raw `modelValue`, empty/null shows `'-'` | — |
| **Function** `(value, formData, field) => string` | Dynamic text | `format: (v) => v === 1 ? '启用' : '禁用'` |
| **String** (format key) | Calls `format[formatKey](value, formData, field)` | `format: 'statusLabel'` |
#### attrs — Pass-through to el-tag
Any `el-tag` native attribute can be set via `attrs`:
```typescript
attrs: { effect: 'dark', round: true, size: 'small' }
```
#### tag in Table Columns
When used in `ITableConfig.column`, tag also supports the `color` property for custom colors:
| color Value Type | Behavior |
|---|---|
| **String** | Fixed custom color: `'#67C23A'` |
| **Function** `(value, row, field) => string` | Dynamic color per row |
| **Array** `['#67C23A', '#F56C6C']` | Color per value index (multi-prop columns) |
### isTable Mode
When `isTable=true`, the form renders as a horizontal search bar (used in table toolbar):
```typescript
// Auto-set: horizontal layout, fixed widths, no submit buttons
<ElPlusForm isTable :formDesc="searchDesc" v-model="queryData" />
```
## Code Examples
### Basic Form
```typescript
const formData = ref({})
const formDesc: IFormDesc = {
name: { type: 'input', label: '姓名', required: true },
phone: { type: 'input', label: '手机号', rules: 'phone' },
email: { type: 'input', label: '邮箱', rules: 'email' },
role: { type: 'select', label: '角色', options: [{ l: '管理员', v: 1 }, { l: '用户', v: 2 }] }
}
```
```html
<ElPlusForm v-model="formData" :formDesc="formDesc" :requestFn="api.createUser" />
```
### Multi-Column with Colspan
```html
<ElPlusForm :column="2" v-model="formData" :formDesc="formDesc" />
```
```typescript
const formDesc: IFormDesc = {
name: { type: 'input', label: '姓名' },
phone: { type: 'input', label: '手机号' },
remark: { type: 'textarea', label: '备注', colspan: 2 } // spans full width
}
```
### Vertical Spanning with Rowspan
When any visible field sets `rowspan`, that group switches to a CSS Grid layout so an item can span multiple rows. Other fields keep flowing around it.
```html
<ElPlusForm :column="3" v-model="formData" :formDesc="formDesc" />
```
```typescript
const formDesc: IFormDesc = {
avatar: { type: 'image', label: '头像', rowspan: 2 }, // spans 2 rows
name: { type: 'input', label: '姓名' },
phone: { type: 'input', label: '手机号' },
email: { type: 'input', label: '邮箱' },
address: { type: 'input', label: '地址' }
}
```
Layout — `avatar` occupies the left column across 2 rows, the rest flow around it:
```
┌──────┬──────┬──────┐
│ │ 姓名 │ 手机 │
│ 头像 ├──────┼──────┤
│ │ 邮箱 │ 地址 │
└──────┴──────┴──────┘
```
**Limitations**
- Only effective with `column >= 2`; on a single-column form and on mobile/narrow screens it degrades to normal stacking.
- Ignored in `isTable` mode (inline search bar).
- `maxShowRowIndex` (collapse first N rows) is not supported in Grid mode — all rows are shown.
- `colspan` and `rowspan` can be combined on the same field.
### Dynamic Visibility and Linking
```typescript
const formDesc: IFormDesc = {
isCompany: { type: 'switch', label: '是否企业' },
companyName: {
type: 'input', label: '企业名称',
vif: (formData) => !!formData.isCompany,
required: (formData) => !!formData.isCompany
},
creditCode: {
type: 'input', label: '信用代码',
vif: (formData) => !!formData.isCompany,
disabled: (formData) => !formData.isVerified
}
}
```
### Options — Three Sources
```typescript
const formDesc: IFormDesc = {
// 1. Static array
status: { type: 'select', label: '状态', options: [{ l: '启用', v: 1 }, { l: '禁用', v: 0 }] },
// 2. Function returning array
roles: { type: 'select', label: '角色', options: () => roleStore.list },
// 3. String key → fetched from globalData
types: { type: 'select', label: '类型', options: 'typeOptions' }
}
```
### Complete Submit Lifecycle
```html
<ElPlusForm
v-model="formData"
:formDesc="formDesc"
:requestFn="api.createUser"
:updateFn="api.updateUser"
:beforeValidate="handleBeforeValidate"
:beforeRequest="handleBeforeRequest"
:success="handleSuccess"
:requestError="handleRequestError"
/>
```
```typescript
const handleBeforeValidate = (data: any) => {
// Return false to cancel submit
return true
}
const handleBeforeRequest = (data: any) => {
// Modify data before request, return false to cancel
data.timestamp = Date.now()
return data
}
const handleSuccess = ({ response, formData, callBack }: IFormBack) => {
ElMessage.success('保存成功')
callBack() // resets loading state
}
```
### daterange Data Transformation
```typescript
const formDesc: IFormDesc = {
dateRange: {
type: 'daterange',
label: '时间范围',
startTimeKey: 'startDate', // default: 'startTime'
endTimeKey: 'endDate', // default: 'endTime'
valueFormat: 'YYYY-MM-DD' // passed to time() formatter
}
}
// getData() returns: { startDate: '2024-01-01', endDate: '2024-01-31' }
```
### Tag Display — Status Column
```typescript
// === In formDesc (form detail / isTable mode) ===
const formDesc: IFormDesc = {
status: {
type: 'tag',
label: '状态',
// tagType: function — dynamic color based on value
tagType: (value) => {
if (value === 1) return 'success' // green
if (value === 0) return 'danger' // red
if (value === 2) return 'warning' // yellow
return 'info' // gray
},
// format: function — dynamic display text
format: (value) => {
const map = { 0: '禁用', 1: '启用', 2: '待审核' }
return map[value] || '未知'
},
// attrs: pass-through to el-tag
attrs: { effect: 'dark', size: 'small' }
}
}
// === In tableConfig.column ===
const columns: Array<IColumnItem> = [
{
prop: 'status', label: '状态', type: 'tag',
tagType: (value) => value === 1 ? 'success' : 'danger',
format: (value) => value === 1 ? '启用' : '禁用'
}
]
// === Using global format functions ===
// main.ts
const formatObj = {
statusColor: (value) => ({ 0: 'danger', 1: 'success', 2: 'warning' }[value] || 'info'),
statusLabel: (value) => ({ 0: '禁用', 1: '启用', 2: '待审核' }[value] || '未知')
}
app.use(elPlusCrud, config, formatObj, globalDataObj)
// formDesc — reference by string key
{
type: 'tag', label: '状态',
tagType: 'statusColor', // calls format.statusColor(value, formData, field)
format: 'statusLabel' // calls format.statusLabel(value, formData, field)
}
// === Table column with custom color ===
{
prop: 'status', label: '状态', type: 'tag',
color: (value, row, field) => value === 1 ? '#67C23A' : '#F56C6C',
format: (value) => value === 1 ? '在线' : '离线'
}
```
### Custom Component Extension
```typescript
// main.ts — register custom component type
app.use(elPlusCrud, {
form: { comList: ['custompicker'] } // registers el-plus-form-custompicker
})
// Create component file: ElPlusFormCustompicker.vue
// Use in formDesc:
{ type: 'custompicker', label: '自定义选择器' }
```
## Anti-Patterns
### FAIL: Use options to control tag display
```typescript
// FAIL: tag does NOT use options — it ignores them completely
{ type: 'tag', label: '状态', options: [{ l: '启用', v: 1 }, { l: '禁用', v: 0 }] }
```
### PASS: Use format for text, tagType for color
```typescript
// tag uses format (display text) + tagType (color), not options
{ type: 'tag', label: '状态', tagType: (v) => v === 1 ? 'success' : 'danger', format: (v) => v === 1 ? '启用' : '禁用' }
```
### FAIL: Manually set internal properties
```typescript
// FAIL: _type, _vif etc. are computed internally
formDesc.name._type = 'el-plus-form-input'
formDesc.name._vif = true
```
### PASS: Set declarative properties, let component handle internals
```typescript
formDesc.name.type = 'input'
formDesc.name.vif = (formData) => !!formData.showName
```
### FAIL: Async operations in vif/disabled functions
```typescript
// FAIL: vif/disabled are synchronous, called on every data change
vif: async (formData) => {
const result = await checkPermission(formData.id)
return result
}
```
### PASS: Use reactive variable for async conditions
```typescript
const showField = ref(false)
checkPermission(data.id).then(r => showField.value = r)
// vif can accept boolean directly
formDesc.name.vif = showField
```
### FAIL: Manually handle daterange splitting
```typescript
// FAIL: don't manually process daterange values
const data = formRef.value.getData()
data.startTime = data.dateRange[0]
data.endTime = data.dateRange[1]
```
### PASS: Use startTimeKey/endTimeKey, component handles it automatically
```typescript
{ type: 'daterange', label: '时间范围', startTimeKey: 'startTime', endTimeKey: 'endTime' }
// getData() already returns { startTime: ..., endTime: ... }
```
## Best Practices
1. Keep `formDesc` key names aligned with backend field names — minimize mapping in `beforeRequest`
2. Use function form for `vif`/`disabled` to enable reactive field linking — component auto-enables deep watch
3. Use `descOrders` array to control field display order instead of relying on object key insertion order
4. In `isTable` mode, set fixed `width` on each field to prevent search bar overflow
5. Always configure `upload` in `ICRUDConfig` globally rather than per-field
6. For 3+ daterange components in one form, use `propPrefix` to avoid key collisions
## Related Skills
- `el-plus-crud-config` — IFormDesc, IFormDescItem, IFormConfig type definitions (direct)
- `el-plus-crud-validation` — rules and required validation configuration (direct)
- `el-plus-crud-table` — table toolbar search bar uses ElPlusForm isTable mode (indirect)